diff --git a/packages/react/src/content/content.tsx b/packages/react/src/content/content.tsx index 6a52feddd..f47a73b45 100644 --- a/packages/react/src/content/content.tsx +++ b/packages/react/src/content/content.tsx @@ -1,6 +1,11 @@ import { cva, cx, type VariantProps } from 'cva'; import { createContext, type HTMLProps, type Ref } from 'react'; -import { type ContextValue, useContextProps } from 'react-aria-components'; +import { + type ContextValue, + Text as RACText, + type TextProps as RACTextProps, + useContextProps, +} from 'react-aria-components'; type HeadingProps = Omit, 'size'> & VariantProps & { @@ -150,6 +155,10 @@ const Caption = ({ className, ...restProps }: CaptionProps) => ( const Footer = (props: FooterProps) =>
; +type TextProps = RACTextProps; + +const Text = (props: TextProps) => ; + export { Caption, Content, @@ -159,9 +168,11 @@ export { HeadingContext, Media, MediaContext, + Text, type CaptionProps, type ContentProps, type FooterProps, type HeadingProps, type MediaProps, + type TextProps, }; diff --git a/packages/react/src/disclosure/disclosure.tsx b/packages/react/src/disclosure/disclosure.tsx index 071e391e8..11ad240ea 100644 --- a/packages/react/src/disclosure/disclosure.tsx +++ b/packages/react/src/disclosure/disclosure.tsx @@ -104,15 +104,16 @@ export const DisclosureStateContext = createContext( null, ); -const Disclosure = ({ ref: _ref, children, ..._props }: DisclosureProps) => { +const Disclosure = ({ ref: _ref, ..._props }: DisclosureProps) => { const [props, ref] = useContextProps( _props, _ref as ForwardedRef, DisclosureContext, ); + const groupState = useContext(DisclosureGroupStateContext); - let { id, ...otherProps } = props; + let { id, children, ...otherProps } = props; const defaultId = useId(); id ||= defaultId; const isExpanded = groupState diff --git a/packages/react/src/form-steps/form-steps.stories.tsx b/packages/react/src/form-steps/form-steps.stories.tsx new file mode 100644 index 000000000..a075d8444 --- /dev/null +++ b/packages/react/src/form-steps/form-steps.stories.tsx @@ -0,0 +1,1429 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { useState } from 'react'; +import { Button } from '../button'; +import { Heading, Text } from '../content'; +import { UNSAFE_Link as Link } from '../link'; +import { UNSAFE_ProgressBar as ProgressBar } from '../progress-bar'; +import { TextArea } from '../textarea'; +import { TextField } from '../textfield'; +import { UNSAFE_FormStep as FormStep, UNSAFE_FormSteps as FormSteps } from './'; + +const meta: Meta = { + title: 'FormSteps', + component: FormSteps, + parameters: { + layout: 'centered', + }, + tags: ['autodocs'], +}; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Bekrefelse + + + + Oppsummering + + + ), +}; + +export const OneCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Bekrefelse + + + + Oppsummering + + + ), +}; + +export const TwoCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Bekrefelse + + + + Oppsummering + + + ), +}; + +export const ThreeCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Bekrefelse + + + + Oppsummering + + + ), +}; + +export const FourCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Bekrefelse + + + + Oppsummering + + + ), +}; + +export const FiveCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Bekrefelse + + + + Oppsummering + + + ), +}; + +export const SixCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Bekrefelse + + + + Oppsummering + + + ), +}; + +export const SevenCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Bekrefelse + + + + Oppsummering + + + ), +}; + +// 3 Steps variants +export const ThreeSteps: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Oppsummering + + + ), +}; + +export const ThreeStepsOneCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Oppsummering + + + ), +}; + +export const ThreeStepsAllCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Oppsummering + + + ), +}; + +// 4 Steps variants +export const FourSteps: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Oppsummering + + + ), +}; + +export const FourStepsOneCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Oppsummering + + + ), +}; + +export const FourStepsTwoCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Oppsummering + + + ), +}; + +export const FourStepsAllCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Oppsummering + + + ), +}; + +// 5 Steps variants +export const FiveSteps: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Oppsummering + + + ), +}; + +export const FiveStepsOneCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Oppsummering + + + ), +}; + +export const FiveStepsTwoCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Oppsummering + + + ), +}; + +export const FiveStepsThreeCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Oppsummering + + + ), +}; + +export const FiveStepsAllCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Oppsummering + + + ), +}; + +// 6 Steps variants +export const SixSteps: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Oppsummering + + + ), +}; + +export const SixStepsOneCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Oppsummering + + + ), +}; + +export const SixStepsTwoCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Oppsummering + + + ), +}; + +export const SixStepsThreeCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Oppsummering + + + ), +}; + +export const SixStepsFourCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Oppsummering + + + ), +}; + +export const SixStepsAllCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Oppsummering + + + ), +}; + +// 7 Steps variants +export const SevenSteps: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Oppsummering + + + ), +}; + +export const SevenStepsOneCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Oppsummering + + + ), +}; + +export const SevenStepsTwoCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Oppsummering + + + ), +}; + +export const SevenStepsThreeCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Oppsummering + + + ), +}; + +export const SevenStepsFourCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Oppsummering + + + ), +}; + +export const SevenStepsFiveCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Oppsummering + + + ), +}; + +export const SevenStepsAllCompleted: Story = { + render: () => ( + + + Personalia + + + + Kontaktinformasjon + + + + Fakturainformasjon + + + + Samtykke + + + + Betalingsinformasjon + + + + Leveringsadresse + + + + Oppsummering + + + ), +}; + +// Interactive form story with 8 steps +type FormData = { + step1: { fornavn: string; etternavn: string; fodselsdato: string }; + step2: { epost: string; telefon: string }; + step3: { adresse: string; postnummer: string; poststed: string }; + step4: { samtykke: string }; + step5: { kontonummer: string; banknavn: string }; + step6: { + leveringsadresse: string; + leveringspostnummer: string; + leveringspoststed: string; + }; + step7: { kommentar: string }; + step8: Record; +}; + +const initialFormData: FormData = { + step1: { fornavn: '', etternavn: '', fodselsdato: '' }, + step2: { epost: '', telefon: '' }, + step3: { adresse: '', postnummer: '', poststed: '' }, + step4: { samtykke: '' }, + step5: { kontonummer: '', banknavn: '' }, + step6: { + leveringsadresse: '', + leveringspostnummer: '', + leveringspoststed: '', + }, + step7: { kommentar: '' }, + step8: {}, +}; + +const stepTitles: Record = { + 1: 'Personalia', + 2: 'Kontaktinformasjon', + 3: 'Fakturainformasjon', + 4: 'Samtykke', + 5: 'Betalingsinformasjon', + 6: 'Leveringsadresse', + 7: 'Bekrefelse', + 8: 'Oppsummering', +}; + +const FormWith8StepsDemo = () => { + const [currentStep, setCurrentStep] = useState(1); + const [formData, setFormData] = useState(initialFormData); + const [completedSteps, setCompletedSteps] = useState>(new Set()); + + const updateFormData = ( + step: K, + field: keyof FormData[K], + value: string, + ) => { + setFormData((prev) => ({ + ...prev, + [step]: { + ...prev[step], + [field]: value, + }, + })); + }; + + const isStepComplete = (step: number): boolean => { + switch (step) { + case 1: + return !!( + formData.step1.fornavn && + formData.step1.etternavn && + formData.step1.fodselsdato + ); + case 2: + return !!(formData.step2.epost && formData.step2.telefon); + case 3: + return !!( + formData.step3.adresse && + formData.step3.postnummer && + formData.step3.poststed + ); + case 4: + return !!formData.step4.samtykke; + case 5: + return !!(formData.step5.kontonummer && formData.step5.banknavn); + case 6: + return !!( + formData.step6.leveringsadresse && + formData.step6.leveringspostnummer && + formData.step6.leveringspoststed + ); + case 7: + return !!formData.step7.kommentar; + default: + return false; + } + }; + + const getProgressValue = (step: number): number => { + // Calculate progress based on filled fields + const calculateFieldProgress = (fields: Record): number => { + const values = Object.values(fields); + const filledCount = values.filter((v) => v.length > 0).length; + return Math.round((filledCount / values.length) * 100); + }; + + switch (step) { + case 1: + return calculateFieldProgress(formData.step1); + case 2: + return calculateFieldProgress(formData.step2); + case 3: + return calculateFieldProgress(formData.step3); + case 4: + return calculateFieldProgress(formData.step4); + case 5: + return calculateFieldProgress(formData.step5); + case 6: + return calculateFieldProgress(formData.step6); + case 7: + return calculateFieldProgress(formData.step7); + case 8: + return currentStep === 8 ? 100 : 0; + default: + return 0; + } + }; + + const handleNext = () => { + if (currentStep < 8 && isStepComplete(currentStep)) { + setCompletedSteps((prev) => new Set(prev).add(currentStep)); + setCurrentStep((prev) => prev + 1); + } + }; + + const handleGoToStep = (step: number) => () => { + if ( + step <= currentStep || + completedSteps.has(step) || + canNavigateToStep(step) + ) { + setCurrentStep(step); + // Update URL without navigation + const url = new URL(window.location.href); + url.searchParams.set('currentStep', String(step)); + window.history.pushState({}, '', url.toString()); + } + }; + + // Check if all steps before a given step are completed + const canNavigateToStep = (step: number): boolean => { + for (let i = 1; i < step; i++) { + if (!completedSteps.has(i)) { + return false; + } + } + return true; + }; + + // Check if a step should render as a Link (navigable) + const isStepNavigable = (step: number): boolean => { + // Current step is not navigable (already there) + if (step === currentStep) return false; + // Completed steps are always navigable + if (completedSteps.has(step)) return true; + // Future steps are navigable if all previous steps are completed + return canNavigateToStep(step); + }; + + const renderStepContent = () => { + switch (currentStep) { + case 1: + return ( +
+ updateFormData('step1', 'fornavn', value)} + /> + updateFormData('step1', 'etternavn', value)} + /> + + updateFormData('step1', 'fodselsdato', value) + } + /> + +
+ ); + case 2: + return ( +
+ updateFormData('step2', 'epost', value)} + /> + updateFormData('step2', 'telefon', value)} + /> + +
+ ); + case 3: + return ( +
+ updateFormData('step3', 'adresse', value)} + /> + updateFormData('step3', 'postnummer', value)} + /> + updateFormData('step3', 'poststed', value)} + /> + +
+ ); + case 4: + return ( +
+