Skip to content

Commit dd52fbe

Browse files
YPE-1564: replace Loading... text with spinner icon in version button (#196)
* 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 <amp@ampcode.com> * Added changeset * 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. * Added tests * Added aria label to the loading button * 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 <amp@ampcode.com> --------- Co-authored-by: Amp <amp@ampcode.com>
1 parent dc9d45e commit dd52fbe

3 files changed

Lines changed: 81 additions & 2 deletions

File tree

.changeset/tricky-keys-train.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@youversion/platform-react-hooks': patch
3+
'@youversion/platform-core': patch
4+
'@youversion/platform-react-ui': patch
5+
---
6+
7+
fix: use spinner icon instead of "Loading..." text in Bible version button

packages/ui/src/components/bible-reader.stories.tsx

Lines changed: 65 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type { Meta, StoryObj } from '@storybook/react-vite';
22
import { expect, fn, screen, spyOn, userEvent, waitFor } from 'storybook/test';
3+
import { http, HttpResponse, delay } from 'msw';
34
import { BibleReader } from './bible-reader';
45
import { setupAuthenticatedUser } from '../test/utils';
56
import { INTER_FONT, SOURCE_SERIF_FONT } from '@/lib/verse-html-utils';
7+
import mockBibles from '../test/mock-data/bibles.json';
68

79
let signInMock: ReturnType<typeof fn>;
810

@@ -630,7 +632,7 @@ export const WithoutAuth: Story = {
630632
const chapterButton = screen.getByRole('button', { name: /change bible book and chapter/i });
631633
await expect(chapterButton).toBeInTheDocument();
632634

633-
const versionButton = screen.getByRole('button', { name: /change bible version/i });
635+
const versionButton = await screen.findByRole('button', { name: /bible version/i });
634636
await expect(versionButton).toBeInTheDocument();
635637

636638
// Settings should still work
@@ -639,6 +641,68 @@ export const WithoutAuth: Story = {
639641
},
640642
};
641643

644+
/**
645+
* Tests that the Bible version button in the toolbar shows a loading spinner
646+
* initially and then transitions to showing the version abbreviation once loaded.
647+
*/
648+
export const VersionButtonLoadingStates: Story = {
649+
tags: ['integration'],
650+
args: {
651+
defaultVersionId: 111,
652+
book: 'JHN',
653+
chapter: '1',
654+
},
655+
parameters: {
656+
msw: {
657+
handlers: [
658+
// Delay the version endpoint so the loading state is reliably observable
659+
http.get('*/v1/bibles/:id', async ({ params }) => {
660+
await delay(1000);
661+
const id = params.id as string;
662+
const bible =
663+
mockBibles.individual[id as keyof typeof mockBibles.individual] ??
664+
mockBibles.collections.default.data.find((b) => b.id === Number(id));
665+
if (bible) return HttpResponse.json(bible);
666+
return new HttpResponse(null, { status: 404 });
667+
}),
668+
],
669+
},
670+
},
671+
render: (args) => (
672+
<div className="yv:h-screen yv:bg-background">
673+
<BibleReader.Root {...args}>
674+
<BibleReader.Content />
675+
<BibleReader.Toolbar />
676+
</BibleReader.Root>
677+
</div>
678+
),
679+
play: async () => {
680+
// The version button should exist in the toolbar (label varies by loading state)
681+
const versionButton = screen.getByRole('button', { name: /bible version/i });
682+
await expect(versionButton).toBeInTheDocument();
683+
684+
// The delayed MSW handler guarantees the loading state is visible
685+
const spinner = versionButton.querySelector('[role="status"]');
686+
await expect(spinner).toBeInTheDocument();
687+
await expect(versionButton).toBeDisabled();
688+
await expect(versionButton).toHaveAttribute('aria-label', 'Loading Bible version');
689+
690+
// After loading completes, the button should show the version abbreviation (e.g. "NIV")
691+
await waitFor(
692+
async () => {
693+
await expect(versionButton).not.toBeDisabled();
694+
// Spinner should be gone
695+
await expect(versionButton.querySelector('[role="status"]')).not.toBeInTheDocument();
696+
// aria-label should switch to "Change" once loaded
697+
await expect(versionButton).toHaveAttribute('aria-label', 'Change Bible version');
698+
// Should display the abbreviation text
699+
await expect(versionButton.textContent).toMatch(/[A-Z]{2,}/);
700+
},
701+
{ timeout: 5000 },
702+
);
703+
},
704+
};
705+
642706
/**
643707
* Tests that a rich intro chapter (Joshua) renders correctly with real-world content
644708
* including structured sections (At a Glance, Purpose, Major Themes), italic spans,

packages/ui/src/components/bible-reader.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,8 +434,16 @@ function Toolbar({ border = 'top' }: { border?: 'top' | 'bottom' }) {
434434
variant="secondary"
435435
className="yv:font-bold yv:text-foreground"
436436
disabled={loading}
437+
aria-label={loading ? 'Loading Bible version' : 'Change Bible version'}
437438
>
438-
{loading ? 'Loading...' : version?.localized_abbreviation || 'Select version'}
439+
{/* This div exists merely as a wrapper to minimize width layout shifting */}
440+
<div className="yv:min-w-[3ch] yv:flex yv:justify-center">
441+
{loading ? (
442+
<LoaderIcon className="yv:size-4 yv:animate-spin yv:text-muted-foreground" />
443+
) : (
444+
version?.localized_abbreviation || 'Select version'
445+
)}
446+
</div>
439447
</Button>
440448
)}
441449
</BibleVersionPicker.Trigger>

0 commit comments

Comments
 (0)