From 9c0230ac93a86291115852eb9c21a9afe0e9a2f7 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Thu, 19 Mar 2026 09:48:03 -0500 Subject: [PATCH 1/6] fix: replace Loading... text with spinner icon in version button Amp-Thread-ID: https://ampcode.com/threads/T-019d0681-30c6-7664-ac27-ea412cabfbce Co-authored-by: Amp --- packages/ui/src/components/bible-reader.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/ui/src/components/bible-reader.tsx b/packages/ui/src/components/bible-reader.tsx index ccd1431f..9e4e6c8d 100644 --- a/packages/ui/src/components/bible-reader.tsx +++ b/packages/ui/src/components/bible-reader.tsx @@ -435,7 +435,11 @@ function Toolbar({ border = 'top' }: { border?: 'top' | 'bottom' }) { className="yv:font-bold yv:text-foreground" disabled={loading} > - {loading ? 'Loading...' : version?.localized_abbreviation || 'Select version'} + {loading ? ( + + ) : ( + version?.localized_abbreviation || 'Select version' + )} )} From 91c2832c746a5b6fc1ca89371406a47f5ae898bc Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Thu, 19 Mar 2026 09:50:31 -0500 Subject: [PATCH 2/6] Added changeset --- .changeset/tricky-keys-train.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/tricky-keys-train.md diff --git a/.changeset/tricky-keys-train.md b/.changeset/tricky-keys-train.md new file mode 100644 index 00000000..51903b34 --- /dev/null +++ b/.changeset/tricky-keys-train.md @@ -0,0 +1,7 @@ +--- +'@youversion/platform-react-hooks': patch +'@youversion/platform-core': patch +'@youversion/platform-react-ui': patch +--- + +fix: use spinner icon instead of "Loading..." text in Bible version button From dcbe3bc98d3dfd31806bafb05c9f1a2bc78ac807 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Thu, 19 Mar 2026 10:38:10 -0500 Subject: [PATCH 3/6] Add wrapper div to bible reader version button This change adds a wrapper div around the version abbreviation within the Bible reader's toolbar button. This is done to minimize layout shifts when the version is loading or selected, ensuring a more stable user experience. --- packages/ui/src/components/bible-reader.tsx | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/ui/src/components/bible-reader.tsx b/packages/ui/src/components/bible-reader.tsx index 9e4e6c8d..16a4849a 100644 --- a/packages/ui/src/components/bible-reader.tsx +++ b/packages/ui/src/components/bible-reader.tsx @@ -435,11 +435,14 @@ function Toolbar({ border = 'top' }: { border?: 'top' | 'bottom' }) { className="yv:font-bold yv:text-foreground" disabled={loading} > - {loading ? ( - - ) : ( - version?.localized_abbreviation || 'Select version' - )} + {/* This div exists merely as a wrapper to minimize width layout shifting */} +
+ {loading ? ( + + ) : ( + version?.localized_abbreviation || 'Select version' + )} +
)} From d1e51bf290583429660ae582c6bddd461eb7ba52 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Thu, 19 Mar 2026 11:54:15 -0500 Subject: [PATCH 4/6] Added tests --- .../src/components/bible-reader.stories.tsx | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/packages/ui/src/components/bible-reader.stories.tsx b/packages/ui/src/components/bible-reader.stories.tsx index b88a3f49..2b188222 100644 --- a/packages/ui/src/components/bible-reader.stories.tsx +++ b/packages/ui/src/components/bible-reader.stories.tsx @@ -639,6 +639,53 @@ export const WithoutAuth: Story = { }, }; +/** + * Tests that the Bible version button in the toolbar shows a loading spinner + * initially and then transitions to showing the version abbreviation once loaded. + */ +export const VersionButtonLoadingStates: Story = { + tags: ['integration'], + args: { + defaultVersionId: 111, + book: 'JHN', + chapter: '1', + }, + render: (args) => ( +
+ + + + +
+ ), + play: async () => { + // The version button should exist in the toolbar + const versionButton = screen.getByRole('button', { name: /change bible version/i }); + await expect(versionButton).toBeInTheDocument(); + + // Initially the button should be disabled and show a spinner while loading + // (the LoaderIcon has role="status" and aria-label="Loading") + const spinner = versionButton.querySelector('[role="status"]'); + // If the data loads fast enough the spinner may already be gone, + // so we just check it is either spinning OR already showing text. + if (spinner) { + await expect(versionButton).toBeDisabled(); + } + + // After loading completes, the button should show the version abbreviation (e.g. "NIV") + await waitFor( + async () => { + await expect(versionButton).not.toBeDisabled(); + // Spinner should be gone + await expect(versionButton.querySelector('[role="status"]')).not.toBeInTheDocument(); + // Should display the abbreviation text + await expect(versionButton.textContent).toMatch(/[A-Z]{2,}/); + }, + { timeout: 5000 }, + ); + }, +}; + /** * Tests that a rich intro chapter (Joshua) renders correctly with real-world content * including structured sections (At a Glance, Purpose, Major Themes), italic spans, From b05f85955ec0b286c523e865b6d113fbf38b42e6 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Thu, 19 Mar 2026 12:01:55 -0500 Subject: [PATCH 5/6] Added aria label to the loading button --- packages/ui/src/components/bible-reader.stories.tsx | 8 ++++++-- packages/ui/src/components/bible-reader.tsx | 1 + 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/ui/src/components/bible-reader.stories.tsx b/packages/ui/src/components/bible-reader.stories.tsx index 2b188222..f9351eeb 100644 --- a/packages/ui/src/components/bible-reader.stories.tsx +++ b/packages/ui/src/components/bible-reader.stories.tsx @@ -659,8 +659,8 @@ export const VersionButtonLoadingStates: Story = { ), play: async () => { - // The version button should exist in the toolbar - const versionButton = screen.getByRole('button', { name: /change bible version/i }); + // The version button should exist in the toolbar (label varies by loading state) + const versionButton = screen.getByRole('button', { name: /bible version/i }); await expect(versionButton).toBeInTheDocument(); // Initially the button should be disabled and show a spinner while loading @@ -670,6 +670,8 @@ export const VersionButtonLoadingStates: Story = { // so we just check it is either spinning OR already showing text. if (spinner) { await expect(versionButton).toBeDisabled(); + // aria-label should indicate loading state for screen readers + await expect(versionButton).toHaveAttribute('aria-label', 'Loading Bible version'); } // After loading completes, the button should show the version abbreviation (e.g. "NIV") @@ -678,6 +680,8 @@ export const VersionButtonLoadingStates: Story = { await expect(versionButton).not.toBeDisabled(); // Spinner should be gone await expect(versionButton.querySelector('[role="status"]')).not.toBeInTheDocument(); + // aria-label should switch to "Change" once loaded + await expect(versionButton).toHaveAttribute('aria-label', 'Change Bible version'); // Should display the abbreviation text await expect(versionButton.textContent).toMatch(/[A-Z]{2,}/); }, diff --git a/packages/ui/src/components/bible-reader.tsx b/packages/ui/src/components/bible-reader.tsx index 16a4849a..b6ed276f 100644 --- a/packages/ui/src/components/bible-reader.tsx +++ b/packages/ui/src/components/bible-reader.tsx @@ -434,6 +434,7 @@ function Toolbar({ border = 'top' }: { border?: 'top' | 'bottom' }) { variant="secondary" className="yv:font-bold yv:text-foreground" disabled={loading} + aria-label={loading ? 'Loading Bible version' : 'Change Bible version'} > {/* This div exists merely as a wrapper to minimize width layout shifting */}
From d41e105b8ddd29a1bf4906378f2be4a68624d4e4 Mon Sep 17 00:00:00 2001 From: Cameron Pak Date: Thu, 19 Mar 2026 12:09:50 -0500 Subject: [PATCH 6/6] test: use MSW delay for deterministic loading-state assertions in BibleReader stories Amp-Thread-ID: https://ampcode.com/threads/T-019d0711-2000-740f-95fe-860fdfd2d8fd Co-authored-by: Amp --- .../src/components/bible-reader.stories.tsx | 33 +++++++++++++------ 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/packages/ui/src/components/bible-reader.stories.tsx b/packages/ui/src/components/bible-reader.stories.tsx index f9351eeb..015e7e48 100644 --- a/packages/ui/src/components/bible-reader.stories.tsx +++ b/packages/ui/src/components/bible-reader.stories.tsx @@ -1,8 +1,10 @@ import type { Meta, StoryObj } from '@storybook/react-vite'; import { expect, fn, screen, spyOn, userEvent, waitFor } from 'storybook/test'; +import { http, HttpResponse, delay } from 'msw'; import { BibleReader } from './bible-reader'; import { setupAuthenticatedUser } from '../test/utils'; import { INTER_FONT, SOURCE_SERIF_FONT } from '@/lib/verse-html-utils'; +import mockBibles from '../test/mock-data/bibles.json'; let signInMock: ReturnType; @@ -630,7 +632,7 @@ export const WithoutAuth: Story = { const chapterButton = screen.getByRole('button', { name: /change bible book and chapter/i }); await expect(chapterButton).toBeInTheDocument(); - const versionButton = screen.getByRole('button', { name: /change bible version/i }); + const versionButton = await screen.findByRole('button', { name: /bible version/i }); await expect(versionButton).toBeInTheDocument(); // Settings should still work @@ -650,6 +652,22 @@ export const VersionButtonLoadingStates: Story = { book: 'JHN', chapter: '1', }, + parameters: { + msw: { + handlers: [ + // Delay the version endpoint so the loading state is reliably observable + http.get('*/v1/bibles/:id', async ({ params }) => { + await delay(1000); + const id = params.id as string; + const bible = + mockBibles.individual[id as keyof typeof mockBibles.individual] ?? + mockBibles.collections.default.data.find((b) => b.id === Number(id)); + if (bible) return HttpResponse.json(bible); + return new HttpResponse(null, { status: 404 }); + }), + ], + }, + }, render: (args) => (
@@ -663,16 +681,11 @@ export const VersionButtonLoadingStates: Story = { const versionButton = screen.getByRole('button', { name: /bible version/i }); await expect(versionButton).toBeInTheDocument(); - // Initially the button should be disabled and show a spinner while loading - // (the LoaderIcon has role="status" and aria-label="Loading") + // The delayed MSW handler guarantees the loading state is visible const spinner = versionButton.querySelector('[role="status"]'); - // If the data loads fast enough the spinner may already be gone, - // so we just check it is either spinning OR already showing text. - if (spinner) { - await expect(versionButton).toBeDisabled(); - // aria-label should indicate loading state for screen readers - await expect(versionButton).toHaveAttribute('aria-label', 'Loading Bible version'); - } + await expect(spinner).toBeInTheDocument(); + await expect(versionButton).toBeDisabled(); + await expect(versionButton).toHaveAttribute('aria-label', 'Loading Bible version'); // After loading completes, the button should show the version abbreviation (e.g. "NIV") await waitFor(