diff --git a/core/src/utils/overlays.ts b/core/src/utils/overlays.ts index af2dafc586..c86c2f7d93 100644 --- a/core/src/utils/overlays.ts +++ b/core/src/utils/overlays.ts @@ -88,10 +88,14 @@ export const createOverlay = ( * interactive elements that meet the following * criteria: * 1. Element does not have a negative tabindex - * 2. Element does not have [hidden] + * 2. Element does not have `hidden` + * 3. Element does not have `disabled` for non-Ionic components. + * 4. Element does not have `disabled` or `disabled="true"` for Ionic components. + * Note: We need this distinction because `disabled="false"` is + * valid usage for the disabled property on ion-button. */ const focusableQueryString = - '[tabindex]:not([tabindex^="-"]):not([hidden]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]), textarea:not([tabindex^="-"]):not([hidden]), button:not([tabindex^="-"]):not([hidden]), select:not([tabindex^="-"]):not([hidden]), .ion-focusable:not([tabindex^="-"]):not([hidden])'; + '[tabindex]:not([tabindex^="-"]):not([hidden]):not([disabled]), input:not([type=hidden]):not([tabindex^="-"]):not([hidden]):not([disabled]), textarea:not([tabindex^="-"]):not([hidden]):not([disabled]), button:not([tabindex^="-"]):not([hidden]):not([disabled]), select:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable:not([tabindex^="-"]):not([hidden]):not([disabled]), .ion-focusable[disabled="false"]:not([tabindex^="-"]):not([hidden])'; export const focusFirstDescendant = (ref: Element, overlay: HTMLIonOverlayElement) => { let firstInput = ref.querySelector(focusableQueryString) as HTMLElement | null; diff --git a/core/src/utils/test/overlays/overlays.e2e.ts b/core/src/utils/test/overlays/overlays.e2e.ts index 901d298eed..605677d3af 100644 --- a/core/src/utils/test/overlays/overlays.e2e.ts +++ b/core/src/utils/test/overlays/overlays.e2e.ts @@ -53,4 +53,61 @@ test.describe('overlays: focus', () => { await page.keyboard.press(tabKey); await expect(visibleButton).toBeFocused(); }); + + test('should not select a disabled focusable element', async ({ page, browserName }) => { + await page.setContent(` + Show Modal + + + Button + Active Button + + + `); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const presentButton = page.locator('ion-button#open-modal'); + const activeButton = page.locator('ion-button#active'); + const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab'; + + await presentButton.click(); + await ionModalDidPresent.next(); + + await page.keyboard.press(tabKey); + await expect(activeButton).toBeFocused(); + + await page.keyboard.press(tabKey); + await expect(activeButton).toBeFocused(); + }); + + test('should select a focusable element with disabled="false"', async ({ page, browserName }) => { + await page.setContent(` + Show Modal + + + Button + Active Button + + + `); + + const ionModalDidPresent = await page.spyOnEvent('ionModalDidPresent'); + const presentButton = page.locator('ion-button#open-modal'); + const disabledFalseButton = page.locator('ion-button#disabled-false'); + const activeButton = page.locator('ion-button#active'); + const tabKey = browserName === 'webkit' ? 'Alt+Tab' : 'Tab'; + + await presentButton.click(); + await ionModalDidPresent.next(); + + await page.keyboard.press(tabKey); + await expect(disabledFalseButton).toBeFocused(); + + await page.keyboard.press(tabKey); + await expect(activeButton).toBeFocused(); + + // Loop back to beginning of overlay + await page.keyboard.press(tabKey); + await expect(disabledFalseButton).toBeFocused(); + }); });