diff --git a/.changeset/chapter-picker-on-select-and-reader-context-threading.md b/.changeset/chapter-picker-on-select-and-reader-context-threading.md new file mode 100644 index 00000000..f2d6c32b --- /dev/null +++ b/.changeset/chapter-picker-on-select-and-reader-context-threading.md @@ -0,0 +1,9 @@ +--- +"@youversion/platform-react-ui": minor +--- + +Add `onSelect` callback to `BibleChapterPicker.Content` and `onChapterPickerPress` to `BibleReader.Root` + +- `BibleChapterPickerSelectData` type exported for `onSelect` payload +- `onSelect` prop on `Content` fires after internal state updates, before `onRequestClose` +- `onChapterPickerPress` prop on `BibleReader.Root` threaded through context to `Toolbar`, suppressing default popover when provided diff --git a/packages/ui/src/components/bible-chapter-picker.test.tsx b/packages/ui/src/components/bible-chapter-picker.test.tsx index 6fdac5ac..7b96b866 100644 --- a/packages/ui/src/components/bible-chapter-picker.test.tsx +++ b/packages/ui/src/components/bible-chapter-picker.test.tsx @@ -4,7 +4,7 @@ // We stub ResizeObserver for jsdom (used by Radix/@floating-ui). The stub methods are intentionally no-ops. /* eslint-disable @typescript-eslint/no-empty-function */ import { describe, it, expect, vi, beforeEach } from 'vitest'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; // ResizeObserver is used by @floating-ui/dom (Radix Popover) @@ -16,6 +16,7 @@ class ResizeObserverMock { globalThis.ResizeObserver = ResizeObserverMock as unknown as typeof ResizeObserver; import { BibleChapterPicker } from './bible-chapter-picker'; +import type { BibleChapterPickerSelectData } from './bible-chapter-picker'; import { useBooks, useTheme } from '@youversion/platform-react-hooks'; import type { BibleBook } from '@youversion/platform-core'; @@ -115,3 +116,83 @@ describe('BibleChapterPicker - default popover mode', () => { expect(await screen.findByPlaceholderText('Search')).toBeInTheDocument(); }); }); + +describe('BibleChapterPicker.Content onSelect', () => { + beforeEach(() => { + vi.clearAllMocks(); + setupDefaultMocks(); + }); + + it('calls onSelect with { book, chapter, versionId } when a normal chapter is clicked (with onChapterPickerPress)', async () => { + const user = userEvent.setup(); + const onSelect = vi.fn(); + + render( + + + + , + ); + + await user.click(screen.getByText('2')); + + expect(onSelect).toHaveBeenCalledTimes(1); + const payload = onSelect.mock.calls[0]![0] as BibleChapterPickerSelectData; + expect(payload).toEqual({ + book: 'GEN', + chapter: '2', + versionId: 3034, + }); + }); + + it('calls onSelect when intro chapter is clicked (with onChapterPickerPress)', async () => { + const user = userEvent.setup(); + const onSelect = vi.fn(); + + render( + + + + , + ); + + const introButton = screen.getByTestId('intro-chapter-button'); + await user.click(introButton); + + expect(onSelect).toHaveBeenCalledTimes(1); + expect(onSelect).toHaveBeenCalledWith({ + book: 'GEN', + chapter: 'INTRO', + versionId: 3034, + }); + }); + + it('preserves default popover behavior when onSelect is not provided', async () => { + const user = userEvent.setup(); + + render( + + + , + ); + + await user.click(screen.getByRole('button')); + expect(await screen.findByText('Books')).toBeInTheDocument(); + + await user.click(screen.getByText('2')); + + await waitFor(() => { + expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/packages/ui/src/components/bible-chapter-picker.tsx b/packages/ui/src/components/bible-chapter-picker.tsx index 0276c9ec..aa4f2b5e 100644 --- a/packages/ui/src/components/bible-chapter-picker.tsx +++ b/packages/ui/src/components/bible-chapter-picker.tsx @@ -27,8 +27,11 @@ export interface BibleChapterPickerPressData { versionId: number; } +export type BibleChapterPickerSelectData = BibleChapterPickerPressData; + export type BibleChapterPickerContentProps = { onRequestClose?: () => void; + onSelect?: (data: BibleChapterPickerSelectData) => void; }; type BibleChapterPickerContextType = { @@ -285,7 +288,7 @@ function Trigger({ asChild = true, children, ...props }: TriggerProps) { ); } -function Content({ onRequestClose }: BibleChapterPickerContentProps) { +function Content({ onRequestClose, onSelect }: BibleChapterPickerContentProps) { const { book, defaultBook, @@ -297,6 +300,7 @@ function Content({ onRequestClose }: BibleChapterPickerContentProps) { registerBookElement, setBook, setChapter, + versionId, } = useBibleChapterPickerContext(); const handleChapterButtonClick = (bookId: string, passageId: string) => { @@ -305,6 +309,7 @@ function Content({ onRequestClose }: BibleChapterPickerContentProps) { setBook(bookId); setChapter(chapterId); setSearchQuery(''); + onSelect?.({ book: bookId, chapter: chapterId, versionId }); onRequestClose?.(); } }; @@ -337,6 +342,7 @@ function Content({ onRequestClose }: BibleChapterPickerContentProps) { key={`${bookItem.id}-${bookItem.intro.passage_id}`} variant="secondary" size="icon" + data-testid="intro-chapter-button" className="yv:aspect-square yv:w-full yv:h-full yv:flex yv:items-center yv:justify-center yv:rounded-[4px]" onClick={() => handleChapterButtonClick(bookItem.id, bookItem.intro?.passage_id || '') diff --git a/packages/ui/src/components/bible-reader.test.tsx b/packages/ui/src/components/bible-reader.test.tsx index f983ce18..cc7616af 100644 --- a/packages/ui/src/components/bible-reader.test.tsx +++ b/packages/ui/src/components/bible-reader.test.tsx @@ -264,3 +264,38 @@ describe('BibleReader theme settings', () => { expect(nextSnap.fontFamily).toBe(INTER_FONT); }); }); + +describe('BibleReader Toolbar - onChapterPickerPress', () => { + beforeEach(() => { + vi.clearAllMocks(); + setupDefaultMocks(); + }); + + it('calls onChapterPickerPress from Root when chapter nav button is clicked and hides popover', async () => { + const user = userEvent.setup(); + const onChapterPickerPress = vi.fn(); + + render( + + + , + ); + + const chapterButton = screen.getByRole('button', { name: 'Change Bible book and chapter' }); + await user.click(chapterButton); + + expect(onChapterPickerPress).toHaveBeenCalledTimes(1); + expect(onChapterPickerPress).toHaveBeenCalledWith({ + book: 'JHN', + chapter: '1', + versionId: 3034, + }); + + expect(screen.queryByPlaceholderText('Search')).not.toBeInTheDocument(); + }); +}); diff --git a/packages/ui/src/components/bible-reader.tsx b/packages/ui/src/components/bible-reader.tsx index d4af5025..1136ad7e 100644 --- a/packages/ui/src/components/bible-reader.tsx +++ b/packages/ui/src/components/bible-reader.tsx @@ -21,7 +21,7 @@ import { } from 'react'; import { cn } from '@/lib/utils'; import { DEFAULT_LICENSE_FREE_BIBLE_VERSION, getAdjacentChapter } from '@youversion/platform-core'; -import { BibleChapterPicker } from './bible-chapter-picker'; +import { BibleChapterPicker, type BibleChapterPickerPressData } from './bible-chapter-picker'; import { BibleVersionPicker } from './bible-version-picker'; import { GearIcon } from './icons/gear'; import { InfoIcon } from './icons/info'; @@ -51,6 +51,7 @@ type BibleReaderContextType = { showVerseNumbers: boolean; background: 'light' | 'dark'; onFootnotePress?: (data: FootnoteData) => void; + onChapterPickerPress?: (data: BibleChapterPickerPressData) => void; }; const BibleReaderContext = createContext(null); @@ -83,6 +84,7 @@ export type RootProps = { showVerseNumbers?: boolean; background?: 'light' | 'dark'; onFootnotePress?: (data: FootnoteData) => void; + onChapterPickerPress?: (data: BibleChapterPickerPressData) => void; children?: ReactNode; }; @@ -181,6 +183,7 @@ function Root({ showVerseNumbers = true, background, onFootnotePress, + onChapterPickerPress, children, }: RootProps) { const [book, setBook] = useControllableState({ @@ -286,6 +289,7 @@ function Root({ showVerseNumbers, background: theme, onFootnotePress, + onChapterPickerPress, }; return ( @@ -552,6 +556,7 @@ function Toolbar({ border = 'top', onOpenBibleThemeSettings }: BibleReaderToolba currentFontSize, setCurrentFontSize, background, + onChapterPickerPress, } = useBibleReaderContext(); const yvContext = useContext(YouVersionContext); const themesSettingsValuesRef = useRef({ @@ -638,6 +643,7 @@ function Toolbar({ border = 'top', onOpenBibleThemeSettings }: BibleReaderToolba onChapterChange={setChapter} versionId={versionId} background={background} + onChapterPickerPress={onChapterPickerPress} > {({ chapterLabel, currentBook, loading }) => ( diff --git a/packages/ui/src/components/index.ts b/packages/ui/src/components/index.ts index e5ecb850..c3f5651d 100644 --- a/packages/ui/src/components/index.ts +++ b/packages/ui/src/components/index.ts @@ -2,6 +2,7 @@ export { BibleChapterPicker, type RootProps, type BibleChapterPickerRootProps, + type BibleChapterPickerSelectData, type TriggerProps, type BibleChapterPickerContentProps, type BibleChapterPickerPressData,