Skip to content

Commit

Permalink
fix(CreateTearsheet): add focus trap behavior (#5329)
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewgallo committed Jun 4, 2024
1 parent 51b3c23 commit 3205383
Show file tree
Hide file tree
Showing 3 changed files with 79 additions and 40 deletions.
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"exportmodal",
"expressivecard",
"fieldsets",
"focusable",
"fullpageerror",
"gridcell",
"guidebanner",
Expand Down
4 changes: 1 addition & 3 deletions e2e/components/WebTerminal/WebTerminal-test.avt.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,7 @@ test.describe('WebTerminal @avt', () => {
});

await page.getByLabel('Web terminal').click();
const modalElement = page.locator(
`.${pkg.prefix}--web-terminal`
);
const modalElement = page.locator(`.${pkg.prefix}--web-terminal`);
await modalElement.evaluate((element) =>
Promise.all(
element.getAnimations().map((animation) => animation.finished)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import React, {
useState,
isValidElement,
PropsWithChildren,
useRef,
MutableRefObject,
RefObject,
} from 'react';
import PropTypes from 'prop-types';
import cx from 'classnames';
Expand Down Expand Up @@ -162,6 +165,9 @@ export let CreateTearsheetStep = forwardRef(
}: CreateTearsheetStepProps,
ref
) => {
const localRef = useRef<HTMLDivElement>(null);
const stepRef = ref || localRef;
const stepRefValue = (stepRef as MutableRefObject<HTMLDivElement>).current;
const stepsContext = useContext(StepsContext);
const stepNumber = useContext(StepNumberContext);
const [shouldIncludeStep, setShouldIncludeStep] =
Expand Down Expand Up @@ -196,15 +202,48 @@ export let CreateTearsheetStep = forwardRef(
setShouldIncludeStep(includeStep);
}, [includeStep, stepsContext, title]);

const setFocusChildrenTabIndex = (
childInputs: NodeListOf<Element>,
value: number
) => {
if (childInputs?.length) {
childInputs.forEach((child) => {
(child as HTMLElement).tabIndex = value;
});
}
};

// Whenever we are the current step, supply our disableSubmit and onNext values to the
// steps container context so that it can manage the 'Next' button appropriately.
useEffect(() => {
const focusElementQuery = `button, input, select, textarea, a`;
if (stepNumber !== stepsContext?.currentStep) {
// Specify tab-index -1 for focusable elements not contained
// in the current step so that the useFocus hook can exclude
// from the focus trap
const childInputs = stepRefValue?.querySelectorAll(focusElementQuery);
setFocusChildrenTabIndex(childInputs, -1);
}
if (stepNumber === stepsContext?.currentStep) {
// Specify tab-index 0 for current step focusable elements
// for the useFocus hook to know which elements to include
// in focus trap
const childInputs = stepRefValue?.querySelectorAll(focusElementQuery);
setFocusChildrenTabIndex(childInputs, 0);

stepsContext.setIsDisabled(!!disableSubmit);
stepsContext?.setOnNext(onNext); // needs to be updated here otherwise there could be stale state values from only initially setting onNext
stepsContext?.setOnPrevious(onPrevious);
}
}, [stepsContext, stepNumber, disableSubmit, onNext, onPrevious]);
}, [
stepsContext,
stepNumber,
disableSubmit,
onNext,
onPrevious,
stepRef,
stepRefValue,
]);

const renderDescription = () => {
if (description) {
Expand All @@ -221,42 +260,43 @@ export let CreateTearsheetStep = forwardRef(
};

return stepsContext ? (
<Grid
{
// Pass through any other property values as HTML attributes.
...rest
}
className={cx(blockClass, className, {
[`${blockClass}__step--hidden-step`]:
stepNumber !== stepsContext?.currentStep,
[`${blockClass}__step--visible-step`]:
stepNumber === stepsContext?.currentStep,
})}
ref={ref}
>
<Column xlg={12} lg={12} md={8} sm={4}>
<h4 className={`${blockClass}--title`}>{title}</h4>

{subtitle && (
<h6 className={`${blockClass}--subtitle`}>{subtitle}</h6>
)}

{renderDescription()}
</Column>

<Column span={100}>
{hasFieldset ? (
<FormGroup
legendText={fieldsetLegendText}
className={`${blockClass}--fieldset`}
>
{children}
</FormGroup>
) : (
children
)}
</Column>
</Grid>
<div ref={stepRef as RefObject<HTMLDivElement>}>
<Grid
{
// Pass through any other property values as HTML attributes.
...rest
}
className={cx(blockClass, className, {
[`${blockClass}__step--hidden-step`]:
stepNumber !== stepsContext?.currentStep,
[`${blockClass}__step--visible-step`]:
stepNumber === stepsContext?.currentStep,
})}
>
<Column xlg={12} lg={12} md={8} sm={4}>
<h4 className={`${blockClass}--title`}>{title}</h4>

{subtitle && (
<h6 className={`${blockClass}--subtitle`}>{subtitle}</h6>
)}

{renderDescription()}
</Column>

<Column span={100}>
{hasFieldset ? (
<FormGroup
legendText={fieldsetLegendText}
className={`${blockClass}--fieldset`}
>
{children}
</FormGroup>
) : (
children
)}
</Column>
</Grid>
</div>
) : (
pconsole.warn(
`You have tried using a ${componentName} component outside of a CreateTearsheet. This is not allowed. ${componentName}s should always be children of the CreateTearsheet`
Expand Down

0 comments on commit 3205383

Please sign in to comment.