From 1d8640f5d0481f820424dd565c64b409d4ad2010 Mon Sep 17 00:00:00 2001 From: Afsal K Date: Fri, 16 Aug 2024 09:59:57 +0530 Subject: [PATCH] fix(tearsheet): resolve disabled first element focus issue (#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 --- .../Tearsheet/Tearsheet-test.avt.e2e.js | 40 ++++++++++ .../Tearsheet/Tearsheet.stories.jsx | 78 +++++++++++++++++++ .../components/Tearsheet/TearsheetShell.tsx | 19 ++++- .../src/global/js/hooks/useFocus.js | 7 +- 4 files changed, 138 insertions(+), 6 deletions(-) diff --git a/e2e/components/Tearsheet/Tearsheet-test.avt.e2e.js b/e2e/components/Tearsheet/Tearsheet-test.avt.e2e.js index 60982d2452..59c876247a 100644 --- a/e2e/components/Tearsheet/Tearsheet-test.avt.e2e.js +++ b/e2e/components/Tearsheet/Tearsheet-test.avt.e2e.js @@ -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', diff --git a/packages/ibm-products/src/components/Tearsheet/Tearsheet.stories.jsx b/packages/ibm-products/src/components/Tearsheet/Tearsheet.stories.jsx index 4f8469e8b3..f2f1002d87 100644 --- a/packages/ibm-products/src/components/Tearsheet/Tearsheet.stories.jsx +++ b/packages/ibm-products/src/components/Tearsheet/Tearsheet.stories.jsx @@ -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 ( + <> + + +
+ setOpen(false)} + slug={slug && sampleSlug} + > +
+
+

Main content

+ + + + +
+
+
+
+ + ); +}; + // eslint-disable-next-line react/prop-types const StackedTemplate = ({ mixedSizes, actions, slug, ...args }) => { const [open1, setOpen1] = useState(false); @@ -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 = { diff --git a/packages/ibm-products/src/components/Tearsheet/TearsheetShell.tsx b/packages/ibm-products/src/components/Tearsheet/TearsheetShell.tsx index 0bc40cd62e..0483c66ec6 100644 --- a/packages/ibm-products/src/components/Tearsheet/TearsheetShell.tsx +++ b/packages/ibm-products/src/components/Tearsheet/TearsheetShell.tsx @@ -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(() => { diff --git a/packages/ibm-products/src/global/js/hooks/useFocus.js b/packages/ibm-products/src/global/js/hooks/useFocus.js index 1b1513b7f2..7d190923e4 100644 --- a/packages/ibm-products/src/global/js/hooks/useFocus.js +++ b/packages/ibm-products/src/global/js/hooks/useFocus.js @@ -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) => { @@ -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}`; @@ -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;