Skip to content

Commit d0d596c

Browse files
authored
YPE-1002 - Fix the UI overflow of the Bible abbreviation inside of the square box (#87)
* chore: add missing environment variable to .env.example * fix(bible-version-picker): ensure Bible version abbreviation text fits within box boundaries * refactor(VersionAbbreviationIcon): adjust text scaling to prevent overflow Adjust text scaling to account for container height, preventing overflow. Also, ensure text does not wrap by adding `yv:whitespace-nowrap` class. * Fix: Remove unnecessary ResizeObserver call  * fix: ensure initial sizing in bible-version-picker * refactor(VersionAbbreviationIcon): simplify resizing Removes unused digitsRef and digitsSize state. The font size for digits is now derived from prefixSize, ensuring consistent scaling. * chore: add changeset file * chore: update changeset to patch instead of minor
1 parent ab93aa6 commit d0d596c

File tree

3 files changed

+115
-23
lines changed

3 files changed

+115
-23
lines changed

.changeset/fifty-buttons-teach.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@youversion/platform-react-ui': patch
3+
'@youversion/platform-core': patch
4+
'@youversion/platform-react-hooks': patch
5+
---
6+
7+
feat(ui): update Bible version picker to fit container bounds

packages/ui/.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1-
# If you supply an environment variable prefixed with STORYBOOK_,
1+
# If you supply an environment variable prefixed with STORYBOOK_,
22
# it will be available in import.meta.env when using the Vite builder.
33
STORYBOOK_YOUVERSION_APP_KEY=""
44

55
# API host - defaults to production (api.youversion.com)
66
STORYBOOK_YOUVERSION_API_HOST=api.youversion.com
7+
8+
STORYBOOK_AUTH_REDIRECT_URL=http://localhost:6006

packages/ui/src/components/bible-version-picker.tsx

Lines changed: 105 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,109 @@
1-
import { createContext, useContext, useState, useMemo, useRef, type ReactNode } from 'react';
21
import { useControllableState } from '@radix-ui/react-use-controllable-state';
2+
import type { BibleVersion } from '@youversion/platform-core';
33
import {
44
useFilteredVersions,
5-
useVersion,
6-
useVersions,
75
useLanguages,
86
useTheme,
7+
useVersion,
8+
useVersions,
99
} from '@youversion/platform-react-hooks';
10+
import { ArrowLeft, Globe, Search } from 'lucide-react';
11+
import {
12+
createContext,
13+
type ReactNode,
14+
useContext,
15+
useEffect,
16+
useMemo,
17+
useRef,
18+
useState,
19+
} from 'react';
20+
import { cn } from '@/lib/utils';
21+
import { Badge } from './ui/badge';
1022
import { Button } from './ui/button';
1123
import { Input } from './ui/input';
12-
import { Popover, PopoverTrigger, PopoverContent } from './ui/popover';
13-
import { Item, ItemGroup, ItemMedia, ItemContent, ItemTitle, ItemDescription } from './ui/item';
14-
import type { BibleVersion } from '@youversion/platform-core';
15-
import { Search, Globe, ArrowLeft } from 'lucide-react';
16-
import { Badge } from './ui/badge';
17-
import { cn } from '@/lib/utils';
24+
import { Item, ItemContent, ItemDescription, ItemGroup, ItemMedia, ItemTitle } from './ui/item';
25+
import { Popover, PopoverContent, PopoverTrigger } from './ui/popover';
26+
27+
// Displays a version abbreviation (e.g., "NIV", "KJV2") centered within a fixed-size icon.
28+
// Dynamically scales the font size to fit the text within the container with padding.
29+
function VersionAbbreviationIcon({ text }: { text: string }) {
30+
const containerRef = useRef<HTMLDivElement>(null);
31+
const prefixRef = useRef<HTMLDivElement>(null);
32+
const [prefixSize, setPrefixSize] = useState(20);
33+
34+
// Split abbreviation into letters and numbers (e.g., "KJV2" → "KJV", "2")
35+
const match = /^(.+?)(\d+)$/.exec(text) || [];
36+
const prefix = match[1] || text;
37+
const digits = match[2];
38+
39+
useEffect(() => {
40+
const container = containerRef.current;
41+
const prefixElement = prefixRef.current;
42+
if (!container || !prefixElement) return;
43+
44+
// Calculate the maximum font size that fits the text within container bounds
45+
const calculateSize = (element: HTMLElement | null) => {
46+
if (!element) return 20;
47+
48+
const containerWidth = container.offsetWidth;
49+
const containerHeight = container.offsetHeight;
50+
// Target 70% of width for horizontal padding, max 40% height for vertical spacing
51+
const targetWidth = containerWidth * 0.7;
52+
const maxHeight = containerHeight * 0.4;
53+
54+
let currentSize = 20;
55+
let ratio = 1;
56+
57+
// Iteratively converge on the optimal size (5 iterations sufficient for convergence)
58+
for (let i = 0; i < 5; i++) {
59+
element.style.fontSize = `${currentSize}px`;
60+
const currentWidth = element.scrollWidth;
61+
const currentHeight = element.offsetHeight;
62+
63+
if (currentWidth > 0) {
64+
const widthRatio = targetWidth / currentWidth;
65+
const heightRatio = maxHeight / currentHeight;
66+
// Use the more restrictive constraint (width or height)
67+
ratio = Math.min(widthRatio, heightRatio);
68+
currentSize = currentSize * ratio;
69+
}
70+
}
71+
72+
// Ensure minimum readable size of 12px
73+
return Math.max(12, currentSize);
74+
};
75+
76+
const updateSizes = () => {
77+
const newPrefixSize = calculateSize(prefixElement);
78+
setPrefixSize(newPrefixSize);
79+
};
80+
81+
// Recalculate when container size changes (e.g., window resize, theme switch)
82+
const resizeObserver = new ResizeObserver(updateSizes);
83+
resizeObserver.observe(container);
84+
updateSizes();
85+
86+
return () => {
87+
resizeObserver.disconnect();
88+
};
89+
}, []);
90+
91+
return (
92+
<div
93+
ref={containerRef}
94+
className="yv:flex yv:flex-col yv:w-full yv:h-full yv:px-2 yv:font-serif yv:leading-none yv:font-bold yv:items-center yv:justify-center"
95+
>
96+
<div ref={prefixRef} className="yv:whitespace-nowrap" style={{ fontSize: `${prefixSize}px` }}>
97+
{prefix}
98+
</div>
99+
{digits && (
100+
<div className="yv:whitespace-nowrap" style={{ fontSize: `${prefixSize}px` }}>
101+
{digits}
102+
</div>
103+
)}
104+
</div>
105+
);
106+
}
18107

19108
type LanguageOption = {
20109
id: string;
@@ -255,19 +344,9 @@ function Content() {
255344
>
256345
<ItemMedia
257346
variant="icon"
258-
className="yv:rounded-[8px] yv:size-12 yv:border-border yv:p-1 yv:flex yv:flex-col yv:justify-center"
347+
className="yv:rounded-[8px] yv:size-12 yv:border-border yv:flex yv:flex-col yv:justify-center yv:items-center"
259348
>
260-
{(() => {
261-
const match = /^(.+?)(\d+)$/.exec(version.localized_abbreviation) || [];
262-
const prefix = match[1] || version.localized_abbreviation;
263-
const digits = match[2];
264-
return (
265-
<div className="yv:font-serif yv:text-sm yv:leading-none yv:font-bold yv:text-center">
266-
<div>{prefix}</div>
267-
{digits && <div>{digits}</div>}
268-
</div>
269-
);
270-
})()}
349+
<VersionAbbreviationIcon text={version.localized_abbreviation} />
271350
</ItemMedia>
272351
<ItemContent>
273352
<ItemTitle className="yv:line-clamp-2 yv:text-left">
@@ -334,7 +413,11 @@ function Content() {
334413
aria-label={language.englishName}
335414
asChild
336415
>
337-
<button className="yv:w-full" onClick={() => handleSelectLanguage(language.id)}>
416+
<button
417+
className="yv:w-full"
418+
onClick={() => handleSelectLanguage(language.id)}
419+
type="button"
420+
>
338421
<ItemContent>
339422
<ItemTitle className="yv:line-clamp-2">{language.englishName}</ItemTitle>
340423
</ItemContent>

0 commit comments

Comments
 (0)