From e27ad0219104d2bd73e9bd993f9a77b546038b10 Mon Sep 17 00:00:00 2001 From: Ryan Gossiaux Date: Sat, 10 Jun 2023 23:28:36 -0700 Subject: [PATCH] Port "Ensure correct DOM node order when performing focus actions" tailwindlabs/headlessui#1038 This test didn't actually fail for me but w/e. --- src/lib/components/tabs/tabs.test.ts | 39 +++++++++++++++++++ .../test-utils/accessibility-assertions.ts | 8 +--- src/lib/utils/focus-management.ts | 16 +++++--- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/src/lib/components/tabs/tabs.test.ts b/src/lib/components/tabs/tabs.test.ts index 613b903e..07cf63f0 100644 --- a/src/lib/components/tabs/tabs.test.ts +++ b/src/lib/components/tabs/tabs.test.ts @@ -83,6 +83,45 @@ describe('Rendering', () => { assertTabs({ active: 0 }) }) + it('should guarantee the order of DOM nodes when performing actions', async () => { + render(svelte` + + + + + + Tab 1 + {#if !hide} + Tab 2 + {/if} + Tab 3 + + + + Content 1 + {#if !hide} + Content 2 + {/if} + Content 3 + + + `) + + await click(getByText('toggle')) // Remove Tab 2 + await click(getByText('toggle')) // Re-add Tab 2 + + await press(Keys.Tab) + assertTabs({ active: 0 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 1 }) + + await press(Keys.ArrowRight) + assertTabs({ active: 2 }) + }) + describe('`slot props`', () => { it('should expose the `selectedIndex` on the `TabGroup` component', async () => { render(svelte` diff --git a/src/lib/test-utils/accessibility-assertions.ts b/src/lib/test-utils/accessibility-assertions.ts index 9e8d2e22..73201eed 100644 --- a/src/lib/test-utils/accessibility-assertions.ts +++ b/src/lib/test-utils/accessibility-assertions.ts @@ -1359,12 +1359,8 @@ export function assertTabs( expect(list).toHaveAttribute("role", "tablist"); expect(list).toHaveAttribute("aria-orientation", orientation); - let activeTab = tabs.find( - (tab) => tab.dataset.headlessuiIndex === "" + active - ); - let activePanel = panels.find( - (panel) => panel.dataset.headlessuiIndex === "" + active - ); + let activeTab = Array.from(list.querySelectorAll('[id^="headlessui-tabs-tab-"]'))[active] + let activePanel = panels.find(panel => panel.id === activeTab.getAttribute('aria-controls')) for (let tab of tabs) { expect(tab).toHaveAttribute("id"); diff --git a/src/lib/utils/focus-management.ts b/src/lib/utils/focus-management.ts index 2590cd87..f9c8118d 100644 --- a/src/lib/utils/focus-management.ts +++ b/src/lib/utils/focus-management.ts @@ -16,10 +16,10 @@ let focusableSelector = [ .map( process.env.NODE_ENV === "test" ? // TODO: Remove this once JSDOM fixes the issue where an element that is - // "hidden" can be the document.activeElement, because this is not possible - // in real browsers. - (selector) => - `${selector}:not([tabindex='-1']):not([style*='display: none'])` + // "hidden" can be the document.activeElement, because this is not possible + // in real browsers. + (selector) => + `${selector}:not([tabindex='-1']):not([style*='display: none'])` : (selector) => `${selector}:not([tabindex='-1'])` ) .join(","); @@ -100,7 +100,13 @@ export function focusElement(element: HTMLElement | null) { export function focusIn(container: HTMLElement | HTMLElement[], focus: Focus) { let elements = Array.isArray(container) - ? container + ? container.slice().sort((a, b) => { + let position = a.compareDocumentPosition(b) + + if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1 + if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1 + return 0 + }) : getFocusableElements(container); let active = document.activeElement as HTMLElement;