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}
+ >
+
+
+
+ >
+ );
+};
+
// 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;