fix: give settings switches, jump chips, selects and search results a 44px touch target on the phone

This commit is contained in:
Linus Rath
2026-10-28 04:58:07 +01:00
parent 6dbaa0fd30
commit 7cb7aaff52
3 changed files with 44 additions and 8 deletions
@@ -0,0 +1,31 @@
import { render, screen } from '@testing-library/react';
import { describe, expect, it } from 'vitest';
import { SectionNav, Select, SettingItem, SettingsSection, ToggleSwitch } from '../settings-section';
// SET-22: on the phone, settings switches (44x24), jump chips (28px) and
// selects (29px) were smaller than the 44px a finger needs (R1).
const HIT = 'pointer-coarse:after:min-h-11';
describe('settings touch targets', () => {
it('switches keep their size and get a 44px hit area', () => {
render(<SettingItem label="Sync"><ToggleSwitch checked onChange={() => {}} /></SettingItem>);
expect(screen.getByRole('switch').className).toContain(HIT);
});
it('jump chips get a 44px hit area', () => {
render(
<div>
<SectionNav />
<SettingsSection id="a" title="One"><span /></SettingsSection>
<SettingsSection id="b" title="Two"><span /></SettingsSection>
</div>
);
expect(screen.getByRole('button', { name: 'One' }).className).toContain(HIT);
});
it('selects are 44px tall on a touch screen', () => {
render(<Select value="a" onChange={() => {}} ariaLabel="Pick" options={[{ value: 'a', label: 'A' }]} />);
expect(screen.getByRole('combobox').className).toContain('pointer-coarse:min-h-11');
});
});
+3 -3
View File
@@ -971,9 +971,9 @@ export function SettingsApp({ linkSegments: routeSegments }: SettingsAppProps =
<button
key={`${tab.id}:${sub.label}`}
onClick={() => handleSubResultSelect(tab.id, sub)}
className="w-full flex items-center ps-12 pe-5 py-2 text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors duration-150 text-start"
className="w-full flex items-center ps-12 pe-5 py-2 pointer-coarse:min-h-11 text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors duration-150 text-start"
>
<span className="truncate">{sub.label}</span>
<span className="truncate" title={sub.label}>{sub.label}</span>
</button>
))}
</div>
@@ -1138,7 +1138,7 @@ export function SettingsApp({ linkSegments: routeSegments }: SettingsAppProps =
onClick={() => handleSubResultSelect(tab.id, sub)}
className="w-full text-start ps-9 pe-3 py-1.5 rounded-md text-xs text-muted-foreground hover:bg-muted hover:text-foreground transition-colors duration-150"
>
<span className="truncate block">{sub.label}</span>
<span className="truncate block" title={sub.label}>{sub.label}</span>
</button>
))}
</div>
+10 -5
View File
@@ -4,6 +4,7 @@ import { ReactNode, createContext, useContext, useEffect, useId, useState, useCa
import { useTranslations } from 'next-intl';
import { Lock } from '@/components/icons';
import { cn } from '@/lib/utils';
import { Button, TOUCH_HIT_AREA } from '@/components/ui/button';
import { useSettingsStore } from '@/stores/settings-store';
/**
@@ -134,14 +135,16 @@ export function SectionNav() {
return (
<div ref={ref} hidden={sections.length < 2} data-testid="settings-section-nav" className="flex flex-wrap gap-1.5 mb-6">
{sections.map((section) => (
<button
<Button
key={section.id}
type="button"
variant="ghost"
size="xs"
onClick={() => { if (ref.current?.parentElement) scrollToSettingsSection(section.id, ref.current.parentElement); }}
className="px-3 py-1.5 text-xs rounded-md bg-muted hover:bg-accent hover:text-accent-foreground text-foreground transition-colors duration-150"
className="px-3 text-xs font-normal bg-muted text-foreground"
>
{section.title}
</button>
</Button>
))}
</div>
);
@@ -257,7 +260,9 @@ export function ToggleSwitch({ checked, onChange, disabled, ariaLabel, testId }:
disabled={disabled}
onClick={() => onChange(!checked)}
className={cn(
'relative inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors duration-150',
'inline-flex h-6 w-11 shrink-0 items-center rounded-full transition-colors duration-150',
// 44px to hit on a touch screen, drawn the same size (R1).
TOUCH_HIT_AREA,
// Off: an outlined track and a grey knob, 3:1 against the page in
// both schemes (a light track with a white knob was ~1.1:1). The
// ring is inset, so the on state keeps its size and look.
@@ -325,7 +330,7 @@ export function Select({ value, onChange, options, disabled, className, ariaLabe
aria-labelledby={ariaLabel ? undefined : labelledBy}
dir="auto"
className={cn(
"px-3 py-1.5 text-sm rounded-md bg-muted border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-colors duration-150",
"px-3 py-1.5 text-sm rounded-md bg-muted border border-border text-foreground focus:outline-none focus:ring-2 focus:ring-ring transition-colors duration-150 pointer-coarse:min-h-11",
disabled ? "opacity-60 cursor-not-allowed" : "cursor-pointer hover:border-muted-foreground",
className
)}