Skip to content
Open
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
138 changes: 82 additions & 56 deletions apps/mobile/src/components/home/section-header.mounted.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,65 +36,91 @@ afterEach(() => {

describe('SectionHeader mounted layout', () => {
// Host props protect the layout contract; only native I4 can prove scaled glyph rendering.
it.each([
{ isRTL: false, alignment: 'text-right' },
{ isRTL: true, alignment: 'text-left' },
])('gives both labels spare width and wrapping with RTL=$isRTL', ({ isRTL, alignment }) => {
i18nManager.isRTL = isRTL;
const root = mount(
createElement(SectionHeader, {
label: 'Live now',
actionLabel: 'See all',
onActionPress: () => undefined,
})
);
const action = root.findByProps({ accessibilityRole: 'button' });
const text = action.find(node => Object.is(node.type, 'Text'));
const label = root.find(
node => Object.is(node.type, 'Text') && node.children.includes('Live now')
);
it.each([{ isRTL: false }, { isRTL: true }])(
'gives both labels spare width and wrapping with RTL=$isRTL',
({ isRTL }) => {
i18nManager.isRTL = isRTL;
const root = mount(
createElement(SectionHeader, {
label: 'Live now',
actionLabel: 'See all',
onActionPress: () => undefined,
})
);
const action = root.findByProps({ accessibilityRole: 'button' });
const text = action.find(node => Object.is(node.type, 'Text'));
const label = root.find(
node => Object.is(node.type, 'Text') && node.children.includes('Live now')
);

expect((label.props.className as string).split(' ')).toEqual(
expect.arrayContaining([
'grow',
'max-w-full',
'font-mono-medium',
'text-[10px]',
'tracking-[1.5px]',
'uppercase',
'text-muted-foreground',
])
);
expect(label.props.numberOfLines).toBeUndefined();
expect(label.props.allowFontScaling).not.toBe(false);
expect(label.props.maxFontSizeMultiplier).toBeUndefined();
expect(label.props.adjustsFontSizeToFit).not.toBe(true);
expect(label.children).toEqual(['Live now']);
if (isRTL) {
expect(label.props.style).toContainEqual({ writingDirection: 'rtl' });
}

expect((label.props.className as string).split(' ')).toEqual(
expect.arrayContaining([
'grow',
'max-w-full',
'font-mono-medium',
'text-[10px]',
'tracking-[1.5px]',
'uppercase',
'text-muted-foreground',
])
);
expect(label.props.numberOfLines).toBeUndefined();
expect(label.props.allowFontScaling).not.toBe(false);
expect(label.props.maxFontSizeMultiplier).toBeUndefined();
expect(label.props.adjustsFontSizeToFit).not.toBe(true);
expect(label.children).toEqual(['Live now']);
if (isRTL) {
expect(label.props.style).toContainEqual({ writingDirection: 'rtl' });
expect((action.parent?.props.className as string | undefined)?.split(' ')).toContain(
'flex-wrap'
);
// The action copy must sit at the row's end in both directions, so the
// box is a row that places its content at the main-axis end. The layout
// is direction-relative and identical under RTL.
expect((action.props.className as string).split(' ')).toEqual(
expect.arrayContaining(['grow', 'max-w-full', 'flex-row', 'justify-end'])
);
expect((text.props.className as string).split(' ')).toEqual(
expect.arrayContaining([
'font-mono-medium',
'text-[11px]',
'tracking-[1.5px]',
'uppercase',
'text-primary',
])
);
expect(text.props.numberOfLines).toBeUndefined();
expect(text.props.allowFontScaling).not.toBe(false);
expect(text.props.maxFontSizeMultiplier).toBeUndefined();
expect(text.children).toEqual(['See all']);
}
);

expect((action.parent?.props.className as string | undefined)?.split(' ')).toContain(
'flex-wrap'
);
expect((action.props.className as string).split(' ')).toEqual(
expect.arrayContaining(['grow', 'max-w-full'])
);
expect((text.props.className as string).split(' ')).toEqual(
expect.arrayContaining([
alignment,
'font-mono-medium',
'text-[11px]',
'tracking-[1.5px]',
'uppercase',
'text-primary',
])
);
expect(text.props.numberOfLines).toBeUndefined();
expect(text.props.allowFontScaling).not.toBe(false);
expect(text.props.maxFontSizeMultiplier).toBeUndefined();
expect(text.children).toEqual(['See all']);
});
it.each([{ isRTL: false }, { isRTL: true }])(
'aligns the action with the row edges, never with a physical text align, with RTL=$isRTL',
({ isRTL }) => {
// React Native swaps `textAlign: 'left'` and `'right'` under RTL (Android
// maps 'left' to Gravity.RIGHT), so a physical alignment would pin the
// action to the inner edge of its box and float "See all" away from the
// row end in Arabic.
i18nManager.isRTL = isRTL;
const root = mount(
createElement(SectionHeader, {
label: 'Live now',
actionLabel: 'See all',
onActionPress: () => undefined,
})
);
const classes = root
.findAll(node => typeof node.props.className === 'string')
.flatMap(node => (node.props.className as string).split(' '));

expect(classes).not.toContain('text-left');
expect(classes).not.toContain('text-right');
}
);

it('keeps the complete accessible action name and activates the supplied destination', () => {
function Destination() {
Expand Down
20 changes: 10 additions & 10 deletions apps/mobile/src/components/home/section-header.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import { I18nManager, Pressable, View } from 'react-native';
import { Pressable, View } from 'react-native';

import { Text } from '@/components/ui/text';
import { cn } from '@/lib/utils';

type SectionHeaderProps = {
label: string;
/** Optional right-aligned link (e.g. "SEE ALL"). */
/** Optional link at the end of the row (e.g. "SEE ALL"). */
actionLabel?: string;
onActionPress?: () => void;
};
Expand All @@ -22,14 +21,15 @@ export function SectionHeader({ label, actionLabel, onActionPress }: Readonly<Se
hitSlop={8}
accessibilityRole="button"
accessibilityLabel={actionLabel}
className="max-w-full grow active:opacity-70"
// The copy sits at the end of the row, so it is placed with the box's
// own direction (`justify-end` on a `flex-row`) and never with a
// physical `text-left`/`text-right`: React Native swaps those two
// under RTL (Android maps `textAlign: 'left'` to `Gravity.RIGHT` when
// the layout is RTL), which floated the action onto the inner edge of
// its box instead of the row's end in Arabic.
className="max-w-full grow flex-row justify-end active:opacity-70"
>
<Text
className={cn(
'shrink font-mono-medium text-[11px] uppercase tracking-[1.5px] text-primary',
I18nManager.isRTL ? 'text-left' : 'text-right'
)}
>
<Text className="shrink font-mono-medium text-[11px] uppercase tracking-[1.5px] text-primary">
{actionLabel}
</Text>
</Pressable>
Expand Down
Loading