diff --git a/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorContext.tsx b/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorContext.tsx index 89cc7a7647b..f2fa1b5f380 100644 --- a/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorContext.tsx +++ b/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorContext.tsx @@ -3,7 +3,15 @@ * */ -import React, { useContext, useEffect, useRef, useState } from 'react' +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useReducer, + useRef, + useState, +} from 'react' import Context, { ContextProps } from '../../shared/Context' import { stepIndicatorDefaultProps } from './StepIndicatorProps' import { extendPropsWithContext } from '../../shared/component-helper' @@ -95,90 +103,35 @@ export function StepIndicatorProvider({ isSidebar = false, ...restOfProps }: StepIndicatorProviderProps) { - const props = { isSidebar, ...restOfProps } + const props = useMemo(() => { + return { isSidebar, ...restOfProps } + }, [isSidebar, restOfProps]) - const data = getData(props) - const countSteps = data.length + const data = useMemo(() => { + if (typeof props.data === 'string') { + return props.data[0] === '[' ? JSON.parse(props.data) : [] + } + + return props.data || [] + }, [props]) const [hasSidebar, setHasSidebar] = useState(true) const [hideSidebar, setHideSidebar] = useState(false) - const [activeStep, setActiveStep] = useState( - getActiveStepFromProps() - ) const [openState, setOpenState] = useState(false) - const listOfReachedSteps = useRef([activeStep].filter(Boolean)).current - - const mediaQueryListener = useRef(null) - - const context = useContext(Context) - const contextValue = makeContextValue() as StepIndicatorContextValues - - // Mount and dismount - useEffect(() => { - const container = document?.getElementById( - 'sidebar__' + props.sidebar_id - ) - - setHasSidebar(Boolean(container)) - - mediaQueryListener.current = onMediaQueryChange( - { - min: '0', - max: 'medium', - }, - (hideSidebar) => { - setHideSidebar(hideSidebar) - }, - { runOnInit: true } - ) - - return () => { - if (mediaQueryListener.current) { - mediaQueryListener.current() - } - } - }, []) - - // Keeps the activeStep state updated with changes to the current_step and data props - useEffect(() => { - const currentStepFromProps = getActiveStepFromProps() - - if (currentStepFromProps !== activeStep) { - setActiveStep(currentStepFromProps) - } - }, [props.current_step, data]) - - // Keeps the listOfReachedSteps state up to date with the activeStep state - useEffect(() => { - if (!listOfReachedSteps.includes(activeStep)) { - listOfReachedSteps.push(activeStep) - } - }, [activeStep]) - - function onChangeState() { + const onChangeState = useCallback(() => { setOpenState(false) - } + }, []) - function openHandler() { + const openHandler = useCallback(() => { setOpenState(true) - } + }, []) - function closeHandler() { + const closeHandler = useCallback(() => { setOpenState(false) - } - - function getData( - props: StepIndicatorProviderProps - ): string[] | StepIndicatorDataItem[] { - if (typeof props.data === 'string') { - return props.data[0] === '[' ? JSON.parse(props.data) : [] - } - - return props.data || [] - } + }, []) - function getActiveStepFromProps() { + const getActiveStepFromProps = useCallback(() => { if (typeof data[0] === 'string') { return props.current_step } @@ -192,9 +145,31 @@ export function StepIndicatorProvider({ return itemWithCurrentStep ? dataWithItems.indexOf(itemWithCurrentStep) : props.current_step - } + }, [data, props.current_step]) + + const countSteps = data.length + const activeStepRef = useRef(getActiveStepFromProps()) + const [, forceUpdate] = useReducer(() => ({}), {}) + const setActiveStep = useCallback((step: number) => { + activeStepRef.current = step + forceUpdate() + }, []) + const listOfReachedSteps = useRef( + [activeStepRef.current].filter(Boolean) + ).current + const mediaQueryListener = useRef(null) + const context = useContext(Context) + + const updateStepTitle = useCallback( + (title: string) => { + return title + ?.replace('%step', String((activeStepRef.current || 0) + 1)) + .replace('%count', String(data?.length || 1)) + }, + [data?.length] + ) - function makeContextValue() { + const makeContextValue = useCallback(() => { const globalContext = extendPropsWithContext( props, stepIndicatorDefaultProps, @@ -215,7 +190,7 @@ export function StepIndicatorProvider({ { hasSidebar, hideSidebar, - activeStep, + activeStep: activeStepRef.current, openState, listOfReachedSteps, data, @@ -237,13 +212,66 @@ export function StepIndicatorProvider({ value.sidebarIsVisible = value.hasSidebar && !value.hideSidebar return value - } + }, [ + closeHandler, + context, + countSteps, + data, + hasSidebar, + hideSidebar, + listOfReachedSteps, + onChangeState, + openHandler, + openState, + props, + setActiveStep, + updateStepTitle, + ]) - function updateStepTitle(title: string) { - return title - ?.replace('%step', String((activeStep || 0) + 1)) - .replace('%count', String(data?.length || 1)) - } + const contextValue = makeContextValue() as StepIndicatorContextValues + + // Mount and dismount + useEffect(() => { + const container = document?.getElementById( + 'sidebar__' + props.sidebar_id + ) + + setHasSidebar(Boolean(container)) + + mediaQueryListener.current = onMediaQueryChange( + { + min: '0', + max: 'medium', + }, + (hideSidebar) => { + setHideSidebar(hideSidebar) + }, + { runOnInit: true } + ) + + return () => { + if (mediaQueryListener.current) { + mediaQueryListener.current() + } + } + }, [props.sidebar_id]) + + // Keeps the activeStep state updated with changes to the current_step and data props + useEffect(() => { + const currentStepFromProps = getActiveStepFromProps() + + if (currentStepFromProps !== activeStepRef.current) { + setActiveStep(currentStepFromProps) + } + }, [props.current_step, data, getActiveStepFromProps, setActiveStep]) + + // Keeps the listOfReachedSteps state up to date with the activeStep state + const activeStep = activeStepRef.current + useEffect(() => { + if (!listOfReachedSteps.includes(activeStep)) { + listOfReachedSteps.push(activeStep) + } + }, [activeStep, listOfReachedSteps]) if (typeof window !== 'undefined' && window['IS_TEST']) { contextValue['no_animation'] = true diff --git a/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorItem.tsx b/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorItem.tsx index e4feb039c18..abd7add0384 100644 --- a/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorItem.tsx +++ b/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorItem.tsx @@ -3,13 +3,7 @@ * */ -import React, { - HTMLProps, - useContext, - useEffect, - useRef, - useState, -} from 'react' +import React, { HTMLProps, useCallback, useContext, useMemo } from 'react' import classnames from 'classnames' import { @@ -84,67 +78,62 @@ function StepIndicatorItem({ disabled: disabled_default = false, ...restOfProps }: StepIndicatorItemProps) { - const props: StepIndicatorItemProps = { - status_state: status_state_default, - inactive: inactive_default, - disabled: disabled_default, - ...restOfProps, - } + const props: StepIndicatorItemProps = useMemo(() => { + return { + status_state: status_state_default, + inactive: inactive_default, + disabled: disabled_default, + ...restOfProps, + } + }, [ + disabled_default, + inactive_default, + restOfProps, + status_state_default, + ]) const context = useContext(StepIndicatorContext) - const [previousStep, setPreviousStep] = useState( - context.activeStep - ) - - const ref = useRef(null) - - const thisReference = { - context, - props, - onClickHandler, - } - - // Effect used to keep track of previous activeStep from context - useEffect(() => { - if (previousStep !== context.activeStep) { - setPreviousStep(context.activeStep) - } - }, [context.activeStep, previousStep]) - - function onClickHandler({ event, item, currentItemNum }) { - const params = { - event, - item, - current_step: currentItemNum, - currentStep: currentItemNum, - } - - const onClickItem = dispatchCustomElementEvent( - thisReference, - 'on_click', - params - ) + const onClickHandler = useCallback( + ({ event, item, currentItemNum }) => { + const params = { + event, + item, + current_step: currentItemNum, + currentStep: currentItemNum, + } - const onClickGlobal = dispatchCustomElementEvent( - context, - 'on_click', - params - ) + const onClickItem = dispatchCustomElementEvent( + { + context, + props, + onClickHandler, + }, + 'on_click', + params + ) + + const onClickGlobal = dispatchCustomElementEvent( + context, + 'on_click', + params + ) + + if (onClickItem === false || onClickGlobal === false) { + return // stop here + } - if (onClickItem === false || onClickGlobal === false) { - return // stop here - } + if (context.activeStep !== currentItemNum) { + context.setActiveStep(currentItemNum) + if (typeof context.onChangeState === 'function') { + context.onChangeState() + } - if (context.activeStep !== currentItemNum) { - context.setActiveStep(currentItemNum) - if (typeof context.onChangeState === 'function') { - context.onChangeState() + dispatchCustomElementEvent(context, 'on_change', params) } - - dispatchCustomElementEvent(context, 'on_change', params) - } - } + }, + [context, props] + ) const { mode, @@ -266,9 +255,7 @@ function StepIndicatorItem({ itemParams.className )} > - - {element} - + {element} {ariaLabel} diff --git a/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorModal.tsx b/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorModal.tsx index a603ab4eba7..5b5d4abc340 100644 --- a/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorModal.tsx +++ b/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorModal.tsx @@ -3,7 +3,13 @@ * */ -import React, { useContext, useEffect, useRef, useState } from 'react' +import React, { + useCallback, + useContext, + useEffect, + useRef, + useState, +} from 'react' import ReactDOM from 'react-dom' import Drawer from '../drawer/Drawer' import StepIndicatorTriggerButton from './StepIndicatorTriggerButton' @@ -23,22 +29,22 @@ function StepIndicatorModal() { ) setContainer(container) - }, []) + }, [context.sidebar_id]) - function closeHandler() { + const closeHandler = useCallback(() => { if (context.hasSidebar) { triggerRef.current?.focus() } context.closeHandler() - } + }, [context]) - function renderPortal() { + const renderPortal = useCallback(() => { if (!container) { return null } return ReactDOM.createPortal(, container) - } + }, [container]) if (context.sidebarIsVisible) { return renderPortal() diff --git a/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorSidebar.tsx b/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorSidebar.tsx index d018754abfe..0293b0c698e 100644 --- a/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorSidebar.tsx +++ b/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorSidebar.tsx @@ -3,7 +3,14 @@ * */ -import React, { useContext, useEffect, useRef, useState } from 'react' +import React, { + useCallback, + useContext, + useEffect, + useMemo, + useRef, + useState, +} from 'react' import classnames from 'classnames' import { extendPropsWithContext } from '../../shared/component-helper' @@ -45,7 +52,9 @@ function StepIndicatorSidebar({ data = stepIndicatorDefaultProps.data, ...restOfProps }: StepIndicatorSidebarProps) { - const props = { current_step, data, ...restOfProps } + const props = useMemo(() => { + return { current_step, data, ...restOfProps } + }, [current_step, data, restOfProps]) const context = useContext(Context) @@ -57,9 +66,9 @@ function StepIndicatorSidebar({ if (!props.showInitialData) { setShowInitialData(false) } - }, []) + }, [props.showInitialData]) - function getContextAndProps() { + const getContextAndProps = useCallback(() => { const providerProps = extendPropsWithContext( props, stepIndicatorDefaultProps, @@ -76,7 +85,7 @@ function StepIndicatorSidebar({ } return providerProps - } + }, [context, props]) const providerProps = showInitialData ? getContextAndProps() : null diff --git a/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorTriggerButton.tsx b/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorTriggerButton.tsx index 5c80ac5880f..47c62ce3cba 100644 --- a/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorTriggerButton.tsx +++ b/packages/dnb-eufemia/src/components/step-indicator/StepIndicatorTriggerButton.tsx @@ -39,7 +39,10 @@ function StepIndicatorTriggerButton( const item = context.data[context.activeStep || 0] const label = context.stepsLabel - const { data, ...contextWithoutData } = context + const { + data, // eslint-disable-line + ...contextWithoutData + } = context const triggerParams = { ...contextWithoutData, diff --git a/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/__tests__/WizardContainer.test.tsx b/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/__tests__/WizardContainer.test.tsx index 783955bb4c7..1833599ab82 100644 --- a/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/__tests__/WizardContainer.test.tsx +++ b/packages/dnb-eufemia/src/extensions/forms/Wizard/Container/__tests__/WizardContainer.test.tsx @@ -653,9 +653,9 @@ describe('Wizard.Container', () => { expect(wizardList()).not.toBeInTheDocument() rerender( - + - Step 1 + ensure re-render @@ -664,6 +664,7 @@ describe('Wizard.Container', () => { ) + expect(output()).toHaveTextContent('ensure re-render') expect(stepTrigger()).toBeInTheDocument() expect(wizardList()).not.toBeInTheDocument() })