Skip to content

YPE-2776 - feat(ui): show loading overlay when changing chapters in BibleReader#259

Open
cameronapak wants to merge 2 commits into
mainfrom
cp/YPE-2776-react-sdk-components-bible-reader-show-loading-spinner-when-changing-chapters
Open

YPE-2776 - feat(ui): show loading overlay when changing chapters in BibleReader#259
cameronapak wants to merge 2 commits into
mainfrom
cp/YPE-2776-react-sdk-components-bible-reader-show-loading-spinner-when-changing-chapters

Conversation

@cameronapak
Copy link
Copy Markdown
Collaborator

@cameronapak cameronapak commented Jun 4, 2026

result.mp4

https://lifechurch.atlassian.net/browse/YPE-2776

What & why

When changing chapters in BibleReader, the previous chapter's text used to pulse while the next chapter loaded. A gentle pulse reads as real content settling in, so the stale text was mistaken for the new chapter for a beat — especially jarring because the heading swaps instantly. A bare spinner was rejected too (content → blank → content flash).

This ships the agreed fix: stale-while-revalidate with a dimmed overlay. The previous chapter's text stays mounted, dims to 40% opacity (static, no pulse), and a spinner floats over it — but only after a ~250ms delay, so fast/cached switches stay instant and never flash any loading UI.

How

All driven from BibleReaderBibleTextView is untouched (too many components depend on it):

  • Lifted usePassage into BibleReader.Content and pass results down via the existing passageState prop, so the reader owns the loading treatment. BibleTextView's internal fetch stays disabled — no double fetch.
  • isRefetching = passageLoading && passage !== nulluseDelayedLoading(…, 250) gates both the dim and the spinner. On refetch we pass loading: false to BibleTextView to kill its pulse; on first load we pass the real loading so it keeps its own centered spinner.
  • Reset main scroll to top on book/chapter change (instant); version-only swaps preserve scroll position. This also guarantees the spinner is in view.
  • A11y: the overlay is a single role="status" / aria-live="polite" live region; LoaderIcon is decorative (aria-hidden). No nested status regions.
  • Hoisted the previously-private useDelayedLoading hook out of bible-card.tsx into packages/ui/src/lib/use-delayed-loading.ts, shared by both components.

Tests

  • Unit (use-delayed-loading.test.ts): deterministic fake-timer coverage of the delay gate — a sub-threshold (fast) load never surfaces the spinner; a slow load surfaces it at the threshold.
  • Integration (ChapterChangeLoadingOverlay story, tagged integration): delayed MSW passage asserts stale text stays mounted (no flash), dim + spinner appear after the delay, the overlay clears and the new chapter renders, and scroll resets to top.

Full UI suite: 18 files / 146 tests green. Typecheck, lint (--max-warnings 0), and production build + style verification all pass. Changeset included (@youversion/platform-react-ui: patch).

Reviewer note

The overlay spinner uses sticky top-1/2 -translate-y-1/2 inside an absolute inset-0 layer to stay viewport-centered during version changes (where scroll is preserved). Chapter changes reset scroll to top, so they're trivially covered. Worth eyeballing the spinner position over a long, scrolled chapter on a version swap via the ChapterChangeLoadingOverlay story.

🤖 Generated with Claude Code

Greptile Summary

This PR adds a stale-while-revalidate loading overlay to BibleReader: when switching chapters, the previous chapter's text stays visible at 40% opacity with a centered spinner floating over it, appearing only after a 250 ms delay so cached/fast switches remain instant.

  • BibleReader.Content now owns the usePassage fetch and passes results into BibleTextView via the existing passageState prop (disabling BibleTextView's internal fetch to prevent double-fetching); isRefetching is derived from passageLoading && passage !== null so first-load still uses BibleTextView's own centered spinner.
  • useDelayedLoading is extracted from bible-card.tsx into a shared packages/ui/src/lib/use-delayed-loading.ts module consumed by both components; timer cleanup via useEffect return correctly prevents multiple simultaneous timers.
  • Scroll-to-top is triggered on [book, chapter] changes (not version changes) via a scrollContainerRef, and the spinner uses viewport-relative top-[50vh] so it stays in view regardless of content height.

Confidence Score: 5/5

Safe to merge — the stale-while-revalidate overlay logic is correctly implemented, no double-fetching occurs, and the delayed spinner correctly suppresses loading UI for fast/cached transitions.

The stale-while-revalidate logic is sound: useApiData intentionally does not reset data to null on refetch, which is exactly what isRefetching = passageLoading && passage !== null relies on. BibleTextView's internal fetch is correctly disabled when passageState is provided. Timer cleanup, viewport-relative spinner positioning, and scroll-reset scope are all correct. The integration story provides deterministic end-to-end coverage of the overlay lifecycle.

No files require special attention.

Important Files Changed

Filename Overview
packages/ui/src/components/bible-reader.tsx Lifts usePassage into BibleReader.Content, adds stale-while-revalidate dim overlay with delayed spinner, resets scroll on chapter/book change. Spinner uses viewport-relative top-[50vh] to stay in view during version changes.
packages/ui/src/lib/use-delayed-loading.ts New shared hook extracted from bible-card.tsx; timer cleanup via useEffect return is correct and satisfies the repo's timer-invalidation convention.
packages/ui/src/lib/use-delayed-loading.test.ts New unit tests with fake timers covering: no-load baseline, fast load (sub-threshold) never surfaces spinner, slow load surfaces spinner at threshold, and immediate reset on load clear.
packages/ui/src/components/bible-reader.stories.tsx Adds ChapterChangeLoadingOverlay integration story (tagged 'integration') with MSW delay; play function asserts stale text, overlay appearance after delay, overlay clear on load, and scroll-top reset.
packages/ui/src/components/bible-card.tsx Removes the inlined useDelayedLoading implementation and replaces it with an import from the shared module; no behavioral change.
.changeset/bible-reader-chapter-loading-overlay.md Patch changeset for @youversion/platform-react-ui with accurate description of the loading overlay behaviour.

Reviews (2): Last reviewed commit: "fix(ui): address Greptile review on chap..." | Re-trigger Greptile

…ibleReader

When changing chapters, BibleReader now keeps the previous chapter's text
mounted, dims it to 40% opacity, and floats a spinner over it after a short
delay — instead of pulsing stale text (confusingly reads as real content) or
flashing a blank spinner. Fast/cached switches stay instant since the dim and
spinner are gated behind a ~250ms delay.

- Lift usePassage into BibleReader.Content and pass passageState down so the
  reader owns the loading treatment without touching BibleTextView. On refetch
  it passes loading:false to suppress BibleTextView's pulse; first load keeps
  BibleTextView's own centered spinner.
- Reset scroll to top on book/chapter change (instant); version-only swaps keep
  scroll position.
- Single role=status live region for the overlay; LoaderIcon is decorative.
- Hoist useDelayedLoading out of bible-card.tsx into a shared lib util.
- Add deterministic unit test for the delay gate + an integration story
  asserting stale text persistence, delayed dim+spinner, recovery, and scroll
  reset.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 4, 2026

🦋 Changeset detected

Latest commit: ea38fa0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@youversion/platform-react-ui Patch
vite-react Patch
@youversion/platform-core Patch
@youversion/platform-react-hooks Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Comment thread packages/ui/src/components/bible-reader.tsx
Comment thread packages/ui/src/components/bible-reader.tsx
- Center the loading spinner with sticky top-[50vh] (viewport-relative)
  instead of top-1/2, which resolved to 50% of the tall passage container
  and could strand the spinner off-screen when scrolled (e.g. on version
  changes where scroll position is preserved).
- Drop redundant aria-live="polite"; role="status" already implies it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@cameronapak cameronapak requested a review from Dustin-Kelley June 5, 2026 16:42
Copy link
Copy Markdown
Collaborator

@bmanquen bmanquen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: go through the code changes and remove comments that you feel are not needed, if the code speaks for itself.

Great work!

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: We can probably remove a lot of the comments left in this file.

Comment on lines +3 to +9
/**
* Returns `true` only after `loading` has been continuously truthy for `delay` ms.
*
* Suppresses loading UI on fast/cached fetches so quick swaps feel instant, surfacing
* a spinner (and any accompanying treatment, e.g. a dimmed stale view) only when a
* fetch is genuinely slow. Shared by `BibleCard` and `BibleReader`.
*/
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need all of these comments?

@bmanquen
Copy link
Copy Markdown
Collaborator

bmanquen commented Jun 5, 2026

thought: should we also not change the heading (book and chapter) until the passage is loaded? So in this scenario it would just grey out the whole reader and not change until everything is loaded. The only thing that would change is the toolbar indicating which book and chapter you are on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants