Skip to content

Commit

Permalink
fix(Wizard): added prop to focus content on next/back
Browse files Browse the repository at this point in the history
  • Loading branch information
thatblindgeye committed Apr 19, 2024
1 parent 340cea4 commit 1749c86
Show file tree
Hide file tree
Showing 7 changed files with 53 additions and 40 deletions.
15 changes: 15 additions & 0 deletions packages/react-core/src/components/Wizard/Wizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ export interface WizardProps extends React.HTMLProps<HTMLDivElement> {
onSave?: (event: React.MouseEvent<HTMLButtonElement>) => void | Promise<void>;
/** Callback function to close the wizard */
onClose?: (event: React.MouseEvent<HTMLButtonElement>) => void;
/** @beta Flag indicating whether the wizard content should be focused after the onNext or onBack callbacks
* are called.
*/
shouldFocusContentOnNextOrBack?: boolean;
}

export const Wizard = ({
Expand All @@ -72,11 +76,13 @@ export const Wizard = ({
onStepChange,
onSave,
onClose,
shouldFocusContentOnNextOrBack = false,
...wrapperProps
}: WizardProps) => {
const [activeStepIndex, setActiveStepIndex] = React.useState(startIndex);
const initialSteps = buildSteps(children);
const firstStepRef = React.useRef(initialSteps[startIndex - 1]);
const wrapperRef = React.useRef(null);

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

const focusMainContentElement = () =>
setTimeout(() => {
wrapperRef?.current?.focus && wrapperRef.current.focus();
}, 0);

const goToNextStep = (event: React.MouseEvent<HTMLButtonElement>, steps: WizardStepType[] = initialSteps) => {
const newStep = steps.find((step) => step.index > activeStepIndex && isStepEnabled(steps, step));

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

setActiveStepIndex(newStep?.index);
onStepChange?.(event, newStep, steps[activeStepIndex - 1], WizardStepChangeScope.Next);
shouldFocusContentOnNextOrBack && focusMainContentElement();
};

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

setActiveStepIndex(newStep?.index);
onStepChange?.(event, newStep, steps[activeStepIndex - 1], WizardStepChangeScope.Back);
shouldFocusContentOnNextOrBack && focusMainContentElement();
};

const goToStepByIndex = (
Expand Down Expand Up @@ -157,6 +170,8 @@ export const Wizard = ({
goToStepById={goToStepById}
goToStepByName={goToStepByName}
goToStepByIndex={goToStepByIndex}
shouldFocusContentOnNextOrBack={shouldFocusContentOnNextOrBack}
mainWrapperRef={wrapperRef}
>
<div
className={css(styles.wizard, className)}
Expand Down
18 changes: 9 additions & 9 deletions packages/react-core/src/components/Wizard/WizardBody.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,14 @@ export const WizardBody = ({
}: WizardBodyProps) => {
const [hasScrollbar, setHasScrollbar] = React.useState(false);
const [previousWidth, setPreviousWidth] = React.useState<number | undefined>(undefined);
const wrapperRef = React.useRef(null);
const WrapperComponent = component;
const { activeStep } = React.useContext(WizardContext);
const { activeStep, shouldFocusContentOnNextOrBack, mainWrapperRef } = React.useContext(WizardContext);
const defaultAriaLabel = ariaLabel || `${activeStep?.name} content`;

React.useEffect(() => {
const resize = () => {
if (wrapperRef?.current) {
const { offsetWidth, offsetHeight, scrollHeight } = wrapperRef.current;
if (mainWrapperRef?.current) {
const { offsetWidth, offsetHeight, scrollHeight } = mainWrapperRef.current;

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

if (wrapperRef?.current) {
observer = getResizeObserver(wrapperRef.current, handleResizeWithDelay);
const { offsetHeight, scrollHeight } = wrapperRef.current;
if (mainWrapperRef?.current) {
observer = getResizeObserver(mainWrapperRef.current, handleResizeWithDelay);
const { offsetHeight, scrollHeight } = mainWrapperRef.current;

setHasScrollbar(offsetHeight < scrollHeight);
setPreviousWidth((wrapperRef.current as HTMLElement).offsetWidth);
setPreviousWidth((mainWrapperRef.current as HTMLElement).offsetWidth);
}

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

return (
<WrapperComponent
ref={wrapperRef}
ref={mainWrapperRef}
{...(shouldFocusContentOnNextOrBack && { tabIndex: -1 })}
{...(component === 'div' && hasScrollbar && { role: 'region' })}
{...(hasScrollbar && { 'aria-label': defaultAriaLabel, 'aria-labelledby': ariaLabelledBy, tabIndex: 0 })}
className={css(styles.wizardMain)}
Expand Down
16 changes: 14 additions & 2 deletions packages/react-core/src/components/Wizard/WizardContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,12 @@ export interface WizardContextProps {
getStep: (stepId: number | string) => WizardStepType;
/** Set step by ID */
setStep: (step: Pick<WizardStepType, 'id'> & Partial<WizardStepType>) => void;
/** @beta Flag indicating whether the wizard content should be focused after the onNext or onBack callbacks
* are called.
*/
shouldFocusContentOnNextOrBack: boolean;
/** @beta Ref for main wizard content element. */
mainWrapperRef: React.RefObject<HTMLElement>;
}

export const WizardContext = React.createContext({} as WizardContextProps);
Expand All @@ -47,6 +53,8 @@ export interface WizardContextProviderProps {
steps: WizardStepType[],
index: number
): void;
shouldFocusContentOnNextOrBack: boolean;
mainWrapperRef: React.RefObject<HTMLElement>;
}

export const WizardContextProvider: React.FunctionComponent<WizardContextProviderProps> = ({
Expand All @@ -59,7 +67,9 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
onClose,
goToStepById,
goToStepByName,
goToStepByIndex
goToStepByIndex,
shouldFocusContentOnNextOrBack,
mainWrapperRef
}) => {
const [currentSteps, setCurrentSteps] = React.useState<WizardStepType[]>(initialSteps);
const [currentFooter, setCurrentFooter] = React.useState<WizardFooterType>();
Expand Down Expand Up @@ -139,7 +149,9 @@ export const WizardContextProvider: React.FunctionComponent<WizardContextProvide
goToStepByIndex: React.useCallback(
(index: number) => goToStepByIndex(null, steps, index),
[goToStepByIndex, steps]
)
),
shouldFocusContentOnNextOrBack,
mainWrapperRef
}}
>
{children}
Expand Down
28 changes: 7 additions & 21 deletions packages/react-core/src/components/Wizard/WizardNavItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export const WizardNavItem = ({
content = '',
isCurrent = false,
isDisabled = false,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isVisited = false,
stepIndex,
onClick,
Expand All @@ -68,23 +69,6 @@ export const WizardNavItem = ({
console.error('WizardNavItem: When using an anchor, please provide an href');
}

const ariaLabel = React.useMemo(() => {
if (status === WizardNavItemStatus.Error || (isVisited && !isCurrent)) {
let label = content.toString();

if (status === WizardNavItemStatus.Error) {
label += `, ${status}`;
}

// No need to signify step is visited if current
if (isVisited && !isCurrent) {
label += ', visited';
}

return label;
}
}, [content, isCurrent, isVisited, status]);

return (
<li
className={css(
Expand All @@ -110,7 +94,6 @@ export const WizardNavItem = ({
aria-disabled={isDisabled ? true : null}
aria-current={isCurrent && !children ? 'step' : false}
{...(isExpandable && { 'aria-expanded': isExpanded })}
{...(ariaLabel && { 'aria-label': ariaLabel })}
{...ouiaProps}
>
{isExpandable ? (
Expand All @@ -127,9 +110,12 @@ export const WizardNavItem = ({
{content}
{/* TODO, patternfly/patternfly#5142 */}
{status === WizardNavItemStatus.Error && (
<span style={{ marginLeft: globalSpacerSm.var }}>
<ExclamationCircleIcon color={globalDangerColor100.var} />
</span>
<>
<span className="pf-v5-screen-reader">, {status}</span>
<span style={{ marginLeft: globalSpacerSm.var }}>
<ExclamationCircleIcon color={globalDangerColor100.var} />
</span>
</>
)}
</>
)}
Expand Down
2 changes: 1 addition & 1 deletion packages/react-core/src/components/Wizard/WizardToggle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ export const WizardToggle = ({
</span>
</button>
<div className={css(styles.wizardOuterWrap)}>
<div className={css(styles.wizardInnerWrap)}>
<div aria-live="polite" className={css(styles.wizardInnerWrap)}>
{nav}
{bodyContent}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -411,7 +411,7 @@ test('incrementally shows/hides steps based on the activeStep when isProgressive
await user.click(nextButton);
expect(
screen.getByRole('button', {
name: 'Test step 1, visited'
name: 'Test step 1'
})
).toBeVisible();
expect(
Expand All @@ -429,12 +429,12 @@ test('incrementally shows/hides steps based on the activeStep when isProgressive
await user.click(nextButton);
expect(
screen.getByRole('button', {
name: 'Test step 1, visited'
name: 'Test step 1'
})
).toBeVisible();
expect(
screen.getByRole('button', {
name: 'Test step 2, visited'
name: 'Test step 2'
})
).toBeVisible();
expect(
Expand All @@ -447,7 +447,7 @@ test('incrementally shows/hides steps based on the activeStep when isProgressive
await user.click(backButton);
expect(
screen.getByRole('button', {
name: 'Test step 1, visited'
name: 'Test step 1'
})
).toBeVisible();
expect(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ const CustomStepTwoFooter = () => {

return (
<WizardFooterWrapper>
<Button variant="primary" onClick={onNext} isLoading={isLoading} isDisabled={isLoading}>
Async Next
</Button>
<Button variant="secondary" onClick={goToPrevStep} isDisabled={isLoading}>
Back
</Button>
<Button variant="primary" onClick={onNext} isLoading={isLoading} isDisabled={isLoading}>
Async Next
</Button>
<Button variant="link" onClick={close} isDisabled={isLoading}>
Cancel
</Button>
Expand Down

0 comments on commit 1749c86

Please sign in to comment.