Skip to content

Commit 05e30b6

Browse files
tujoworkerlangz
andauthored
fix(Forms): show error in Wizard menu and prevent submission if previous steps contain errors or have an unknown state (#4638)
Co-authored-by: Anders <anderslangseth@gmail.com>
1 parent 9c1aa46 commit 05e30b6

File tree

20 files changed

+1433
-454
lines changed

20 files changed

+1433
-454
lines changed

packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/Container/Examples.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,43 @@ export const WithStatusMessage = () => {
252252
)
253253
}
254254

255+
export const WithStatusMessageInMenu = () => {
256+
return (
257+
<ComponentBox data-visual-test="wizard-with-status-message-in-menu">
258+
<Form.Handler
259+
onSubmit={(data) => {
260+
console.log('onSubmit', data)
261+
}}
262+
>
263+
<Wizard.Container
264+
onStepChange={async (index, mode) => {
265+
console.log('onStepChange', index, mode)
266+
}}
267+
mode="loose"
268+
initialActiveIndex={2}
269+
>
270+
<Wizard.Step title="Step 1">
271+
<Field.String label="Step 1" path="/step1" required />
272+
<Wizard.Buttons />
273+
</Wizard.Step>
274+
275+
<Wizard.Step title="Step 2">
276+
<Field.String label="Step 2" path="/step2" required />
277+
<Wizard.Buttons />
278+
</Wizard.Step>
279+
280+
<Wizard.Step title="Step 3">
281+
<Field.String label="Step 3" path="/step3" />
282+
<Wizard.Buttons />
283+
</Wizard.Step>
284+
</Wizard.Container>
285+
286+
<Form.SubmitButton />
287+
</Form.Handler>
288+
</ComponentBox>
289+
)
290+
}
291+
255292
export const OnSubmitRequest = () => {
256293
return (
257294
<ComponentBox>

packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/Container/demos.mdx

+6
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,12 @@ import * as Examples from './Examples'
1414

1515
<Examples.AsyncWizardContainer />
1616

17+
### With StatusMessage in Menu
18+
19+
This example uses the `loose` mode to demonstrate status messages. Press the `Send` button to see the status message. You may also navigate to the previous steps and press the `Send` button again.
20+
21+
<Examples.WithStatusMessageInMenu />
22+
1723
### With StatusMessage
1824

1925
<Examples.WithStatusMessage />

packages/dnb-design-system-portal/src/docs/uilib/extensions/forms/Wizard/Container/info.mdx

+5
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,11 @@ const MyForm = () => {
175175
}
176176
```
177177

178+
## Modes
179+
180+
- The `strict` mode is the default. The user can only navigate forward using the [Wizard.NextButton](/uilib/extensions/forms/Wizard/NextButton/), not via the menu. However, the previous step remains active, allowing the user to go back at any time – even if there are errors in the current step.
181+
- Use `loose` mode if the user should be able to navigate freely between all steps, including those that have not been visited before. When there is an error in the current step, the user can navigate to other steps via the menu, but not via the [Wizard.NextButton](/uilib/extensions/forms/Wizard/NextButton/).
182+
178183
## Accessibility
179184

180185
The `Wizard.Step` component uses an `aria-label` attribute that matches the title property value. The step content is enclosed within a section element, which further enhances accessibility.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import React, { useCallback, useContext, useReducer } from 'react'
2+
import WizardContext from '../Context'
3+
import StepIndicator from '../../../../components/StepIndicator'
4+
import { StepIndicatorItemProps } from '../../../../components/step-indicator/StepIndicatorItem'
5+
import { useTranslation } from '../../hooks'
6+
7+
export function DisplaySteps({
8+
mode,
9+
variant,
10+
noAnimation,
11+
handleChange,
12+
sidebarId,
13+
}) {
14+
const [, forceUpdate] = useReducer(() => ({}), {})
15+
const {
16+
id,
17+
activeIndexRef,
18+
stepsRef,
19+
updateTitlesRef,
20+
hasInvalidStepsState,
21+
} = useContext(WizardContext) || {}
22+
updateTitlesRef.current = () => {
23+
forceUpdate()
24+
}
25+
const translations = useTranslation()
26+
27+
const sidebar_id =
28+
variant === 'drawer' && !sidebarId ? undefined : sidebarId ?? id
29+
30+
const getTriggerStatus = useCallback(() => {
31+
if (hasInvalidStepsState(['error'])) {
32+
return {
33+
status: translations.Step.stepHasError,
34+
status_state: 'error',
35+
} satisfies Omit<StepIndicatorItemProps, 'title' | 'currentItemNum'>
36+
}
37+
if (hasInvalidStepsState(['unknown'])) {
38+
return {
39+
status: 'Unknown state',
40+
status_state: 'warn',
41+
} satisfies Omit<StepIndicatorItemProps, 'title' | 'currentItemNum'>
42+
}
43+
}, [hasInvalidStepsState, translations.Step.stepHasError])
44+
45+
return (
46+
<aside className="dnb-forms-wizard-layout__indicator">
47+
<StepIndicator.Sidebar sidebar_id={sidebar_id} />
48+
<StepIndicator
49+
bottom
50+
current_step={activeIndexRef.current}
51+
data={Object.values(stepsRef.current).map(
52+
({ title, inactive, status, statusState }) =>
53+
({
54+
title,
55+
inactive,
56+
status,
57+
status_state: statusState,
58+
}) satisfies Omit<StepIndicatorItemProps, 'currentItemNum'>
59+
)}
60+
mode={mode}
61+
no_animation={noAnimation}
62+
on_change={handleChange}
63+
sidebar_id={sidebar_id}
64+
triggerButtonProps={getTriggerStatus()}
65+
/>
66+
</aside>
67+
)
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import React, { useContext } from 'react'
2+
import { useTranslation } from '../../hooks'
3+
import { convertJsxToString } from '../../../../shared/component-helper'
4+
import WizardContext from '../Context/WizardContext'
5+
import Step, {
6+
Props as StepProps,
7+
handleDeprecatedProps as handleDeprecatedStepProps,
8+
} from '../Step/Step'
9+
10+
export function IterateOverSteps({ children }) {
11+
const {
12+
check,
13+
stepsRef,
14+
activeIndexRef,
15+
totalStepsRef,
16+
stepStatusRef,
17+
prerenderFieldProps,
18+
prerenderFieldPropsRef,
19+
} = useContext(WizardContext)
20+
21+
stepsRef.current = {}
22+
let incrementIndex = -1
23+
24+
const translations = useTranslation()
25+
26+
const childrenArray = React.Children.map(children, (child) => {
27+
if (React.isValidElement(child)) {
28+
let step = child
29+
30+
if (child?.type !== Step && typeof child.type === 'function') {
31+
step = child.type.apply(child.type, [
32+
child.props,
33+
]) as React.ReactElement
34+
35+
if (step?.type === Step) {
36+
child = step
37+
}
38+
}
39+
40+
if (child?.type === Step) {
41+
const { title, inactive, include, includeWhen, id } =
42+
handleDeprecatedStepProps(child.props)
43+
44+
if (include === false) {
45+
return null
46+
}
47+
48+
if (
49+
includeWhen &&
50+
!check({
51+
visibleWhen: includeWhen,
52+
})
53+
) {
54+
return null
55+
}
56+
57+
incrementIndex++
58+
const index = incrementIndex
59+
const state = stepStatusRef.current[index]
60+
61+
stepsRef.current[index] = {
62+
id,
63+
title:
64+
title !== undefined
65+
? convertJsxToString(title)
66+
: 'Title missing',
67+
inactive,
68+
status:
69+
state === 'error'
70+
? translations.Step.stepHasError
71+
: state === 'unknown'
72+
? 'Unknown state'
73+
: undefined,
74+
statusState: state === 'error' ? 'error' : undefined, // undefined shows 'warn' by default
75+
}
76+
const key = `${index}-${activeIndexRef.current}`
77+
const clone = (props) =>
78+
React.cloneElement(child as React.ReactElement<StepProps>, props)
79+
80+
if (
81+
prerenderFieldProps &&
82+
typeof document !== 'undefined' &&
83+
index !== activeIndexRef.current &&
84+
typeof prerenderFieldPropsRef.current['step-' + index] ===
85+
'undefined'
86+
) {
87+
prerenderFieldPropsRef.current['step-' + index] = () =>
88+
clone({
89+
key,
90+
index,
91+
prerenderFieldProps: true,
92+
})
93+
}
94+
95+
return clone({
96+
key,
97+
index,
98+
})
99+
}
100+
}
101+
102+
return child
103+
})
104+
105+
// Ensure we never have a higher index than the available children
106+
// else we get a white screen
107+
if (childrenArray?.length === 0) {
108+
activeIndexRef.current = 0
109+
} else if (childrenArray?.length < activeIndexRef.current + 1) {
110+
activeIndexRef.current = childrenArray.length - 1
111+
}
112+
113+
totalStepsRef.current = childrenArray?.length
114+
115+
return childrenArray
116+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import React, { useContext, useRef } from 'react'
2+
import ReactDOM from 'react-dom'
3+
import DataContext, {
4+
defaultContextState,
5+
} from '../../DataContext/Context'
6+
import WizardContext, {
7+
WizardContextState,
8+
} from '../Context/WizardContext'
9+
10+
export function PrerenderFieldPropsOfOtherSteps({
11+
prerenderFieldPropsRef,
12+
}: {
13+
prerenderFieldPropsRef: WizardContextState['prerenderFieldPropsRef']
14+
}) {
15+
const hasRenderedRef = useRef(true)
16+
if (!hasRenderedRef.current) {
17+
return null
18+
}
19+
hasRenderedRef.current = false
20+
21+
return (
22+
<PrerenderPortal>
23+
<PrerenderFieldPropsProvider>
24+
<iframe title="Wizard Prerender" hidden>
25+
{Object.values(prerenderFieldPropsRef.current).map((Fn, i) => (
26+
<Fn key={i} />
27+
))}
28+
</iframe>
29+
</PrerenderFieldPropsProvider>
30+
</PrerenderPortal>
31+
)
32+
}
33+
34+
function PrerenderPortal({ children }) {
35+
if (typeof document !== 'undefined') {
36+
return ReactDOM.createPortal(children, document.body)
37+
}
38+
}
39+
40+
function PrerenderFieldPropsProvider({ children }) {
41+
const { data, setFieldInternals, updateDataValue } =
42+
useContext(DataContext)
43+
44+
return (
45+
<DataContext.Provider
46+
value={{
47+
...defaultContextState,
48+
hasContext: true,
49+
prerenderFieldProps: true,
50+
51+
// Only enable data and these methods
52+
data,
53+
setFieldInternals,
54+
updateDataValue,
55+
}}
56+
>
57+
<WizardContext.Provider value={{ prerenderFieldProps: true }}>
58+
{children}
59+
</WizardContext.Provider>
60+
</DataContext.Provider>
61+
)
62+
}

0 commit comments

Comments
 (0)