From 3e71c8a81748d86c07258f5f952ef51efce1f8c4 Mon Sep 17 00:00:00 2001 From: Stephanie Hong <41085564+JelloBagel@users.noreply.github.com> Date: Fri, 20 Oct 2023 01:09:12 +0000 Subject: [PATCH 1/5] Allow iterateFocusableElements options from focus-zone --- src/focus-zone.ts | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/focus-zone.ts b/src/focus-zone.ts index c4bbcb4..157c42d 100644 --- a/src/focus-zone.ts +++ b/src/focus-zone.ts @@ -1,6 +1,6 @@ import {polyfill as eventListenerSignalPolyfill} from './polyfills/event-listener-signal.js' import {isMacOS} from './utils/user-agent.js' -import {iterateFocusableElements} from './utils/iterate-focusable-elements.js' +import {IterateFocusableElements, iterateFocusableElements} from './utils/iterate-focusable-elements.js' import {uniqueId} from './utils/unique-id.js' eventListenerSignalPolyfill() @@ -114,7 +114,7 @@ const KEY_TO_DIRECTION = { /** * Options that control the behavior of the arrow focus behavior. */ -export interface FocusZoneSettings { +export type FocusZoneSettings = IterateFocusableElements & { /** * Choose the behavior applied in cases where focus is currently at either the first or * last element of the container. @@ -504,8 +504,13 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings): } } + const iterateFocusableElementsOptions: IterateFocusableElements = { + reverse: settings?.reverse, + strict: settings?.strict, + onlyTabbable: settings?.onlyTabbable, + } // Take all tabbable elements within container under management - beginFocusManagement(...iterateFocusableElements(container)) + beginFocusManagement(...iterateFocusableElements(container, iterateFocusableElementsOptions)) // Open the first tabbable element for tabbing const initialElement = @@ -519,14 +524,14 @@ export function focusZone(container: HTMLElement, settings?: FocusZoneSettings): for (const mutation of mutations) { for (const removedNode of mutation.removedNodes) { if (removedNode instanceof HTMLElement) { - endFocusManagement(...iterateFocusableElements(removedNode)) + endFocusManagement(...iterateFocusableElements(removedNode, iterateFocusableElementsOptions)) } } } for (const mutation of mutations) { for (const addedNode of mutation.addedNodes) { if (addedNode instanceof HTMLElement) { - beginFocusManagement(...iterateFocusableElements(addedNode)) + beginFocusManagement(...iterateFocusableElements(addedNode, iterateFocusableElementsOptions)) } } } From d6251ee1a0bbb37db930a43d1e7afca320944326 Mon Sep 17 00:00:00 2001 From: Stephanie Hong <41085564+JelloBagel@users.noreply.github.com> Date: Fri, 20 Oct 2023 01:09:52 +0000 Subject: [PATCH 2/5] Remove focus if display none --- src/utils/iterate-focusable-elements.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/utils/iterate-focusable-elements.ts b/src/utils/iterate-focusable-elements.ts index 8dacb5d..3a4a58d 100644 --- a/src/utils/iterate-focusable-elements.ts +++ b/src/utils/iterate-focusable-elements.ts @@ -98,10 +98,12 @@ export function isFocusable(elem: HTMLElement, strict = false): boolean { // Each of the conditions checked below require a reflow, thus are gated by the `strict` // argument. If any are true, the element is not focusable, even if tabindex is set. if (strict) { + const style = getComputedStyle(elem) const sizeInert = elem.offsetWidth === 0 || elem.offsetHeight === 0 - const visibilityInert = ['hidden', 'collapse'].includes(getComputedStyle(elem).visibility) + const visibilityInert = ['hidden', 'collapse'].includes(style.visibility) + const displayInert = style.display === 'none' || !elem.offsetParent const clientRectsInert = elem.getClientRects().length === 0 - if (sizeInert || visibilityInert || clientRectsInert) { + if (sizeInert || visibilityInert || clientRectsInert || displayInert) { return false } } From 82dc29ddae0e1726ff6d14c8588c1af4f7896ce8 Mon Sep 17 00:00:00 2001 From: Stephanie Hong <41085564+JelloBagel@users.noreply.github.com> Date: Fri, 20 Oct 2023 01:10:06 +0000 Subject: [PATCH 3/5] Add tests for strict option --- src/__tests__/focus-zone.test.tsx | 80 +++++++++++++++++++ .../iterate-focusable-elements.test.tsx | 67 ++++++++++++++++ 2 files changed, 147 insertions(+) diff --git a/src/__tests__/focus-zone.test.tsx b/src/__tests__/focus-zone.test.tsx index 1ddfc3c..9d94cbb 100644 --- a/src/__tests__/focus-zone.test.tsx +++ b/src/__tests__/focus-zone.test.tsx @@ -21,6 +21,26 @@ beforeAll(() => { getClientRects: { get: () => () => [42], }, + offsetParent: { + get() { + // eslint-disable-next-line @typescript-eslint/no-this-alias + for (let element = this; element; element = element.parentNode) { + if (element.style?.display?.toLowerCase() === 'none') { + return null + } + } + + if (this.style?.position?.toLowerCase() === 'fixed') { + return null + } + + if (this.tagName.toLowerCase() in ['html', 'body']) { + return null + } + + return this.parentNode + }, + }, }) } catch { // ignore @@ -569,3 +589,63 @@ it('Should handle elements being reordered', async () => { controller.abort() }) + +it('Should ignore hidden elements if strict', async () => { + const user = userEvent.setup() + const {container} = render( +