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
11 changes: 11 additions & 0 deletions .changeset/breezy-tables-jam.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@youversion/platform-react-ui': minor
'@youversion/platform-core': minor
'@youversion/platform-react-hooks': minor
---

feat(ui): add bible reader settings

- refactor popover component to have consistent styling across
multiple components and reduce duplication in code.
- add bible reader settings and save the users settings to localStorage.
29 changes: 29 additions & 0 deletions packages/core/src/__tests__/polyfills.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
class LocalStorageMock {
private store: Record<string, string> = {};

getItem(key: string): string | null {
return this.store[key] ?? null;
}

setItem(key: string, value: string): void {
this.store[key] = value;
}

removeItem(key: string): void {
delete this.store[key];
}

clear(): void {
this.store = {};
}

get length(): number {
return Object.keys(this.store).length;
}

key(index: number): string | null {
return Object.keys(this.store)[index] ?? null;
}
}

globalThis.localStorage = new LocalStorageMock() as Storage;
2 changes: 1 addition & 1 deletion packages/core/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
environment: 'node',
setupFiles: ['./src/__tests__/setup.ts'],
setupFiles: ['./src/__tests__/polyfills.ts', './src/__tests__/setup.ts'],
testTimeout: 10_000,
coverage: {
provider: 'v8',
Expand Down
7 changes: 6 additions & 1 deletion packages/ui/.storybook/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import { fileURLToPath } from 'url';

const config: StorybookConfig = {
stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
addons: ['@storybook/addon-docs', '@storybook/addon-onboarding', '@storybook/addon-vitest'],
addons: [
'@storybook/addon-docs',
'@storybook/addon-onboarding',
'@storybook/addon-vitest',
{ name: '@storybook/addon-coverage', options: { istanbul: { include: ['**/stories/**'] } } },
],
framework: '@storybook/react-vite',
staticDirs: ['../public'], // This is for Storybook mock service worker
viteFinal: (config) => {
Expand Down
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@
"devDependencies": {
"@internal/eslint-config": "workspace:*",
"@internal/tsconfig": "workspace:*",
"@storybook/addon-coverage": "^3.0.0",
"@storybook/addon-docs": "10.0.0",
"@storybook/addon-onboarding": "10.0.0",
"@storybook/addon-vitest": "10.0.0",
Expand Down
23 changes: 2 additions & 21 deletions packages/ui/src/components/bible-chapter-picker.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
import { useControllableState } from '@radix-ui/react-use-controllable-state';
import { useBooks, useTheme } from '@youversion/platform-react-hooks';
import { type BibleBook } from '@youversion/platform-core';
import { Info, Search, XIcon } from 'lucide-react';
import { Info, Search } from 'lucide-react';
import { Button } from './ui/button';
import { Popover, PopoverTrigger, PopoverContent, PopoverClose } from './ui/popover';
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from './ui/accordion';
Expand Down Expand Up @@ -137,26 +137,7 @@ function Root({
{children}

{/* data-yv-sdk for styles is needed because the popover gets rendered outside of the providers scope **/}
<PopoverContent
data-yv-sdk
data-yv-theme={theme}
side="top"
className="yv:grid yv:grid-rows-[auto_1fr_auto] yv:bg-background yv:p-0 yv:h-full yv:max-h-[66vh] yv:w-96 yv:sm:w-sm yv:overflow-hidden yv:rounded-2xl yv:border-0 yv:shadow-lg"
>
<section className="yv:bg-muted yv:py-3 yv:w-full yv:rounded-t-2xl yv:px-4 yv:border-b yv:border-border yv:flex yv:flex-row yv:justify-between">
<h2 className="yv:font-bold yv:text-base">Books</h2>
<PopoverClose asChild>
<Button
variant="ghost"
size="icon"
className="yv:w-6 yv:h-6 yv:text-muted-foreground"
>
<XIcon />
<span className="yv:sr-only">Close</span>
</Button>
</PopoverClose>
</section>

<PopoverContent heading="Books" theme={theme} side="top">
<Accordion
className="yv:relative yv:overflow-y-auto yv:bg-background yv:px-6"
type="single"
Expand Down
104 changes: 102 additions & 2 deletions packages/ui/src/components/bible-reader.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { expect, screen, userEvent, waitFor } from 'storybook/test';
import { BibleReader } from './bible-reader';

const meta: Meta<typeof BibleReader.Root> = {
Expand All @@ -7,13 +8,16 @@ const meta: Meta<typeof BibleReader.Root> = {
parameters: {
layout: 'fullscreen',
},
beforeEach: () => {
localStorage.clear();
},
argTypes: {
versionId: {
control: 'number',
description: 'The Bible version ID to display',
},
fontSize: {
control: { type: 'range', min: 14, max: 24, step: 1 },
control: { type: 'range', min: 8, max: 24, step: 1 },
description: 'Font size in pixels',
},
lineHeight: {
Expand Down Expand Up @@ -45,9 +49,9 @@ type Story = StoryObj<typeof BibleReader.Root>;
* Default uncontrolled story: Component manages its own state with John 1 NIV as default
*/
export const Default: Story = {
tags: ['integration'],
args: {
versionId: 111,
fontSize: 16,
lineHeight: 1.6,
fontFamily: "'Inter', sans-serif",
showVerseNumbers: true,
Expand All @@ -61,6 +65,59 @@ export const Default: Story = {
</BibleReader.Root>
</div>
),
play: async ({ canvasElement }) => {
await waitFor(
async () => {
const verseContainer = canvasElement.querySelector('[data-slot="yv-bible-renderer"]');
await expect(verseContainer).toBeInTheDocument();
},
{ timeout: 5000 },
);

const verseContainer = canvasElement.querySelector('[data-slot="yv-bible-renderer"]');
await expect(verseContainer).toBeInTheDocument();

const themeContainer = canvasElement.querySelector('[data-yv-theme="light"]');
await expect(themeContainer).toBeInTheDocument();

const settingsButton = screen.getByRole('button', { name: /settings/i });
await userEvent.click(settingsButton);

await waitFor(async () => {
await expect(await screen.findByText('Reader Settings')).toBeInTheDocument();
});

const fontButtons = screen.getAllByRole('button', { name: /font/i });
await expect(fontButtons.length).toBe(2);

const decreaseFontButton = screen.getByTestId('decrease-font-size');
const increaseFontButton = screen.getByTestId('increase-font-size');

await userEvent.click(increaseFontButton);
await expect(localStorage.getItem('youversion-platform:reader:font-size')).toBe('18');
await userEvent.click(increaseFontButton);
await userEvent.click(increaseFontButton);
await expect(localStorage.getItem('youversion-platform:reader:font-size')).toBe('20');

await userEvent.click(decreaseFontButton);
await userEvent.click(decreaseFontButton);
await expect(localStorage.getItem('youversion-platform:reader:font-size')).toBe('16');
await userEvent.click(decreaseFontButton);
await userEvent.click(decreaseFontButton);
await userEvent.click(decreaseFontButton);
await expect(localStorage.getItem('youversion-platform:reader:font-size')).toBe('12');

const interButton = screen.getByRole('button', { name: /inter/i });
const sourceSerifButton = screen.getByRole('button', { name: /source serif/i });

await userEvent.click(sourceSerifButton);
await expect(localStorage.getItem('youversion-platform:reader:font-family')).toBe(
'Source Serif',
);

await userEvent.click(interButton);
await expect(localStorage.getItem('youversion-platform:reader:font-family')).toBe('Inter');
},
};

/**
Expand Down Expand Up @@ -89,6 +146,7 @@ export const DarkTheme: Story = {
* Custom styling with larger font and increased line height
*/
export const CustomStyling: Story = {
tags: ['integration'],
args: {
versionId: 111,
fontSize: 18,
Expand All @@ -105,6 +163,48 @@ export const CustomStyling: Story = {
</BibleReader.Root>
</div>
),
play: async ({ canvasElement }) => {
await waitFor(
async () => {
const verseContainer = canvasElement.querySelector('[data-slot="yv-bible-renderer"]');
await expect(verseContainer).toBeInTheDocument();
},
{ timeout: 5000 },
);

await expect(localStorage.getItem('youversion-platform:reader:font-size')).toBe('18');
},
};

export const FontSizeOutOfRange: Story = {
tags: ['integration'],
args: {
versionId: 111,
fontSize: 28,
lineHeight: 2.0,
fontFamily: "'Nunito Sans', sans-serif",
showVerseNumbers: false,
background: 'light',
},
render: (args) => (
<div className="yv:h-screen yv:bg-background">
<BibleReader.Root {...args}>
<BibleReader.Toolbar border="bottom" />
<BibleReader.Content />
</BibleReader.Root>
</div>
),
play: async ({ canvasElement }) => {
await waitFor(
async () => {
const verseContainer = canvasElement.querySelector('[data-slot="yv-bible-renderer"]');
await expect(verseContainer).toBeInTheDocument();
},
{ timeout: 5000 },
);

await expect(localStorage.getItem('youversion-platform:reader:font-size')).toBe('16');
},
};

export const RealAPI: Story = {
Expand Down
Loading