fix: give menu rows, the phone More panel's close and the subject star a 44px touch target

This commit is contained in:
Linus Rath
2026-10-28 07:00:24 +01:00
parent 0758cc487c
commit e2908676ec
6 changed files with 53 additions and 14 deletions
@@ -200,3 +200,14 @@ describe('phone More panel parity', () => {
expect(within(menu).queryByRole('menuitem', { name: /keyboard_shortcuts/ })).toBeNull();
});
});
describe('phone touch targets (MAIL-18)', () => {
it('draws the panel close at 44px and gives the subject star the touch hit area', () => {
renderViewer({ onToggleStar: vi.fn() });
const close = screen.getByTestId('more-panel-close');
expect(close.className).toContain('h-11');
expect(close.className).toContain('w-11');
const star = screen.getByTestId('subject-star');
expect(star).toHaveAccessibleName('tooltips.star');
expect(star.className).toContain('pointer-coarse:after:min-h-11');
});
});
+12 -10
View File
@@ -4562,16 +4562,16 @@ export function EmailViewer({
) : (
<span className="text-sm font-semibold text-foreground">{t('more_actions')}</span>
)}
<Button
variant="ghost"
size="icon"
<IconButton
size="icon-lg"
role="menuitem"
aria-label={tCommon('close')}
label={tCommon('close')}
data-testid="more-panel-close"
onClick={() => { setMoreMenuOpen(false); setMoreMenuSub(null); }}
className="h-9 w-9"
className="-me-2"
>
<X className="w-5 h-5" />
</Button>
</IconButton>
</div>
<div ref={mobileSubListRef} className="flex-1 overflow-y-auto py-2">
{moreMenuSub === null && (
@@ -4808,16 +4808,18 @@ export function EmailViewer({
{/* Star inline with subject (top toolbar mode). Mountain View
has it on the sender's line instead. */}
{toolbarPosition === 'top' && !mvViewer && (
<button
<IconButton
onClick={onToggleStar}
className="flex-shrink-0 p-1 rounded hover:bg-muted transition-colors"
title={isStarred ? t('tooltips.unstar') : t('tooltips.star')}
label={isStarred ? t('tooltips.unstar') : t('tooltips.star')}
aria-pressed={!!isStarred}
data-testid="subject-star"
className="flex-shrink-0 -my-1"
>
<Star className={cn(
"w-4 h-4 lg:w-5 lg:h-5 transition-colors",
isStarred ? "fill-amber-400 text-amber-400" : "text-muted-foreground/40"
)} />
</button>
</IconButton>
)}
{isImportant && (
<span className="px-1.5 lg:px-2 py-0.5 bg-warning/15 text-warning rounded-full text-xs font-medium whitespace-nowrap flex-shrink-0 self-center">
+1 -1
View File
@@ -44,7 +44,7 @@ export function MoveTargetList({ tree, targetIds, onPick, variant = "menu" }: Mo
const label = localizeMailboxName(node.role, node.name, (key) => tSidebar(`mailboxes.${key}`));
const rowClass = panel
? "w-full pe-4 py-2.5 min-h-[44px] text-sm text-start flex items-center gap-3"
: "w-full pe-3 py-1.5 text-sm text-start flex items-center gap-2";
: "w-full pe-3 py-1.5 text-sm text-start flex items-center gap-2 pointer-coarse:min-h-11";
const icon = <Icon className={cn(panel ? "w-5 h-5" : "w-4 h-4", "flex-shrink-0")} />;
return (
<div key={node.id}>
+1 -1
View File
@@ -101,7 +101,7 @@ export function TagPicker({
const rowClass = cn(
"w-full text-start flex items-center gap-2 hover:bg-muted cursor-pointer",
touch ? "px-4 py-2.5 min-h-[44px] text-sm gap-3" : "px-3 py-1.5 text-sm",
touch ? "px-4 py-2.5 min-h-[44px] text-sm gap-3" : "px-3 py-1.5 text-sm pointer-coarse:min-h-11",
);
const dotClass = touch ? "w-3.5 h-3.5" : "w-3 h-3";
const checkClass = touch ? "w-4 h-4" : "w-3.5 h-3.5";
@@ -0,0 +1,25 @@
import { render, screen } from '@testing-library/react';
import { describe, it, expect } from 'vitest';
import { ContextMenu, ContextMenuSubMenu, ContextMenuItem } from '../context-menu';
// jsdom can't match `pointer: coarse`, so this checks the rule is on the rows:
// 44px on a touch screen (R7), the desktop's own height elsewhere.
describe('ContextMenu rows on a touch screen', () => {
it('gives items and submenu entries a 44px minimum height under a coarse pointer only', () => {
render(
<ContextMenu isOpen position={{ x: 10, y: 10 }} onClose={() => {}}>
<ContextMenuItem label="Reply" onClick={() => {}} />
<ContextMenuSubMenu label="Move to" testId="sub">
<ContextMenuItem label="Archive" onClick={() => {}} />
</ContextMenuSubMenu>
</ContextMenu>,
);
const item = screen.getByRole('menuitem', { name: 'Reply' });
const sub = screen.getByTestId('sub');
for (const row of [item, sub]) {
expect(row.className).toContain('pointer-coarse:min-h-11');
expect(row.className).toContain('py-1.5');
expect(row.className).not.toMatch(/(^|\s)min-h-11/);
}
});
});
+3 -2
View File
@@ -512,7 +512,8 @@ export function ContextMenuItem({
disabled={disabled}
title={title}
className={cn(
"w-full px-3 py-1.5 text-sm text-start flex items-center gap-2",
// 44px rows on a touch screen (R7); the desktop keeps its density.
"w-full px-3 py-1.5 text-sm text-start flex items-center gap-2 pointer-coarse:min-h-11",
"transition-colors duration-150",
"focus:outline-none focus-visible:bg-muted",
disabled && "opacity-50 cursor-not-allowed",
@@ -637,7 +638,7 @@ export function ContextMenuSubMenu({
ref={triggerRef}
data-submenu-trigger=""
className={cn(
"w-full px-3 py-1.5 text-sm flex items-center gap-2 outline-none",
"w-full px-3 py-1.5 text-sm flex items-center gap-2 outline-none pointer-coarse:min-h-11",
"transition-colors duration-150",
disabled ? "opacity-50 cursor-not-allowed" : "cursor-pointer hover:bg-muted focus-visible:bg-muted",
isOpen && !disabled && "bg-muted"