Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
83 changes: 82 additions & 1 deletion packages/ui/src/components/bible-chapter-picker.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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';

Expand Down Expand Up @@ -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(
<BibleChapterPicker.Root
versionId={3034}
book="GEN"
chapter="1"
onChapterPickerPress={vi.fn()}
>
<BibleChapterPicker.Trigger />
<BibleChapterPicker.Content onSelect={onSelect} />
</BibleChapterPicker.Root>,
);

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(
<BibleChapterPicker.Root
versionId={3034}
book="GEN"
chapter="1"
onChapterPickerPress={vi.fn()}
>
<BibleChapterPicker.Trigger />
<BibleChapterPicker.Content onSelect={onSelect} />
</BibleChapterPicker.Root>,
);

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(
<BibleChapterPicker.Root versionId={3034} book="GEN" chapter="1">
<BibleChapterPicker.Trigger />
</BibleChapterPicker.Root>,
);

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();
});
});
});
8 changes: 7 additions & 1 deletion packages/ui/src/components/bible-chapter-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ export interface BibleChapterPickerPressData {
versionId: number;
}

export type BibleChapterPickerSelectData = BibleChapterPickerPressData;

export type BibleChapterPickerContentProps = {
onRequestClose?: () => void;
onSelect?: (data: BibleChapterPickerSelectData) => void;
};

type BibleChapterPickerContextType = {
Expand Down Expand Up @@ -285,7 +288,7 @@ function Trigger({ asChild = true, children, ...props }: TriggerProps) {
);
}

function Content({ onRequestClose }: BibleChapterPickerContentProps) {
function Content({ onRequestClose, onSelect }: BibleChapterPickerContentProps) {
const {
book,
defaultBook,
Expand All @@ -297,6 +300,7 @@ function Content({ onRequestClose }: BibleChapterPickerContentProps) {
registerBookElement,
setBook,
setChapter,
versionId,
} = useBibleChapterPickerContext();

const handleChapterButtonClick = (bookId: string, passageId: string) => {
Expand All @@ -305,6 +309,7 @@ function Content({ onRequestClose }: BibleChapterPickerContentProps) {
setBook(bookId);
setChapter(chapterId);
setSearchQuery('');
onSelect?.({ book: bookId, chapter: chapterId, versionId });
onRequestClose?.();
}
};
Expand Down Expand Up @@ -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 || '')
Expand Down
35 changes: 35 additions & 0 deletions packages/ui/src/components/bible-reader.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
<BibleReader.Root
defaultVersionId={3034}
defaultBook="JHN"
defaultChapter="1"
onChapterPickerPress={onChapterPickerPress}
>
<BibleReader.Toolbar />
</BibleReader.Root>,
);

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();
});
});
8 changes: 7 additions & 1 deletion packages/ui/src/components/bible-reader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -51,6 +51,7 @@ type BibleReaderContextType = {
showVerseNumbers: boolean;
background: 'light' | 'dark';
onFootnotePress?: (data: FootnoteData) => void;
onChapterPickerPress?: (data: BibleChapterPickerPressData) => void;
};

const BibleReaderContext = createContext<BibleReaderContextType | null>(null);
Expand Down Expand Up @@ -83,6 +84,7 @@ export type RootProps = {
showVerseNumbers?: boolean;
background?: 'light' | 'dark';
onFootnotePress?: (data: FootnoteData) => void;
onChapterPickerPress?: (data: BibleChapterPickerPressData) => void;
children?: ReactNode;
};

Expand Down Expand Up @@ -181,6 +183,7 @@ function Root({
showVerseNumbers = true,
background,
onFootnotePress,
onChapterPickerPress,
children,
}: RootProps) {
const [book, setBook] = useControllableState({
Expand Down Expand Up @@ -286,6 +289,7 @@ function Root({
showVerseNumbers,
background: theme,
onFootnotePress,
onChapterPickerPress,
};

return (
Expand Down Expand Up @@ -552,6 +556,7 @@ function Toolbar({ border = 'top', onOpenBibleThemeSettings }: BibleReaderToolba
currentFontSize,
setCurrentFontSize,
background,
onChapterPickerPress,
} = useBibleReaderContext();
const yvContext = useContext(YouVersionContext);
const themesSettingsValuesRef = useRef<BibleThemeSettingsValues>({
Expand Down Expand Up @@ -638,6 +643,7 @@ function Toolbar({ border = 'top', onOpenBibleThemeSettings }: BibleReaderToolba
onChapterChange={setChapter}
versionId={versionId}
background={background}
onChapterPickerPress={onChapterPickerPress}
>
<BibleChapterPicker.Trigger>
{({ chapterLabel, currentBook, loading }) => (
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export {
BibleChapterPicker,
type RootProps,
type BibleChapterPickerRootProps,
type BibleChapterPickerSelectData,
Comment thread
cameronapak marked this conversation as resolved.
type TriggerProps,
type BibleChapterPickerContentProps,
type BibleChapterPickerPressData,
Expand Down
Loading