Skip to content

Commit fe1d86c

Browse files
fix(Wizard): added prop to focus content on next/back (#10285)
* fix(Wizard): added prop to focus content on next/back * Added new example * Removed beta flag on context props * Removed aria-live attr * Updated prop name * Updated leftover instnaces of ...onNextOrBack prop name
1 parent 883f1f6 commit fe1d86c

File tree

8 files changed

+97
-40
lines changed

8 files changed

+97
-40
lines changed

packages/react-core/src/components/Wizard/Wizard.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
5555
onSave?: (event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>;
5656
/** Callback function to close the wizard */
5757
onClose?: (event: React.MouseEvent<HTMLButtonElement>) => void;
58+
/** @beta Flag indicating whether the wizard content should be focused after the onNext or onBack callbacks
59+
* are called.
60+
*/
61+
shouldFocusContent?: boolean;
5862
}
5963

6064
export const Wizard = ({
@@ -72,11 +76,13 @@ export const Wizard = ({
7276
onStepChange,
7377
onSave,
7478
onClose,
79+
shouldFocusContent = false,
7580
...wrapperProps
7681
}: WizardProps) => {
7782
const [activeStepIndex, setActiveStepIndex] = React.useState(startIndex);
7883
const initialSteps = buildSteps(children);
7984
const firstStepRef = React.useRef(initialSteps[startIndex - 1]);
85+
const wrapperRef = React.useRef(null);
8086

8187
// When the startIndex maps to a parent step, focus on the first sub-step
8288
React.useEffect(() => {
@@ -85,6 +91,11 @@ export const Wizard = ({
8591
}
8692
}, [startIndex]);
8793

94+
const focusMainContentElement = () =>
95+
setTimeout(() => {
96+
wrapperRef?.current?.focus && wrapperRef.current.focus();
97+
}, 0);
98+
8899
const goToNextStep = (event: React.MouseEvent<HTMLButtonElement>, steps: WizardStepType[] = initialSteps) => {
89100
const newStep = steps.find((step) => step.index > activeStepIndex && isStepEnabled(steps, step));
90101

@@ -94,6 +105,7 @@ export const Wizard = ({
94105

95106
setActiveStepIndex(newStep?.index);
96107
onStepChange?.(event, newStep, steps[activeStepIndex - 1], WizardStepChangeScope.Next);
108+
shouldFocusContent && focusMainContentElement();
97109
};
98110

99111
const goToPrevStep = (event: React.MouseEvent<HTMLButtonElement>, steps: WizardStepType[] = initialSteps) => {
@@ -103,6 +115,7 @@ export const Wizard = ({
103115

104116
setActiveStepIndex(newStep?.index);
105117
onStepChange?.(event, newStep, steps[activeStepIndex - 1], WizardStepChangeScope.Back);
118+
shouldFocusContent && focusMainContentElement();
106119
};
107120

108121
const goToStepByIndex = (
@@ -157,6 +170,8 @@ export const Wizard = ({
157170
goToStepById={goToStepById}
158171
goToStepByName={goToStepByName}
159172
goToStepByIndex={goToStepByIndex}
173+
shouldFocusContent={shouldFocusContent}
174+
mainWrapperRef={wrapperRef}
160175
>
161176
<div
162177
className={css(styles.wizard, className)}

packages/react-core/src/components/Wizard/WizardBody.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -36,15 +36,14 @@ export const WizardBody = ({
3636
}: WizardBodyProps) => {
3737
const [hasScrollbar, setHasScrollbar] = React.useState(false);
3838
const [previousWidth, setPreviousWidth] = React.useState<number | undefined>(undefined);
39-
const wrapperRef = React.useRef(null);
4039
const WrapperComponent = component;
41-
const { activeStep } = React.useContext(WizardContext);
40+
const { activeStep, shouldFocusContent, mainWrapperRef } = React.useContext(WizardContext);
4241
const defaultAriaLabel = ariaLabel || `${activeStep?.name} content`;
4342

4443
React.useEffect(() => {
4544
const resize = () => {
46-
if (wrapperRef?.current) {
47-
const { offsetWidth, offsetHeight, scrollHeight } = wrapperRef.current;
45+
if (mainWrapperRef?.current) {
46+
const { offsetWidth, offsetHeight, scrollHeight } = mainWrapperRef.current;
4847

4948
if (previousWidth !== offsetWidth) {
5049
setPreviousWidth(offsetWidth);
@@ -56,12 +55,12 @@ export const WizardBody = ({
5655
const handleResizeWithDelay = debounce(resize, 250);
5756
let observer = () => {};
5857

59-
if (wrapperRef?.current) {
60-
observer = getResizeObserver(wrapperRef.current, handleResizeWithDelay);
61-
const { offsetHeight, scrollHeight } = wrapperRef.current;
58+
if (mainWrapperRef?.current) {
59+
observer = getResizeObserver(mainWrapperRef.current, handleResizeWithDelay);
60+
const { offsetHeight, scrollHeight } = mainWrapperRef.current;
6261

6362
setHasScrollbar(offsetHeight < scrollHeight);
64-
setPreviousWidth((wrapperRef.current as HTMLElement).offsetWidth);
63+
setPreviousWidth((mainWrapperRef.current as HTMLElement).offsetWidth);
6564
}
6665

6766
return () => {
@@ -71,7 +70,8 @@ export const WizardBody = ({
7170

7271
return (
7372
<WrapperComponent
74-
ref={wrapperRef}
73+
ref={mainWrapperRef}
74+
{...(shouldFocusContent && { tabIndex: -1 })}
7575
{...(component === 'div' && hasScrollbar && { role: 'region' })}
7676
{...(hasScrollbar && { 'aria-label': defaultAriaLabel, 'aria-labelledby': ariaLabelledBy, tabIndex: 0 })}
7777
className={css(styles.wizardMain)}

packages/react-core/src/components/Wizard/WizardContext.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,12 @@ export interface WizardContextProps {
2828
getStep: (stepId: number | string) => WizardStepType;
2929
/** Set step by ID */
3030
setStep: (step: Pick<WizardStepType, 'id'> & Partial<WizardStepType>) => void;
31+
/** Flag indicating whether the wizard content should be focused after the onNext or onBack callbacks
32+
* are called.
33+
*/
34+
shouldFocusContent: boolean;
35+
/** Ref for main wizard content element. */
36+
mainWrapperRef: React.RefObject<HTMLElement>;
3137
}
3238

3339
export const WizardContext = React.createContext({} as WizardContextProps);
@@ -47,6 +53,8 @@ export interface WizardContextProviderProps {
4753
steps: WizardStepType[],
4854
index: number
4955
): void;
56+
shouldFocusContent: boolean;
57+
mainWrapperRef: React.RefObject<HTMLElement>;
5058
}
5159

5260
export const WizardContextProvider: React.FunctionComponent<WizardContextProviderProps> = ({
@@ -59,7 +67,9 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
5967
onClose,
6068
goToStepById,
6169
goToStepByName,
62-
goToStepByIndex
70+
goToStepByIndex,
71+
shouldFocusContent,
72+
mainWrapperRef
6373
}) => {
6474
const [currentSteps, setCurrentSteps] = React.useState<WizardStepType[]>(initialSteps);
6575
const [currentFooter, setCurrentFooter] = React.useState<WizardFooterType>();
@@ -139,7 +149,9 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
139149
goToStepByIndex: React.useCallback(
140150
(index: number) => goToStepByIndex(null, steps, index),
141151
[goToStepByIndex, steps]
142-
)
152+
),
153+
shouldFocusContent,
154+
mainWrapperRef
143155
}}
144156
>
145157
{children}

packages/react-core/src/components/Wizard/WizardNavItem.tsx

Lines changed: 7 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ export const WizardNavItem = ({
4444
content = '',
4545
isCurrent = false,
4646
isDisabled = false,
47+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
4748
isVisited = false,
4849
stepIndex,
4950
onClick,
@@ -68,23 +69,6 @@ export const WizardNavItem = ({
6869
console.error('WizardNavItem: When using an anchor, please provide an href');
6970
}
7071

71-
const ariaLabel = React.useMemo(() => {
72-
if (status === WizardNavItemStatus.Error || (isVisited && !isCurrent)) {
73-
let label = content.toString();
74-
75-
if (status === WizardNavItemStatus.Error) {
76-
label += `, ${status}`;
77-
}
78-
79-
// No need to signify step is visited if current
80-
if (isVisited && !isCurrent) {
81-
label += ', visited';
82-
}
83-
84-
return label;
85-
}
86-
}, [content, isCurrent, isVisited, status]);
87-
8872
return (
8973
<li
9074
className={css(
@@ -110,7 +94,6 @@ export const WizardNavItem = ({
11094
aria-disabled={isDisabled ? true : null}
11195
aria-current={isCurrent && !children ? 'step' : false}
11296
{...(isExpandable && { 'aria-expanded': isExpanded })}
113-
{...(ariaLabel && { 'aria-label': ariaLabel })}
11497
{...ouiaProps}
11598
>
11699
{isExpandable ? (
@@ -127,9 +110,12 @@ export const WizardNavItem = ({
127110
{content}
128111
{/* TODO, patternfly/patternfly#5142 */}
129112
{status === WizardNavItemStatus.Error && (
130-
<span style={{ marginLeft: globalSpacerSm.var }}>
131-
<ExclamationCircleIcon color={globalDangerColor100.var} />
132-
</span>
113+
<>
114+
<span className="pf-v5-screen-reader">, {status}</span>
115+
<span style={{ marginLeft: globalSpacerSm.var }}>
116+
<ExclamationCircleIcon color={globalDangerColor100.var} />
117+
</span>
118+
</>
133119
)}
134120
</>
135121
)}

packages/react-core/src/components/Wizard/__tests__/Wizard.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -411,7 +411,7 @@ test('incrementally shows/hides steps based on the activeStep when isProgressive
411411
await user.click(nextButton);
412412
expect(
413413
screen.getByRole('button', {
414-
name: 'Test step 1, visited'
414+
name: 'Test step 1'
415415
})
416416
).toBeVisible();
417417
expect(
@@ -429,12 +429,12 @@ test('incrementally shows/hides steps based on the activeStep when isProgressive
429429
await user.click(nextButton);
430430
expect(
431431
screen.getByRole('button', {
432-
name: 'Test step 1, visited'
432+
name: 'Test step 1'
433433
})
434434
).toBeVisible();
435435
expect(
436436
screen.getByRole('button', {
437-
name: 'Test step 2, visited'
437+
name: 'Test step 2'
438438
})
439439
).toBeVisible();
440440
expect(
@@ -447,7 +447,7 @@ test('incrementally shows/hides steps based on the activeStep when isProgressive
447447
await user.click(backButton);
448448
expect(
449449
screen.getByRole('button', {
450-
name: 'Test step 1, visited'
450+
name: 'Test step 1'
451451
})
452452
).toBeVisible();
453453
expect(

packages/react-core/src/components/Wizard/examples/Wizard.md

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ propComponents:
1616
'WizardContextProps',
1717
'WizardBasicStep',
1818
'WizardParentStep',
19-
'WizardSubStep',
19+
'WizardSubStep'
2020
]
2121
---
2222

@@ -57,91 +57,119 @@ import layout from '@patternfly/react-styles/css/layouts/Bullseye/bullseye';
5757
### Basic
5858

5959
```ts file="./WizardBasic.tsx"
60+
61+
```
62+
63+
### Focus content on next/back
64+
65+
To focus the main content element of the `Wizard`, pass in the `shouldFocusContent` property. It is recommended that this is passed in so that users can navigate through a `WizardStep` content in order.
66+
67+
If a `WizardStep` is passed a `body={null}` property, you must manually handle focus.
68+
69+
```ts file="./WizardFocusOnNextBack.tsx"
70+
6071
```
6172

6273
### Basic with disabled steps
6374

6475
```ts file="./WizardBasicDisabledSteps.tsx"
76+
6577
```
6678

6779
### Anchors for nav items
6880

6981
```ts file="./WizardWithNavAnchors.tsx"
82+
7083
```
7184

7285
### Incrementally enabled steps
7386

7487
```ts file="./WizardStepVisitRequired.tsx"
88+
7589
```
7690

7791
### Expandable steps
7892

7993
```ts file="./WizardExpandableSteps.tsx"
94+
8095
```
8196

8297
### Progress after submission
8398

8499
```ts file="./WizardWithSubmitProgress.tsx"
100+
85101
```
86102

87103
### Enabled on form validation
88104

89105
```ts file="./WizardEnabledOnFormValidation.tsx"
106+
90107
```
91108

92109
### Validate on button press
93110

94111
```ts file="./WizardValidateOnButtonPress.tsx"
112+
95113
```
96114

97115
### Progressive steps
98116

99117
```ts file="./WizardProgressiveSteps.tsx"
118+
100119
```
101120

102121
### Get current step
103122

104123
```ts file="./WizardGetCurrentStep.tsx"
124+
105125
```
106126

107127
### Within modal
108128

109129
```ts file="./WizardWithinModal.tsx"
130+
110131
```
111132

112133
### Step drawer content
113134

114135
```ts file="./WizardStepDrawerContent.tsx"
136+
115137
```
116138

117139
### Custom navigation
118140

119141
```ts file="./WizardWithCustomNav.tsx"
142+
120143
```
121144

122145
### Header
123146

124147
```ts file="./WizardWithHeader.tsx"
148+
125149
```
126150

127151
### Custom footer
128152

129153
```ts file="./WizardWithCustomFooter.tsx"
154+
130155
```
131156

132157
### Custom navigation item
133158

134159
```ts file="./WizardWithCustomNavItem.tsx"
160+
135161
```
136162

137163
### Toggle step visibility
138164

139165
```ts file="./WizardToggleStepVisibility.tsx"
166+
140167
```
141168

142169
### Step error status
143170

144171
```ts file="./WizardStepErrorStatus.tsx"
172+
145173
```
146174

147175
## Hooks
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import { Wizard, WizardStep } from '@patternfly/react-core';
3+
4+
export const WizardFocusOnNextBack: React.FunctionComponent = () => (
5+
<Wizard shouldFocusContent title="Wizard that focuses content on next or back click">
6+
<WizardStep name="Step 1" id="wizard-focus-first-step">
7+
Step 1 content
8+
</WizardStep>
9+
<WizardStep name="Step 2" id="wizard-focus-second-step">
10+
Step 2 content
11+
</WizardStep>
12+
<WizardStep name="Review" id="wizard-focus-review-step" footer={{ nextButtonText: 'Finish' }}>
13+
Review step content
14+
</WizardStep>
15+
</Wizard>
16+
);

0 commit comments

Comments
 (0)