mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 00:14:23 +00:00
fix: give settings switches, jump chips, selects and search results a 44px touch target on the phone
This commit is contained in:
@@ -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');
|
||||
});
|
||||
});
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user