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
Expand Up @@ -105,7 +105,16 @@ describe('account surface states', () => {
const { renderer, unmount } = await renderWithProviders(createElement(DeviceSessionsScreen));
expect(renderer.root.findAll(node => String(node.type) === 'QueryError')).toHaveLength(0);
expect(renderer.root.findAll(node => String(node.type) === 'EmptyState')).toHaveLength(0);
expect(renderer.root.findAll(node => String(node.type) === 'Pressable')).toHaveLength(1);
const signOut = renderer.root.findAll(node => String(node.type) === 'Pressable');
expect(signOut).toHaveLength(1);
// The native rem is 14pt, so `min-h-11`/`min-w-11` is only 38.5pt — below
// the app's 44pt minimum. The px form (what button.tsx and the image viewer
// use) makes the visible box itself 44pt; hitSlop 8 keeps the effective
// area larger still. The loading skeleton reserves this same box (asserted
// below), so the taller control does not jump the list when rows arrive.
expect(signOut[0]?.props.className).toContain('min-h-[44px]');
expect(signOut[0]?.props.className).toContain('min-w-[44px]');
expect(signOut[0]?.props.hitSlop).toBe(8);
unmount();
});

Expand All @@ -114,6 +123,15 @@ describe('account surface states', () => {
query.isError = true;
const { renderer, unmount } = await renderWithProviders(createElement(DeviceSessionsScreen));
expect(renderer.root.findAll(node => String(node.type) === 'Skeleton')).toHaveLength(12);
// Every skeleton row reserves the sign-out control's final 44x44pt box, so
// the loaded rows are the same height and the list does not jump on load.
const reserved = renderer.root.findAll(
node =>
String(node.type) === 'View' &&
String(node.props.className).includes('min-h-[44px]') &&
String(node.props.className).includes('min-w-[44px]')
);
expect(reserved).toHaveLength(3);
expect(renderer.root.findAll(node => String(node.type) === 'QueryError')).toHaveLength(0);
expect(renderer.root.findAll(node => String(node.type) === 'EmptyState')).toHaveLength(0);
unmount();
Expand Down
9 changes: 7 additions & 2 deletions apps/mobile/src/components/device-sessions-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ function SessionRow({ session, disabled, onPress }: SessionRowProps) {
? t('deviceSessions.signOutThisDevice')
: t('deviceSessions.signOutThisSession')
}
className="shrink-0 active:opacity-70"
className="min-h-[44px] min-w-[44px] shrink-0 items-center justify-center active:opacity-70"
>
<LogOut size={16} color={colors.destructive} />
</Pressable>
Expand Down Expand Up @@ -187,7 +187,12 @@ export function DeviceSessionsScreen() {
<Skeleton className="h-5 w-28" />
<Skeleton className="h-4 w-48" />
</View>
<Skeleton className="h-4 w-4 rounded" />
{/* Reserve the sign-out control's final 44x44pt box so the
loaded row is the same height as its skeleton and the list
does not jump when sessions arrive. The glyph stays 16pt. */}
<View className="min-h-[44px] min-w-[44px] items-center justify-center">
<Skeleton className="h-4 w-4 rounded" />
</View>
</View>
))}
</View>
Expand Down
17 changes: 17 additions & 0 deletions apps/mobile/src/components/image-viewer-modal.mounted.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,23 @@ describe('ImageViewerModal mounted', () => {
Object.assign(safeArea, { top: 0, bottom: 0, left: 0, right: 0 });
});

it('gives the close and share controls a 44pt minimum touch target', async () => {
const renderer = await mountViewer({ onShare: () => undefined });

// Both icon controls must meet the app's 44pt minimum, not the old 40pt
// box. Native rem is 14pt, so `min-h-11` is only 38.5pt; the px form is
// what button.tsx uses for its icon size (`h-[44px] w-[44px]`) and these
// controls have no hitSlop to close the gap.
const close = pressableByLabel(renderer.root, 'Close photo.png');
const share = pressableByLabel(renderer.root, 'Share photo.png');
expect(close?.props.className).toContain('min-h-[44px]');
expect(close?.props.className).toContain('min-w-[44px]');
expect(share?.props.className).toContain('min-h-[44px]');
expect(share?.props.className).toContain('min-w-[44px]');

renderer.unmount();
});

it('shows the Image unavailable fallback and keeps Share enabled on decode failure', async () => {
const onShare = vi.fn<() => void>();
const renderer = await mountViewer({ onShare });
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/src/components/image-viewer-modal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export function ImageViewerModal({
>
<Pressable
onPress={onClose}
className="h-10 w-10 items-center justify-center rounded-md bg-secondary active:opacity-70"
className="min-h-[44px] min-w-[44px] shrink-0 items-center justify-center rounded-md bg-secondary active:opacity-70"
accessibilityRole="button"
accessibilityLabel={t('imageViewer.close', { filename })}
>
Expand All @@ -190,7 +190,7 @@ export function ImageViewerModal({
onPress={onShare}
disabled={sharing || uri === null}
accessibilityState={{ disabled: uri === null, busy: sharing }}
className="h-10 w-10 items-center justify-center rounded-md bg-secondary active:opacity-70 disabled:opacity-50"
className="min-h-[44px] min-w-[44px] shrink-0 items-center justify-center rounded-md bg-secondary active:opacity-70 disabled:opacity-50"
accessibilityRole="button"
accessibilityLabel={t('imageViewer.share', { filename })}
>
Expand Down