mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 00:12:39 +00:00
fix: give chip remove buttons, tree expanders, task circles and month events a 24px target
This commit is contained in:
@@ -104,7 +104,8 @@ describe('CalendarMonthView tasks', () => {
|
||||
// its day's cell, below the 30px date header.
|
||||
const thu = chipSlot('thu');
|
||||
expect(dayColumn(thu)).toBe(3);
|
||||
expect(thu.style.top).toBe('52px');
|
||||
// Rows are 26px apart: 24px chips, big enough to hit (WCAG 2.5.8).
|
||||
expect(thu.style.top).toBe('56px');
|
||||
const fri = chipSlot('fri');
|
||||
expect(dayColumn(fri)).toBe(4);
|
||||
expect(fri.style.top).toBe('30px');
|
||||
@@ -118,15 +119,15 @@ describe('CalendarMonthView tasks', () => {
|
||||
onOpenDay,
|
||||
});
|
||||
const row = document.querySelector<HTMLElement>('[data-week="2026-09-07"]');
|
||||
// jsdom measures no viewport, so rows keep the 130px minimum: room for
|
||||
// four 22px rows under the 30 + 4 offset, the last taken by the link -
|
||||
// jsdom measures no viewport, so rows keep the 146px minimum: room for
|
||||
// four 26px rows under the 30 + 4 offset, the last taken by the link -
|
||||
// three entries, not one, on a short desktop window.
|
||||
expect(row?.style.minHeight).toBe('130px');
|
||||
expect(row?.style.minHeight).toBe('146px');
|
||||
expect(screen.getByText('Event e1')).toBeInTheDocument();
|
||||
expect(document.querySelector('[data-calendar-task="b"]')).not.toBeNull();
|
||||
expect(document.querySelector('[data-calendar-task="c"]')).toBeNull();
|
||||
const more = screen.getByRole('button', { name: 'events.more' });
|
||||
expect(more.parentElement?.style.top).toBe(`${30 + 66}px`);
|
||||
expect(more.parentElement?.style.top).toBe(`${30 + 78}px`);
|
||||
fireEvent.click(more);
|
||||
expect(onOpenDay).toHaveBeenCalledWith(new Date(2026, 8, 10));
|
||||
});
|
||||
|
||||
@@ -462,7 +462,7 @@ export function CalendarDayView({
|
||||
<div className="space-y-0.5">
|
||||
{/* The same chip as the month and week grids. */}
|
||||
{dayTasks.map((task) => (
|
||||
<div key={task.id} className={isMobile ? undefined : "h-5"}>
|
||||
<div key={task.id} className={isMobile ? undefined : "h-6"}>
|
||||
<CalendarTaskChip
|
||||
task={task}
|
||||
calendar={calendars.find((c) => task.calendarIds[c.id])}
|
||||
|
||||
@@ -102,7 +102,8 @@ export function CalendarMonthView({
|
||||
// into full entries via "Show time in month view" (#666).
|
||||
const showChips = !isMobile || showTimeInMonthView;
|
||||
const overlayTop = isMobile ? 34 : 30;
|
||||
const rowHeight = isMobile ? 18 : 22;
|
||||
// 24px chips with a mouse (WCAG 2.5.8); touch screens show dots.
|
||||
const rowHeight = isMobile ? 18 : 26;
|
||||
const chipHeight = rowHeight - 2;
|
||||
// A wide-screen week row fits at least three entries and the "N more"
|
||||
// link, so a short window (1280x720) does not cut every day to one.
|
||||
|
||||
@@ -225,7 +225,7 @@ export const ParticipantInput = forwardRef<ParticipantInputHandle, ParticipantIn
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemove(p.email)}
|
||||
className="flex-shrink-0 p-0.5 rounded-full hover:bg-muted-foreground/20 transition-colors min-w-[20px] min-h-[20px] flex items-center justify-center"
|
||||
className="flex-shrink-0 p-0.5 rounded-full hover:bg-muted-foreground/20 transition-colors min-w-6 min-h-6 -m-0.5 flex items-center justify-center"
|
||||
aria-label={`${t("remove")} ${p.name || p.email}`}
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
|
||||
@@ -45,7 +45,8 @@ export function CalendarTaskChip({ task, calendar, onToggleComplete, onSelect, t
|
||||
onClick={(e) => { e.stopPropagation(); onToggleComplete?.(task); }}
|
||||
className={cn(
|
||||
"group flex items-center flex-shrink-0",
|
||||
touch ? "self-stretch w-11 justify-center" : "h-full ps-1 pe-0.5",
|
||||
// 24px wide, so the circle keeps a 24px target next to the title.
|
||||
touch ? "self-stretch w-11 justify-center" : "h-full w-6 justify-center",
|
||||
)}
|
||||
aria-label={isCompleted ? t("tasks.mark_incomplete") : t("tasks.mark_complete")}
|
||||
>
|
||||
|
||||
@@ -33,6 +33,13 @@ describe('TagBadge', () => {
|
||||
expect(onRemove).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
it('gives the remove button a 24px target: a 12px icon and 6px padding all round', () => {
|
||||
render(<TagBadge tagId="work" variant="badge" onRemove={() => {}} />);
|
||||
const remove = screen.getByRole('button', { name: 'remove_tag' });
|
||||
expect(remove).toHaveClass('p-1.5');
|
||||
expect(remove.querySelector('svg')).toHaveClass('w-3', 'h-3');
|
||||
});
|
||||
|
||||
it('leaves the dot alone, having nowhere to put the control', () => {
|
||||
render(<TagBadge tagId="work" variant="dot" onRemove={() => {}} />);
|
||||
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
||||
|
||||
@@ -4603,7 +4603,7 @@ export function EmailComposer({
|
||||
)}
|
||||
<button
|
||||
onClick={() => removeAttachment(index)}
|
||||
className={cn("ms-1 hover:text-destructive min-w-[20px] min-h-[20px] flex items-center justify-center", TOUCH_HIT_AREA)}
|
||||
className={cn("ms-0.5 -my-0.5 hover:text-destructive min-w-6 min-h-6 flex items-center justify-center", TOUCH_HIT_AREA)}
|
||||
title={att.uploading ? t('upload_cancel') : t('recipient_remove')}
|
||||
aria-label={att.uploading ? t('upload_cancel') : `${t('recipient_remove')} ${att.name}`}
|
||||
>
|
||||
@@ -5722,7 +5722,7 @@ export function RecipientChipInput({
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className={cn("flex items-center justify-center w-4 h-4 rounded-full hover:bg-muted-foreground/20 transition-colors", TOUCH_HIT_AREA)}
|
||||
className={cn("flex items-center justify-center w-6 h-6 -m-1 rounded-full hover:bg-muted-foreground/20 transition-colors", TOUCH_HIT_AREA)}
|
||||
aria-label={isEditing ? t('recipient_remove') : t('recipient_remove_named', { name: recipientDisplayName(chip) })}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
@@ -108,7 +108,8 @@ export function TagBadge({
|
||||
<button
|
||||
type="button"
|
||||
onClick={onRemove}
|
||||
className="ms-0.5 shrink-0 rounded-full p-0.5 hover:bg-black/10 dark:hover:bg-white/10"
|
||||
// 24px to hit, drawn as before: the margins give the padding back.
|
||||
className="-my-1 -me-1 shrink-0 rounded-full p-1.5 hover:bg-black/10 dark:hover:bg-white/10"
|
||||
title={t("remove_tag")}
|
||||
aria-label={t("remove_tag")}
|
||||
>
|
||||
|
||||
@@ -216,7 +216,7 @@ export function FilesSidebar({
|
||||
type="button"
|
||||
onClick={() => toggleExpanded(folder.id)}
|
||||
aria-expanded={isOpen}
|
||||
className="p-0.5 -ms-1 rounded hover:bg-foreground/10 text-muted-foreground shrink-0"
|
||||
className="flex items-center justify-center size-6 -ms-[7px] -me-[3px] -my-[3px] rounded hover:bg-foreground/10 text-muted-foreground shrink-0"
|
||||
aria-label={isOpen ? t("collapse_folder", { name: folder.name }) : t("expand_folder", { name: folder.name })}
|
||||
>
|
||||
{isOpen ? <ChevronDown className="w-3.5 h-3.5" /> : <ChevronRight className="w-3.5 h-3.5" />}
|
||||
@@ -316,7 +316,7 @@ export function FilesSidebar({
|
||||
type="button"
|
||||
onClick={() => toggleSection("tree")}
|
||||
aria-expanded={!collapsed.tree}
|
||||
className="p-0.5 -ms-1 rounded hover:bg-foreground/10 text-muted-foreground shrink-0"
|
||||
className="flex items-center justify-center size-6 -ms-[7px] -me-[3px] -my-[3px] rounded hover:bg-foreground/10 text-muted-foreground shrink-0"
|
||||
aria-label={collapsed.tree ? t("expand_folder", { name: t("my_files") }) : t("collapse_folder", { name: t("my_files") })}
|
||||
>
|
||||
{collapsed.tree ? <ChevronRight className="w-3.5 h-3.5" /> : <ChevronDown className="w-3.5 h-3.5" />}
|
||||
|
||||
@@ -82,4 +82,14 @@ describe('folder list on touch', () => {
|
||||
expect(toggle.className).toContain('pointer-coarse:after:min-h-11');
|
||||
expect(toggle.getAttribute('aria-label')).toBeTruthy();
|
||||
});
|
||||
|
||||
it('gives the chevron a 24px target with a mouse too, in its 20px slot', () => {
|
||||
useSettingsStore.setState({ hideAccountSwitcher: true } as never);
|
||||
const withChild = [...mailboxes, { ...box('trips', 'Trips', null), parentId: 'travel' } as Mailbox];
|
||||
render(<Sidebar mailboxes={withChild} selectedMailbox="inbox" />);
|
||||
const toggle = row('travel').querySelector<HTMLElement>('[data-testid="folder-expand-toggle"]')!;
|
||||
expect(toggle.style.width).toBe('24px');
|
||||
expect(toggle.style.height).toBe('24px');
|
||||
expect(toggle.style.margin).toBe('-2px');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -286,7 +286,7 @@ function SidebarRowCounts({
|
||||
e.stopPropagation();
|
||||
onUnreadClick();
|
||||
}}
|
||||
className={cn(unreadClass, "cursor-pointer hover:underline rounded-sm focus-visible:outline-hidden")}
|
||||
className={cn(unreadClass, "inline-flex items-center justify-center min-w-6 min-h-6 -my-1 cursor-pointer hover:underline rounded-sm focus-visible:outline-hidden")}
|
||||
title={t('unread_filter', { count: unreadCount })}
|
||||
aria-label={t('unread_filter', { count: unreadCount })}
|
||||
>
|
||||
@@ -467,7 +467,9 @@ function SidebarRow({
|
||||
aria-label={isExpanded ? tShell('collapse_folder', { name: label }) : tShell('expand_folder', { name: label })}
|
||||
aria-expanded={isExpanded}
|
||||
className={cn("flex items-center justify-center rounded hover:bg-muted active:bg-accent transition-colors", TOUCH_HIT_AREA)}
|
||||
style={{ width: CHEVRON_SLOT, height: CHEVRON_SLOT }}
|
||||
// 24px to hit (WCAG 2.5.8) in the 20px slot: the negative
|
||||
// margin gives the extra 2px on each side back.
|
||||
style={{ width: 24, height: 24, margin: (CHEVRON_SLOT - 24) / 2 }}
|
||||
title={isExpanded ? t('collapse_tooltip') : t('expand_tooltip')}
|
||||
>
|
||||
{isExpanded ? (
|
||||
|
||||
@@ -90,7 +90,7 @@ export function SearchChips({
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onRemoveFilter(chip.key)}
|
||||
className="ms-0.5 p-0.5 rounded-full hover:bg-primary/20 transition-colors"
|
||||
className="-my-1 -me-1 p-1.5 rounded-full hover:bg-primary/20 transition-colors"
|
||||
>
|
||||
<X className="w-3 h-3" />
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user