@dnb/eufemia
Version:
DNB Eufemia Design System UI Library
387 lines (386 loc) • 13.7 kB
JavaScript
"use client";
import _extends from "@babel/runtime-corejs3/helpers/esm/extends";
import React, { useContext, useCallback, useRef, useReducer, useMemo, useEffect } from 'react';
import classnames from 'classnames';
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 { DisplaySteps } from "./DisplaySteps.js";
import { IterateOverSteps } from "./IterateOverSteps.js";
import { PrerenderFieldPropsOfOtherSteps } from "./PrerenderFieldPropsOfOtherSteps.js";
import { useIsomorphicLayoutEffect as useLayoutEffect } from "../../../../shared/helpers/useIsomorphicLayoutEffect.js";
function handleDeprecatedProps(props) {
const {
variant,
sidebarId,
...rest
} = props;
return rest;
}
function WizardContainer(props) {
const {
className,
id: idProp,
mode = 'strict',
initialActiveIndex = 0,
omitScrollManagement,
omitFocusManagement,
onStepChange,
children,
noAnimation = false,
expandedInitially = false,
prerenderFieldProps = true,
keepInDOM,
validationMode,
outset = true,
...rest
} = handleDeprecatedProps(props);
const dataContext = useContext(DataContext);
const {
hasContext,
setFormState,
handleSubmitCall,
setShowAllErrors,
setSubmitState,
hasFieldState
} = 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();
const stepElementRef = useRef();
const preventNextStepRef = useRef(false);
const stepsRef = useRef(new Map());
const tmpStepsRef = useRef();
const stepIndexRef = useRef(-1);
const updateTitlesRef = useRef();
const prerenderFieldPropsRef = useRef({});
const bypassOnNavigation = validationMode === 'bypassOnNavigation';
const sharedStateRef = useRef();
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 => {
var _stepsRef$current$get, _stepsRef$current$get2;
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 = stepsRef.current.get(index)) === null || _stepsRef$current$get === void 0 ? void 0 : _stepsRef$current$get.id;
if (id) {
Object.assign(options, {
id
});
}
const previousId = (_stepsRef$current$get2 = stepsRef.current.get(previousIndex)) === null || _stepsRef$current$get2 === void 0 ? void 0 : _stepsRef$current$get2.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 === null || onStepChange === void 0 ? void 0 : onStepChange(index, mode, getStepChangeOptions(index));
}, [getStepChangeOptions, onStepChange]);
const {
setFocus,
scrollToTop,
isInteractionRef
} = useHandleLayoutEffect({
elementRef,
stepElementRef
});
const executeLayoutAnimationRef = useRef();
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) {
var _onStepChangeEventsRe;
onStepChangeEventsRef === null || onStepChangeEventsRef === void 0 || (_onStepChangeEventsRe = onStepChangeEventsRef.current) === null || _onStepChangeEventsRe === void 0 || _onStepChangeEventsRe.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) && !(hasFieldState !== null && hasFieldState !== void 0 && hasFieldState('pending'))) {
await onSubmit();
}
}
}
}
}, [bypassOnNavigation, callOnStepChange, getStepChangeOptions, handleLayoutEffect, handleSubmitCall, 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(({
current_step
}) => {
setActiveIndex(current_step, mode === 'loose' ? {
skipErrorCheck: true
} : undefined);
}, [mode, setActiveIndex]);
const setFormError = useCallback(error => {
setSubmitState === null || setSubmitState === void 0 || 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 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(() => {
var _updateTitlesRef$curr;
(_updateTitlesRef$curr = updateTitlesRef.current) === null || _updateTitlesRef$curr === void 0 || _updateTitlesRef$curr.call(updateTitlesRef);
}, [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()) {
var _executeLayoutAnimati;
callOnStepChange(activeIndexRef.current, 'stepListModified');
(_executeLayoutAnimati = executeLayoutAnimationRef.current) === null || _executeLayoutAnimati === void 0 || _executeLayoutAnimati.call(executeLayoutAnimationRef);
}
tmpStepsRef.current = totalStepsRef.current;
}, [totalStepsRef.current, callOnStepChange, stepsLengthDidChange]);
if (!hasContext) {
warn('You may wrap Wizard.Container in Form.Handler');
return React.createElement(Handler, null, React.createElement(WizardContainer, _extends({}, props, {
id: id
})));
}
return React.createElement(WizardContext.Provider, {
value: providerValue
}, React.createElement(Space, _extends({
className: classnames('dnb-forms-wizard-layout', className),
innerRef: elementRef
}, rest), React.createElement(DisplaySteps, {
mode: mode,
noAnimation: noAnimation,
expandedInitially: expandedInitially,
handleChange: handleChange,
outset: outset
}), React.createElement("div", {
className: "dnb-forms-wizard-layout__contents"
}, React.createElement(IterateOverSteps, null, children))), prerenderFieldProps && !keepInDOM && React.createElement(PrerenderFieldPropsOfOtherSteps, {
prerenderFieldPropsRef: prerenderFieldPropsRef,
stepsRef: stepsRef
}));
}
WizardContainer._supportsSpacingProps = true;
export default WizardContainer;
//# sourceMappingURL=WizardContainer.js.map