Skip to content

Commit

Permalink
fix(tearsheet): resolve disabled first element focus issue (carbon-de…
Browse files Browse the repository at this point in the history
…sign-system#5840)

* fix(tearsheet): resolve disabled first element focus issue

* fix(tearsheet): remove unwanted disabed attribute from input

* test(tearsheet): implement tearsheet avt test

* chore(useFocus): remove unwanted sting interpolation
  • Loading branch information
makafsal authored Aug 16, 2024
1 parent 8cb241b commit 1d8640f
Show file tree
Hide file tree
Showing 4 changed files with 138 additions and 6 deletions.
40 changes: 40 additions & 0 deletions e2e/components/Tearsheet/Tearsheet-test.avt.e2e.js
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,46 @@ test.describe('Tearsheet @avt', () => {
await expect(modalElement).not.toBeInViewport();
});

test('@avt-first-element-disabled-focus-behaviour', async ({ page }) => {
await visitStory(page, {
component: 'Tearsheet',
id: 'ibm-products-components-tearsheet--first-element-disabled',
globals: {
carbonTheme: 'white',
},
});

const modalElement = page.locator(`.${carbon.prefix}--modal.is-visible`);
const openButton = page.getByText('Open Tearsheet');
const input2 = page.locator('#tss-ft2');
const closeIcon = page.getByLabel('Close the tearsheet');

// Pressing 'Tab' key to focus on the "Open Tearsheet" button in the Storybook
await page.keyboard.press('Tab');
await expect(openButton).toBeFocused();

// Pressing 'Enter' key to open the Tearsheet
await page.keyboard.press('Enter');

// Opening Tearsheet
await modalElement.evaluate((element) =>
Promise.all(
element.getAnimations().map((animation) => animation.finished)
)
);

await expect(page).toHaveNoACViolations(
'Tearsheet @avt-focus-return-to-launcher-button'
);

// Initially expect close button to be focused
await expect(closeIcon).toBeFocused();

// Press 'Tab' key to focus the second input box
await page.keyboard.press('Tab');
await expect(input2).toBeFocused();
});

test('@avt-focus-return-to-launcher-button', async ({ page }) => {
await visitStory(page, {
component: 'Tearsheet',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,72 @@ const ReturnFocusTemplate = ({ actions, slug, ...args }) => {
);
};

const FirstElementDisabledTemplate = ({ actions, slug, ...args }) => {
const [open, setOpen] = useState(false);

const wiredActions =
actions &&
Array.prototype.map.call(actions, (action) => {
if (action.label === 'Cancel') {
const previousClick = action.onClick;
return {
...action,
onClick: (evt) => {
setOpen(false);
previousClick(evt);
},
};
}
return action;
});

const ref = useRef();

return (
<>
<style>{`.${pkg.prefix}--tearsheet { opacity: 0 }`};</style>
<Button onClick={() => setOpen(true)}>Open Tearsheet</Button>
<div ref={ref}>
<Tearsheet
{...args}
actions={wiredActions}
open={open}
onClose={() => setOpen(false)}
slug={slug && sampleSlug}
>
<div className="tearsheet-stories__dummy-content-block">
<Form>
<p>Main content</p>
<FormGroup
legendId="tearsheet-form-group"
legendText="FormGroup Legend"
>
<TextInput
id="tss-ft1"
labelText="Enter an important value here"
style={
// stylelint-disable-next-line carbon/layout-token-use
{ marginBottom: '1em' }
}
disabled
/>
<TextInput
id="tss-ft2"
labelText="Here is an entry field:"
style={
// stylelint-disable-next-line carbon/layout-token-use
{ marginBottom: '1em' }
}
/>
</FormGroup>
</Form>
</div>
</Tearsheet>
</div>
</>
);
};

// eslint-disable-next-line react/prop-types
const StackedTemplate = ({ mixedSizes, actions, slug, ...args }) => {
const [open1, setOpen1] = useState(false);
Expand Down Expand Up @@ -482,6 +548,18 @@ ReturnFocusToOpenButton.args = {
actions: 7,
};

export const firstElementDisabled = FirstElementDisabledTemplate.bind({});
firstElementDisabled.storyName = 'First Element Disabled';
firstElementDisabled.args = {
closeIconDescription,
hasCloseIcon: true,
description,
onClose: action('onClose called'),
title,
actions: 7,
selectorPrimaryFocus: '#tss-ft1',
};

export const fullyLoaded = Template.bind({});
fullyLoaded.storyName = 'Tearsheet with all header items and influencer';
fullyLoaded.args = {
Expand Down
19 changes: 15 additions & 4 deletions packages/ibm-products/src/components/Tearsheet/TearsheetShell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,13 +306,24 @@ export const TearsheetShell = React.forwardRef(

// Callback to give the tearsheet the opportunity to claim focus
handleStackChange.claimFocus = function () {
if (selectorPrimaryFocus) {
return getSpecificElement(
if (
selectorPrimaryFocus &&
getSpecificElement(modalRef?.current, selectorPrimaryFocus)
) {
const specifiedEl = getSpecificElement(
modalRef?.current,
selectorPrimaryFocus
)?.focus();
);

if (
specifiedEl &&
window?.getComputedStyle(specifiedEl)?.display !== 'none'
) {
return specifiedEl.focus();
}
}
firstElement?.focus();

setTimeout(() => firstElement?.focus(), 0);
};

useEffect(() => {
Expand Down
7 changes: 5 additions & 2 deletions packages/ibm-products/src/global/js/hooks/useFocus.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import { pkg } from '../../../settings';
import { useCallback, useEffect } from 'react';

export const getSpecificElement = (parentEl, elementId) => {
return elementId ? parentEl?.querySelector(elementId) : null;
const element = parentEl?.querySelector(elementId);

return elementId && !element?.disabled ? element : null;
};

export const useFocus = (modalRef, selectorPrimaryFocus) => {
Expand All @@ -19,7 +21,7 @@ export const useFocus = (modalRef, selectorPrimaryFocus) => {
// Querying focusable element in the modal
// Query to exclude hidden elements in the modal from querySelectorAll() method
// feel free to include more if needed :)
const notQuery = `:not(.${carbonPrefix}--visually-hidden,.${tearsheetBaseClass}__header--no-close-icon,.${carbonPrefix}--btn--disabled,[aria-hidden="true"],[tabindex="-1"])`;
const notQuery = `:not(.${carbonPrefix}--visually-hidden,.${tearsheetBaseClass}__header--no-close-icon,.${carbonPrefix}--btn--disabled,[aria-hidden="true"],[tabindex="-1"],[disabled])`;
// Queries to include element types button, input, select, textarea
const queryButton = `button${notQuery}`;
const queryInput = `input${notQuery}`;
Expand All @@ -41,6 +43,7 @@ export const useFocus = (modalRef, selectorPrimaryFocus) => {
(el) => window?.getComputedStyle(el)?.display !== 'none'
);
}

const first = focusableElements?.[0];
const last = focusableElements?.[focusableElements?.length - 1];
const all = focusableElements;
Expand Down

0 comments on commit 1d8640f

Please sign in to comment.