From 917b313d5db8d4888e73f03f4735fc465b202cee Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Wed, 28 Oct 2026 10:11:15 +0100 Subject: [PATCH] fix: show a message row's quick actions when the row gets the keyboard focus --- .../email/__tests__/thread-list-item.test.tsx | 18 ++++++++++++++++++ components/email/thread-list-item.tsx | 3 +++ 2 files changed, 21 insertions(+) diff --git a/components/email/__tests__/thread-list-item.test.tsx b/components/email/__tests__/thread-list-item.test.tsx index 396877efc..0e32699c4 100644 --- a/components/email/__tests__/thread-list-item.test.tsx +++ b/components/email/__tests__/thread-list-item.test.tsx @@ -808,6 +808,24 @@ describe('ThreadListItem hover actions', () => { fireEvent.pointerOver(row!, { relatedTarget: screen.getByTestId('list') }); expect(screen.getByTestId('hover-action-delete')).toBeTruthy(); }); + + // Tabbing to a row's checkbox shows them too (group-focus-within), so they + // have to be there without a pointer. + it.each([ + ['a single message', () => groupEmailsByThread([makeEmail()])[0]], + ['a conversation', () => groupEmailsByThread([ + makeEmail({ id: 'a', receivedAt: '2024-01-15T10:00:00Z' }), + makeEmail({ id: 'b', receivedAt: '2024-01-15T11:00:00Z' }), + ])[0]], + ])('mounts them for %s when a control in the row gets the keyboard focus', (_name, makeThread) => { + const { container } = renderInList(makeThread()); + expect(screen.queryByTestId('hover-action-delete')).toBeNull(); + const row = screen.queryByTestId('email-list-item') ?? container.querySelector('[data-unread]'); + const control = row!.querySelector('button, [tabindex]:not([tabindex="-1"])'); + expect(control).not.toBeNull(); + act(() => { control!.focus(); }); + expect(screen.getByTestId('hover-action-delete')).toBeTruthy(); + }); }); describe('ThreadListItem open message', () => { diff --git a/components/email/thread-list-item.tsx b/components/email/thread-list-item.tsx index d393878c1..487d03247 100644 --- a/components/email/thread-list-item.tsx +++ b/components/email/thread-list-item.tsx @@ -80,6 +80,7 @@ export function listMailboxRole(state: { ?? (state.isUnifiedView ? (state.unifiedRole ?? undefined) : undefined); } +/** Hover actions mount on the first pointerover or keyboard focus in the row. */ function useArmOnPointerOver(): [boolean, () => void] { const [armed, setArmed] = React.useState(false); const arm = React.useCallback(() => setArmed(true), []); @@ -545,6 +546,7 @@ const SingleEmailItem = React.forwardRef( {...touchHandlers} data-testid="email-list-item" onPointerOver={armHoverActions} + onFocus={armHoverActions} onPointerDown={(e) => { if (onPrefetch && isMousePressOnRow(e)) onPrefetch(); }} data-email-id={email.id} data-subject={email.subject || ''} @@ -1198,6 +1200,7 @@ export const ThreadListItem = React.forwardRef { if (onPrefetch && isMousePressOnRow(e)) onPrefetch(latestEmail); }} aria-current={isSelected ? 'true' : undefined} data-unread={hasUnread ? 'true' : 'false'}