diff --git a/.changeset/academy-unrest-linear.md b/.changeset/academy-unrest-linear.md new file mode 100644 index 000000000..e027328a3 --- /dev/null +++ b/.changeset/academy-unrest-linear.md @@ -0,0 +1,61 @@ +--- +'@equinor/fusion-react-stepper': major +'@equinor/fusion-react-components-stories': patch +--- + +Changed implementation of the Stepper component from being uncontrolled to having a both an uncontrolled and controlled mode. + +- Required 'activeStepKey' prop removed. Replaced with 'initialStepKey' and 'stepKey' (both optional) +- If 'stepKey' is set, then the component becomes controlled by that prop. +- If 'stepKey' is set and 'initialStepKey' is set, then the initial step will be that prop, but otherwise be uncontrolled. +- If none of these props are set then it will be uncontrolled, and default to the first step of the steps provided as the initial step. +- Updated 'Stepper' README and Storybook with examples of usage. + +This is a breaking change since the 'activeStepKey' was required, and now needs to be either removed or replaced with one of the two new optional props. + +## Migration guide: + +### Uncontrolled usage: +```tsx +import { Stepper } from '@equinor/fusion-react-stepper'; + +/** initialStepKey here is optional. If not set it defaults to the first step ('step1') */ + console.log('active: ', e, ' keys: ', k)} props> + + Step content 1 + + + Step content 2 + + + Step content 3 + + +``` + +### Controlled usage: +```tsx +import { Stepper } from '@equinor/fusion-react-stepper'; + +const [activeStep, setActiveStep] = useState('step1'); +const onChangeStep = (stepKey: string, allSteps: StepKey[]) => { + console.log('active: ', stepKey, ' keys: ', allSteps) + if (activeStep !== stepKey) { + setActiveStep(String(stepKey)); + } +}; + +return ( + + + Step content 1 + + + Step content 2 + + + Step content 3 + + +); +``` diff --git a/packages/stepper/README.md b/packages/stepper/README.md index 01c11f55c..5d191ebb0 100644 --- a/packages/stepper/README.md +++ b/packages/stepper/README.md @@ -1,5 +1,5 @@ -# @equinor/fusion-react-stepper +# @equinor/fusion-react-stepper [![Published on npm](https://img.shields.io/npm/v/@equinor/fusion-react-stepper.svg)](https://www.npmjs.com/package/@equinor/fusion-react-stepper) @@ -9,30 +9,32 @@ ### for stepper -Name | Type | Default | Description -------------------------- | ----------------------------- | ----------------------------- | ----------- -`activeStepKey` | `string` | `/` | Select/change active stepp, use *** stepKey ***. `required` -`forceOrder` | `boolean` | `false` | Can't skip steps. Steps will have specific order. -`verticalSteps` | `boolean` | `false` | Change stepper layout to vertical. Vertical positioning of steps. -`hideNavButtons` | `boolean` | `false` | Show/hide next and previous navigation buttons for stepper. -`onChange` | `(stepKey: string) => void` | | onChange event for active step. +Name | Type | Default | Description +------------------------- |-------------------------------| ----------------------------- | ----------- +`initialStepKey` | `string / undefined` | `/` | Sets the initial step key for an uncontrolled stepper component. If undefined the stepper will use the first step as initial step. +`stepKey` | `string / undefined` | `/` | Used for making the stepper a controlled component. If it is set then it is the property that decides the currentStep and initialStepKey is ignored. +`forceOrder` | `boolean` | `false` | Can't skip steps. Steps will have specific order. +`verticalSteps` | `boolean` | `false` | Change stepper layout to vertical. Vertical positioning of steps. +`hideNavButtons` | `boolean` | `false` | Show/hide next and previous navigation buttons for stepper. +`onChange` | `(stepKey: string, allSteps: StepKey[]) => void` | | onChange event for active step. ### for step Name | Type | Default | Description ------------------------- | ----------------------------- | ----------------------------- | ----------- -`stepKey` | `string` | `/` | Step key of step. Used for *** activeStepKey ***. `required` +`stepKey` | `string` | `/` | Step key of step. Used for ***activeStepKey***. `required` `title` | `string` | `/` | Title of step -`description` | `string` | `/` | Description of step +`description` | `string` | `/` | Description of step `disabled` | `boolean` | `false` | Disable step. Can't be clicked, but can manually navigate to it - ## Example Usage +### Uncontrolled usage + ```tsx import { Stepper } from '@equinor/fusion-react-stepper'; - console.log('active: ', e)} props> + console.log('active: ', e, ' keys: ', k)} props> Step content 1 @@ -45,4 +47,30 @@ import { Stepper } from '@equinor/fusion-react-stepper'; ``` - +### Controlled usage + +```tsx +import { Stepper } from '@equinor/fusion-react-stepper'; + +const [activeStep, setActiveStep] = useState('step1'); +const onChangeStep = (stepKey: string, allSteps: StepKey[]) => { + console.log('active: ', stepKey, ' keys: ', allSteps) + if (activeStep !== stepKey) { + setActiveStep(String(stepKey)); + } +}; + +return ( + + + Step content 1 + + + Step content 2 + + + Step content 3 + + +); +``` diff --git a/packages/stepper/src/Stepper.tsx b/packages/stepper/src/Stepper.tsx index 60755759d..5c710b49c 100644 --- a/packages/stepper/src/Stepper.tsx +++ b/packages/stepper/src/Stepper.tsx @@ -1,4 +1,13 @@ -import { useState, useEffect, useCallback, PropsWithChildren, createContext, useContext } from 'react'; +import { + useState, + useEffect, + useCallback, + PropsWithChildren, + createContext, + useContext, + Children, + ReactElement, +} from 'react'; import { findNextAvailable, findPrevAvailable, getSteps } from './utils'; import StepperContent from './StepperContent'; @@ -6,7 +15,8 @@ import StepperContent from './StepperContent'; export type StepperProps = { readonly onChange?: (stepKey: string, allSteps: StepKey[]) => void; readonly forceOrder?: boolean; - readonly activeStepKey: string; + readonly initialStepKey?: string; + readonly stepKey?: string; readonly hideNavButtons?: boolean; readonly verticalSteps?: boolean; readonly horizontalTitle?: boolean; @@ -21,6 +31,8 @@ export type StepKey = { done: boolean; }; +export type StepKeys = StepKey[]; + /** Create context for Stepper */ type StepperContextType = { verticalSteps?: boolean; @@ -46,7 +58,8 @@ export const useStepperContext = () => { export const Stepper = ({ children, - activeStepKey, + initialStepKey, + stepKey, forceOrder, onChange, hideNavButtons, @@ -56,7 +69,10 @@ export const Stepper = ({ }: PropsWithChildren): JSX.Element => { /** State to manage step keys, current step key, and active step position */ const [stepKeys, setStepKeys] = useState([]); - const [currentStepKey, setCurrentStepKey] = useState(activeStepKey); + /** Fallback to key of first step if stepKey/initialStepKey is not set */ + const [currentStepKey, setCurrentStepKey] = useState( + stepKey ?? initialStepKey ?? (Children.toArray(children)[0] as ReactElement).props.stepKey, + ); const [activeStepPosition, setActiveStepPosition] = useState(0); /** State to manage navigation button availability */ @@ -69,10 +85,10 @@ export const Stepper = ({ setStepKeys(steps); }, [children]); - /** Effect to update currentStepKey when activeStepKey changes */ + /** Effect to update currentStepKey when stepKey changes */ useEffect(() => { - setCurrentStepKey(activeStepKey); - }, [activeStepKey]); + stepKey && setCurrentStepKey(stepKey); + }, [stepKey]); /** Effect to update activeStepPosition, canNext, and canPrev when stepKeys or currentStepKey change */ useEffect(() => { @@ -90,11 +106,12 @@ export const Stepper = ({ /** Callback to handle step change */ const handleChange = useCallback( - (stepKey: string, allSteps: StepKey[]) => { - setCurrentStepKey(stepKey); - onChange && onChange(stepKey, allSteps); + (newStepKey: string, allSteps: StepKey[]) => { + /** If stepKey is undefined we call setCurrentStepKey here since it is then an uncontrolled component */ + !stepKey && setCurrentStepKey(newStepKey); + onChange && onChange(newStepKey, allSteps); }, - [onChange], + [onChange, stepKey], ); return ( diff --git a/packages/stepper/src/index.tsx b/packages/stepper/src/index.tsx index 14cf2d940..47498757e 100644 --- a/packages/stepper/src/index.tsx +++ b/packages/stepper/src/index.tsx @@ -1,3 +1,4 @@ export { Step, StepProps } from './Step'; export { type BadgeProps } from './StepBadge'; -export { Stepper, StepperProps } from './Stepper'; +export { Stepper } from './Stepper'; +export type { StepKey, StepKeys, StepperProps } from './Stepper'; diff --git a/storybook/src/stories/stepper/stepper.mdx b/storybook/src/stories/stepper/stepper.mdx index 520c93092..4e1f73add 100644 --- a/storybook/src/stories/stepper/stepper.mdx +++ b/storybook/src/stories/stepper/stepper.mdx @@ -16,17 +16,20 @@ import * as stories from './stepper.stories'; {readme} - + -## basic - +## Basic Uncontrolled + -## vertical - +## Vertical Uncontrolled + -## controlled +## Controlled +## Controlled With External Buttons + + ; -export const basic: Story = { +export const basicUncontrolled: Story = { args: { - activeStepKey: '0', + initialStepKey: '0', onChange: (e, k) => console.log('active: ', e, ' keys: ', k), }, render: (props) => { @@ -40,17 +39,44 @@ export const basic: Story = { }, }; -export const vertical: Story = { - ...basic, +export const verticalUncontrolled: Story = { + ...basicUncontrolled, args: { - activeStepKey: '0', + initialStepKey: '0', verticalSteps: true, }, }; export const controlled: Story = { args: { - activeStepKey: '0', + initialStepKey: '0', + }, + render: (props) => { + const [activeStep, setActiveStep] = useState('0'); + const onChangeStep = (stepKey: string, allSteps: StepKeys) => { + console.log('active: ', stepKey, ' keys: ', allSteps); + if (activeStep !== stepKey) { + setActiveStep(String(stepKey)); + } + }; + faker.seed(1); + return ( + + {[...Array(5)].map((_, index) => ( + +
+

{faker.lorem.paragraphs()}

+
+
+ ))} +
+ ); + }, +}; + +export const controlledWithExternalButtons: Story = { + args: { + initialStepKey: '0', hideNavButtons: true, forceOrder: true, }, @@ -59,7 +85,7 @@ export const controlled: Story = { const [activeStep, setActiveStep] = useState(0); const steps = 5; return ( - + {[...Array(steps)].map((_, index) => (