diff --git a/.changeset/calm-firefox-focus.md b/.changeset/calm-firefox-focus.md new file mode 100644 index 00000000..c0950715 --- /dev/null +++ b/.changeset/calm-firefox-focus.md @@ -0,0 +1,5 @@ +--- +'@youversion/platform-react-ui': patch +--- + +Keep Firefox keyboard traversal moving directly from the Bible version picker controls to the first version row. diff --git a/packages/ui/src/components/bible-card.stories.tsx b/packages/ui/src/components/bible-card.stories.tsx index cb800f78..d01aa363 100644 --- a/packages/ui/src/components/bible-card.stories.tsx +++ b/packages/ui/src/components/bible-card.stories.tsx @@ -227,11 +227,10 @@ export const WithVersionPicker: Story = { await userEvent.click(versionPickerButton); const overlay = await getVersionPickerOverlay(pickerRoot); - await expect(await overlay.findByRole('dialog')).toBeInTheDocument(); + const dialog = await overlay.findByRole('dialog'); + await expect(dialog).toBeInTheDocument(); await waitFor(async () => { - await expect(pickerRoot.activeElement).toBe( - overlay.getByRole('button', { name: /select.*language/i }), - ); + await expect(pickerRoot.activeElement).toBe(dialog); }); // Wait for versions to actually load (not just the container) diff --git a/packages/ui/src/components/bible-reader.stories.tsx b/packages/ui/src/components/bible-reader.stories.tsx index 614d70ba..158af1eb 100644 --- a/packages/ui/src/components/bible-reader.stories.tsx +++ b/packages/ui/src/components/bible-reader.stories.tsx @@ -546,10 +546,9 @@ export const SignedInReaderOverlayOwnership: Story = { ); await userEvent.click(versionTrigger); const versionCanvas = await getPickerOverlay(versionRoot); + const versionDialog = await versionCanvas.findByRole('dialog'); await waitFor(async () => { - await expect(versionRoot.activeElement).toBe( - versionCanvas.getByRole('button', { name: /select.*language/i }), - ); + await expect(versionRoot.activeElement).toBe(versionDialog); }); await userEvent.click(versionCanvas.getByRole('button', { name: 'Close' })); await waitFor(async () => { diff --git a/packages/ui/src/components/bible-version-picker.shadow-isolation.stories.tsx b/packages/ui/src/components/bible-version-picker.shadow-isolation.stories.tsx index dfaf7807..39d18ed7 100644 --- a/packages/ui/src/components/bible-version-picker.shadow-isolation.stories.tsx +++ b/packages/ui/src/components/bible-version-picker.shadow-isolation.stories.tsx @@ -356,24 +356,31 @@ export const TopLayerEscapesClippingAndPreservesSemantics: Story = { }, }; -export const ForwardTabMovesFromCloseToFirstVersion: Story = { +export const ForwardTabFollowsPickerV2Order: Story = { play: async ({ canvasElement }) => { const { userEvent: browserUserEvent } = await import('vitest/browser'); const { root, panel } = await openPicker(canvasElement); - const languageTrigger = within(panel).getByRole('button', { name: /select.*language/i }); const closeButton = within(panel).getByRole('button', { name: /close/i }); + const versionSearch = within(panel).getByRole('textbox', { name: /search bible versions/i }); + const languageEntry = within(panel).getByRole('button', { name: /^language /i }); const firstVersion = await waitForElement( panel, '[data-testid="version-list"] button', 'version row not rendered', ); - await waitFor(async () => await expect(root.activeElement).toBe(languageTrigger)); + await waitFor(async () => await expect(root.activeElement).toBe(panel)); const forwardTraversalKey = /^((?!chrome|android).)*safari/i.test(navigator.userAgent) ? '{Alt>}{Tab}{/Alt}' : '{Tab}'; await browserUserEvent.keyboard(forwardTraversalKey); await expect(root.activeElement).toBe(closeButton); + await browserUserEvent.keyboard(forwardTraversalKey); + await expect(root.activeElement).toBe(versionSearch); + + await browserUserEvent.keyboard(forwardTraversalKey); + await expect(root.activeElement).toBe(languageEntry); + await browserUserEvent.keyboard(forwardTraversalKey); await expect(root.activeElement).toBe(firstVersion); }, @@ -417,7 +424,7 @@ export const StylingFocusDismissalAndRapidReopen: Story = { if (!topLayer) throw new Error('local top-layer container not created synchronously'); const panel = topLayer.querySelector('[data-slot="popover-content"]'); if (!panel) throw new Error('version picker panel not rendered synchronously'); - const languageTrigger = within(panel).getByRole('button', { name: /select.*language/i }); + const closeButton = within(panel).getByRole('button', { name: /close/i }); const openingPanelWindow = panel.ownerDocument.defaultView; if (!openingPanelWindow) throw new Error('version picker window not available'); @@ -436,7 +443,7 @@ export const StylingFocusDismissalAndRapidReopen: Story = { await waitFor(async () => { await expect(topLayer.querySelector('[data-slot="popover-content"]')).toBe(panel); await expect(panel).toHaveAttribute('data-state', 'open'); - await expect(root.activeElement).toBe(languageTrigger); + await expect(root.activeElement).toBe(closeButton); }); await expect(canvasElement.ownerDocument.scrollingElement?.scrollTop ?? 0).toBe( initialScrollTop, @@ -546,25 +553,34 @@ export const ProviderDirectionRejectsHostVisualValues: Story = { await expect(ownerWindow.getComputedStyle(trigger).direction).toBe('rtl'); await expect(ownerWindow.getComputedStyle(wrapper).direction).toBe('ltr'); - await userEvent.click(within(panel).getByRole('button', { name: /select a language/i })); + await userEvent.click(within(panel).getByRole('button', { name: /^language /i })); const languageTabs = await waitForElement( panel, '[data-slot="tabs-list"]', 'language tabs did not render', ); const languageTabsContainer = languageTabs.parentElement; - const languageContent = languageTabs.closest('[data-yv-sdk]'); - const languageInputGroup = languageContent?.querySelector( - '[data-slot="input-group"]', - ); - if (!languageTabsContainer || !languageInputGroup) { - throw new Error('language panel geometry landmarks not rendered'); - } + if (!languageTabsContainer) throw new Error('language tabs container not rendered'); const languageTabsInlineSize = languageTabs.offsetWidth; - const languageInputRadius = ownerWindow.getComputedStyle(languageInputGroup).borderRadius; const panelInlineSize = panel.offsetWidth; await expect(languageTabsInlineSize).toBe(languageTabsContainer.clientWidth - 32); - await expect(languageInputRadius).toBe('30px'); + + await userEvent.click(within(panel).getByRole('button', { name: /search languages/i })); + const baselineLanguageInputGroup = await waitForElement( + panel, + '[data-slot="input-group"]', + 'language search input group did not render', + ); + const languageInputRadius = ownerWindow.getComputedStyle( + baselineLanguageInputGroup, + ).borderRadius; + await userEvent.click(within(panel).getByRole('button', { name: /^back$/i })); + const languageTabsBeforeHostileStyle = await waitForElement( + panel, + '[data-slot="tabs-list"]', + 'language tabs did not return', + ); + await expect(languageTabsBeforeHostileStyle.offsetWidth).toBe(languageTabsInlineSize); const hostileStyle = ownerDocument.createElement('style'); hostileStyle.textContent = ` @@ -616,14 +632,36 @@ export const ProviderDirectionRejectsHostVisualValues: Story = { await expect(panelStyle.getPropertyValue('--yv-spacing').trim()).toBe('.25rem'); await expect(panelStyle.getPropertyValue('--spacing').trim()).toBe('.25rem'); await expect(panelStyle.getPropertyValue('--yv-radius').trim()).toBe('2rem'); - await expect(languageTabs.offsetWidth).toBe(languageTabsInlineSize); + await expect(languageTabsBeforeHostileStyle.offsetWidth).toBe(languageTabsInlineSize); + + await userEvent.click(within(panel).getByRole('button', { name: /search languages/i })); + const languageInputGroup = await waitForElement( + panel, + '[data-slot="input-group"]', + 'language search input group did not render', + ); await expect(ownerWindow.getComputedStyle(languageInputGroup).borderRadius).toBe( languageInputRadius, ); + + await userEvent.click(within(panel).getByRole('button', { name: /^back$/i })); + const restoredLanguageTabs = await waitForElement( + panel, + '[data-slot="tabs-list"]', + 'language tabs did not return', + ); + await expect(restoredLanguageTabs.offsetWidth).toBe(languageTabsInlineSize); + + await userEvent.click(within(panel).getByRole('button', { name: /back to bible versions/i })); + const restoredVersionInputGroup = await waitForElement( + panel, + '[data-slot="input-group"]', + 'version search input group did not return', + ); await waitFor(async () => { await expect(geometrySnapshot(trigger)).toEqual(triggerGeometry); await expect(panel.offsetWidth).toBe(panelInlineSize); - await expect(geometrySnapshot(inputGroup)).toEqual(inputGeometry); + await expect(geometrySnapshot(restoredVersionInputGroup)).toEqual(inputGeometry); }); } finally { hostileStyle.remove(); diff --git a/packages/ui/src/components/bible-version-picker/version-list.tsx b/packages/ui/src/components/bible-version-picker/version-list.tsx index 8dd8d7af..f4a45f03 100644 --- a/packages/ui/src/components/bible-version-picker/version-list.tsx +++ b/packages/ui/src/components/bible-version-picker/version-list.tsx @@ -249,7 +249,7 @@ export function VersionList({ {headerOwnsChrome ? : null} -
+
{filteredRecentVersions.length > 0 && ( <>