@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
501 lines (500 loc) • 16.9 kB
JavaScript
"use client";
import { Children, useCallback, useContext, useEffect, isValidElement, useMemo, useReducer, useRef, useSyncExternalStore } from 'react';
import { clsx } from 'clsx';
import { Space } from "../../../../components/index.js";
import { warn } from "../../../../shared/component-helper.js";
import { isAsync } from "../../../../shared/helpers/isAsync.js";
import useId from "../../../../shared/helpers/useId.js";
import WizardContext from "../Context/WizardContext.js";
import DataContext from "../../DataContext/Context.js";
import useEventListener from "../../DataContext/Provider/useEventListener.js";
import Handler from "../../Form/Handler/Handler.js";
import { createReferenceKey, useSharedState } from "../../../../shared/helpers/useSharedState.js";
import useHandleLayoutEffect from "./useHandleLayoutEffect.js";
import useStepAnimation from "./useStepAnimation.js";
import useVisibility from "../../Form/Visibility/useVisibility.js";
import useDataValue from "../../hooks/useDataValue.js";
import usePath from "../../hooks/usePath.js";
import { DisplaySteps } from "./DisplaySteps.js";
import { IterateOverSteps } from "./IterateOverSteps.js";
import { PrerenderFieldPropsOfOtherSteps } from "./PrerenderFieldPropsOfOtherSteps.js";
import Step from "../Step/Step.js";
import { useIsomorphicLayoutEffect as useLayoutEffect } from "../../../../shared/helpers/useIsomorphicLayoutEffect.js";
import withComponentMarkers from "../../../../shared/helpers/withComponentMarkers.js";
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
function collectIncludeWhenDependencies(children) {
const dataPaths = new Set();
const dataItemPaths = new Set();
const fieldStatePaths = new Set();
const fieldStateItemPaths = new Set();
let hasWrappedStepChildren = false;
const addIncludeWhen = includeWhen => {
if (!includeWhen) {
return;
}
const usesItemPath = 'itemPath' in includeWhen;
const path = usesItemPath ? includeWhen.itemPath : includeWhen.path;
if ('hasValue' in includeWhen) {
if (usesItemPath) {
dataItemPaths.add(path);
} else {
dataPaths.add(path);
}
}
if ('isValid' in includeWhen) {
if (usesItemPath) {
fieldStateItemPaths.add(path);
} else {
fieldStatePaths.add(path);
}
}
};
Children.forEach(children, child => {
if (isValidElement(child)) {
if (child.type === Step) {
addIncludeWhen(child.props.includeWhen);
} else if (typeof child.type === 'function') {
hasWrappedStepChildren = true;
}
}
});
return {
dataPaths: Array.from(dataPaths),
dataItemPaths: Array.from(dataItemPaths),
fieldStatePaths: Array.from(fieldStatePaths),
fieldStateItemPaths: Array.from(fieldStateItemPaths),
hasWrappedStepChildren
};
}
function WizardContainer(props) {
const {
className,
id: idProp,
mode = 'strict',
initialActiveIndex = 0,
omitScrollManagement,
omitFocusManagement,
onStepChange,
children,
noAnimation = false,
expandedInitially = false,
prerenderFieldProps = true,
keepInDOM,
validationMode,
outset = false,
...rest
} = props;
const dataContext = useContext(DataContext);
const {
hasContext,
setFormState,
handleSubmitCall,
setShowAllErrors,
setSubmitState,
hasFieldState,
hasErrors,
setFieldEventListener
} = dataContext;
const id = useId(idProp);
const [, forceUpdate] = useReducer(() => ({}), {});
const activeIndexRef = useRef(initialActiveIndex);
const totalStepsRef = useRef(NaN);
const visitedStepsRef = useRef(new Map());
const fieldErrorRef = useRef(new Map());
const storeStepStateRef = useRef(new Map());
const onStepChangeEventsRef = useRef(new Set());
const hasErrorInOtherStepRef = useRef(false);
const elementRef = useRef(undefined);
const stepElementRef = useRef(undefined);
const preventNextStepRef = useRef(false);
const stepsRef = useRef(new Map());
const tmpStepsRef = useRef(undefined);
const stepIndexRef = useRef(-1);
const updateTitlesRef = useRef(undefined);
const prerenderFieldPropsRef = useRef({});
const bypassOnNavigation = validationMode === 'bypassOnNavigation';
const sharedStateRef = useRef(undefined);
sharedStateRef.current = useSharedState(hasContext && id ? createReferenceKey(id, 'wizard') : undefined);
const hasFieldErrorInStep = useCallback(index => {
return Array.from(fieldErrorRef.current.values()).some(({
index: i,
hasError
}) => {
return i === index && hasError;
});
}, []);
const setStepAsVisited = useCallback(index => {
visitedStepsRef.current.set(index, true);
}, []);
useEffect(() => {
if (!initialActiveIndex) {
setStepAsVisited(activeIndexRef.current);
}
}, [initialActiveIndex, setStepAsVisited]);
const syncStepsState = useCallback((index = undefined, forStates = ['unknown', 'error']) => {
const checkUnknown = forStates.includes('unknown');
const checkError = forStates.includes('error');
for (let i = 0; i < totalStepsRef.current; i++) {
if (index !== undefined && index !== i) {
continue;
}
let result = undefined;
if (checkUnknown) {
const state = i < activeIndexRef.current && visitedStepsRef.current.get(i) === undefined;
if (state) {
result = 'unknown';
}
}
if (checkError) {
const state = hasFieldErrorInStep(i);
const existingState = storeStepStateRef.current.get(i);
if (state) {
result = 'error';
} else if (existingState === 'error') {
if (i === activeIndexRef.current) {
result = undefined;
} else {
result = existingState;
}
}
}
storeStepStateRef.current.set(i, result);
}
}, [hasFieldErrorInStep]);
const hasInvalidStepsState = useCallback((index = undefined, forStates = ['unknown', 'error']) => {
syncStepsState();
const checkUnknown = forStates.includes('unknown');
const checkError = forStates.includes('error');
for (let i = 0; i < totalStepsRef.current; i++) {
if (index !== undefined && index !== i) {
continue;
}
const state = storeStepStateRef.current.get(i);
if (checkUnknown) {
if (state === 'unknown') {
return true;
}
}
if (checkError) {
if (state === 'error') {
return true;
}
}
}
return false;
}, [syncStepsState]);
const setFieldError = useCallback((index, path, hasError) => {
fieldErrorRef.current.set(path, {
index,
hasError
});
}, []);
const preventNavigation = useCallback((shouldPrevent = true) => {
preventNextStepRef.current = shouldPrevent;
}, []);
const getStepChangeOptions = useCallback(index => {
const previousIndex = activeIndexRef.current;
const totalSteps = Number.isNaN(totalStepsRef.current) ? stepsRef.current.size : totalStepsRef.current;
const options = {
preventNavigation,
totalSteps,
previousStep: {
index: previousIndex
}
};
const id = stepsRef.current.get(index)?.id;
if (id) {
Object.assign(options, {
id
});
}
const previousId = stepsRef.current.get(previousIndex)?.id;
if (previousId) {
Object.assign(options.previousStep, {
id: previousId
});
}
return options;
}, [preventNavigation]);
const callOnStepChange = useCallback(async (index, mode) => {
if (isAsync(onStepChange)) {
return await onStepChange(index, mode, getStepChangeOptions(index));
}
return onStepChange?.(index, mode, getStepChangeOptions(index));
}, [getStepChangeOptions, onStepChange]);
const {
setFocus,
scrollToTop,
isInteractionRef
} = useHandleLayoutEffect({
elementRef,
stepElementRef
});
const executeLayoutAnimationRef = useRef(undefined);
useStepAnimation({
activeIndexRef,
stepElementRef,
executeLayoutAnimationRef
});
const handleLayoutEffect = useCallback(() => {
if (!omitFocusManagement) {
setFocus();
}
if (!omitScrollManagement) {
scrollToTop();
}
}, [omitScrollManagement, omitFocusManagement, setFocus, scrollToTop]);
const handleStepChange = useCallback(async ({
index,
skipErrorCheck,
skipStepChangeCall,
skipStepChangeCallBeforeMounted,
skipStepChangeCallFromHook,
mode
}) => {
let didSubmit = false;
const onSubmit = async () => {
if (!skipStepChangeCallFromHook) {
onStepChangeEventsRef?.current?.forEach(onStepChange => {
if (typeof onStepChange === 'function') {
onStepChange(index, mode, getStepChangeOptions(index));
}
});
}
let result = undefined;
if (!skipStepChangeCall && !(skipStepChangeCallBeforeMounted && !isInteractionRef.current)) {
result = await callOnStepChange(index, mode);
}
setFormState('abort');
setShowAllErrors(bypassOnNavigation ? false : hasInvalidStepsState(index, ['error']));
if (!preventNextStepRef.current && !(result instanceof Error)) {
handleLayoutEffect();
activeIndexRef.current = index;
setStepAsVisited(activeIndexRef.current);
forceUpdate();
}
preventNextStepRef.current = false;
didSubmit = true;
return result;
};
await handleSubmitCall({
skipErrorCheck,
skipFieldValidation: skipErrorCheck,
enableAsyncBehavior: isAsync(onStepChange),
onSubmit: bypassOnNavigation ? () => null : onSubmit
});
if (!didSubmit) {
if (bypassOnNavigation) {
await onSubmit();
} else {
if (mode === 'next') {
if (!hasInvalidStepsState(activeIndexRef.current) && !hasErrors?.() && !hasFieldState?.('pending')) {
await onSubmit();
}
}
}
}
}, [bypassOnNavigation, callOnStepChange, getStepChangeOptions, handleLayoutEffect, handleSubmitCall, hasErrors, hasFieldState, hasInvalidStepsState, isInteractionRef, onStepChange, setFormState, setShowAllErrors, setStepAsVisited]);
const setActiveIndex = useCallback((index, options) => {
if (index === activeIndexRef.current) {
return;
}
const mode = index > activeIndexRef.current ? 'next' : 'previous';
handleStepChange({
index,
skipErrorCheck: mode === 'previous',
mode,
...options
});
}, [handleStepChange]);
const handlePrevious = useCallback(() => {
setActiveIndex(activeIndexRef.current - 1);
}, [setActiveIndex]);
const handleNext = useCallback(() => {
setActiveIndex(activeIndexRef.current + 1);
}, [setActiveIndex]);
const handleChange = useCallback(({
currentStep
}) => {
setActiveIndex(currentStep, mode === 'loose' ? {
skipErrorCheck: true
} : undefined);
}, [mode, setActiveIndex]);
const setFormError = useCallback(error => {
setSubmitState?.({
error
});
}, [setSubmitState]);
const handleSubmit = useCallback(({
preventSubmit
}) => {
if (hasInvalidStepsState(undefined, ['error'])) {
return preventSubmit();
}
if (activeIndexRef.current + 1 < totalStepsRef.current) {
handleNext();
preventSubmit();
}
}, [hasInvalidStepsState, handleNext]);
useEventListener('onSubmit', handleSubmit);
const {
check
} = useVisibility({});
const includeWhenDependencies = useMemo(() => collectIncludeWhenDependencies(children), [children]);
const includeWhenDataPaths = useMemo(() => {
if (includeWhenDependencies.hasWrappedStepChildren) {
return [...includeWhenDependencies.dataPaths, '//'];
}
return includeWhenDependencies.dataPaths;
}, [includeWhenDependencies]);
useDataValue(includeWhenDataPaths, undefined, {
pathType: 'absolute'
});
useDataValue(includeWhenDependencies.dataItemPaths, undefined, {
pathType: 'iterate'
});
const {
makePath,
makeIteratePath
} = usePath();
const includeWhenFieldStateDependencyPaths = useMemo(() => {
const paths = new Set();
includeWhenDependencies.fieldStatePaths.forEach(path => {
paths.add(makePath(path));
});
includeWhenDependencies.fieldStateItemPaths.forEach(path => {
paths.add(makeIteratePath(path));
});
if (includeWhenDependencies.hasWrappedStepChildren) {
paths.add(undefined);
}
return Array.from(paths);
}, [includeWhenDependencies, makeIteratePath, makePath]);
const includeWhenSnapshotVersionRef = useRef(0);
const subscribeIncludeWhenFieldState = useCallback(callback => {
if (includeWhenFieldStateDependencyPaths.length === 0 || !setFieldEventListener) {
return () => undefined;
}
let isSubscribed = true;
const handleFieldStateUpdate = params => {
if (params && 'state' in params && params.state.isFocused === true) {
return undefined;
}
includeWhenSnapshotVersionRef.current += 1;
queueMicrotask(() => {
if (isSubscribed) {
callback();
}
});
};
includeWhenFieldStateDependencyPaths.forEach(path => {
setFieldEventListener(path, 'onSetMountedFieldState', handleFieldStateUpdate);
setFieldEventListener(path, 'onSetFieldError', handleFieldStateUpdate);
});
return () => {
isSubscribed = false;
includeWhenFieldStateDependencyPaths.forEach(path => {
setFieldEventListener(path, 'onSetMountedFieldState', handleFieldStateUpdate, {
remove: true
});
setFieldEventListener(path, 'onSetFieldError', handleFieldStateUpdate, {
remove: true
});
});
};
}, [includeWhenFieldStateDependencyPaths, setFieldEventListener]);
const getIncludeWhenSnapshot = useCallback(() => includeWhenSnapshotVersionRef.current, []);
useSyncExternalStore(subscribeIncludeWhenFieldState, getIncludeWhenSnapshot, getIncludeWhenSnapshot);
const mapOverChildrenRef = useRef(false);
const enableMapOverChildren = useCallback(() => {
mapOverChildrenRef.current = true;
}, []);
const activeIndex = activeIndexRef.current;
const providerValue = useMemo(() => {
return {
id,
activeIndex,
initialActiveIndex,
stepElementRef,
stepsRef,
updateTitlesRef,
activeIndexRef,
stepIndexRef,
totalStepsRef,
prerenderFieldProps,
prerenderFieldPropsRef,
hasErrorInOtherStepRef,
onStepChangeEventsRef,
keepInDOM,
enableMapOverChildren,
mapOverChildrenRef,
check,
setActiveIndex,
handlePrevious,
hasInvalidStepsState,
setFieldError,
handleNext,
setFormError
};
}, [id, activeIndex, initialActiveIndex, prerenderFieldProps, keepInDOM, enableMapOverChildren, check, setActiveIndex, handlePrevious, hasInvalidStepsState, setFieldError, handleNext, setFormError]);
useLayoutEffect(() => {
if (id && hasContext) {
sharedStateRef.current.extend(providerValue);
}
}, [hasContext, id, providerValue]);
useLayoutEffect(() => {
updateTitlesRef.current?.();
}, [stepsRef.current]);
const stepsLengthDidChange = useCallback(() => {
const tmpCount = tmpStepsRef.current;
if (tmpCount === undefined) {
return false;
}
const count = totalStepsRef.current;
return count !== 0 && tmpCount !== 0 && count !== tmpCount;
}, []);
useLayoutEffect(() => {
if (stepsLengthDidChange()) {
callOnStepChange(activeIndexRef.current, 'stepListModified');
executeLayoutAnimationRef.current?.();
}
tmpStepsRef.current = totalStepsRef.current;
}, [totalStepsRef.current, callOnStepChange, stepsLengthDidChange]);
if (!hasContext) {
warn('You may wrap Wizard.Container in Form.Handler');
return _jsx(Handler, {
children: _jsx(WizardContainer, {
...props,
id: id
})
});
}
return _jsxs(WizardContext, {
value: providerValue,
children: [_jsxs(Space, {
className: clsx('dnb-forms-wizard-layout', className),
ref: elementRef,
...rest,
children: [_jsx(DisplaySteps, {
mode: mode,
noAnimation: noAnimation,
expandedInitially: expandedInitially,
handleChange: handleChange,
outset: outset
}), _jsx("div", {
className: "dnb-forms-wizard-layout__contents",
children: _jsx(IterateOverSteps, {
children: children
})
})]
}), prerenderFieldProps && !keepInDOM && _jsx(PrerenderFieldPropsOfOtherSteps, {
prerenderFieldPropsRef: prerenderFieldPropsRef,
stepsRef: stepsRef
})]
});
}
withComponentMarkers(WizardContainer, {
_supportsSpacingProps: true
});
export default WizardContainer;
//# sourceMappingURL=WizardContainer.js.map