refactor(emailclient): full a11y pass, component decomposition, keyboard shortcuts

Accessibility:
- All components: data-testid, aria-label, aria-pressed, aria-current
- ComposeWindow: role=dialog, aria-modal, focus trap, Escape handler
- EmailCard: role=article, keyboard nav (Enter/Space), aria-current
- ThreadList: role=list with listitem wrappers, role=status empty state
- FolderNavigation: role=navigation, role=list, aria-current
- RecipientInput: role=group, aria-label per type, onKeyPress→onKeyDown
- BodyEditor: role=toolbar, aria-pressed on mode buttons
- StarButton: MaterialIcon (star/star_border), dynamic aria-label
- MarkAsReadCheckbox: dynamic label based on read state
- EmailHeader: role=banner, <time> element, data-testids

Component decomposition:
- Extract useEmailClient hook (state + callbacks)
- Extract demo-emails.ts (data constants)
- EmailClientContent.tsx: 267→127 LOC (composition only)

New: useKeyboardShortcuts hook (Gmail-style, 47 LOC)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-19 15:51:54 +00:00
parent 6e394d7846
commit f22caa6e16
15 changed files with 884 additions and 453 deletions

View File

@@ -10,3 +10,4 @@ export { useMailboxes, type UseMailboxesResult, type Folder } from './useMailbox
export { useAccounts, type UseAccountsResult, type EmailAccount } from './useAccounts'
export { useCompose, type UseComposeResult, type EmailDraft } from './useCompose'
export { useMessages, type UseMessagesResult, type Message } from './useMessages'
export { useKeyboardShortcuts, type KeyboardShortcutMap } from './useKeyboardShortcuts'

View File

@@ -0,0 +1,47 @@
import { useEffect } from 'react'
/**
* Map of shortcut keys to handler functions.
* Use '#' for Shift+3, '?' for Shift+/, etc.
*/
export interface KeyboardShortcutMap {
[key: string]: () => void
}
const IGNORED_TAGS = new Set([
'INPUT', 'TEXTAREA', 'SELECT',
])
function isEditable(el: EventTarget | null): boolean {
if (!el || !(el instanceof HTMLElement)) return false
if (IGNORED_TAGS.has(el.tagName)) return true
return el.isContentEditable
}
/**
* Gmail-style keyboard shortcuts hook.
* Fires mapped actions on keydown unless an
* input/textarea/contenteditable is focused.
* @param shortcuts - key-to-action map
* @param enabled - toggle listener (default true)
*/
export function useKeyboardShortcuts(
shortcuts: KeyboardShortcutMap,
enabled = true
): void {
useEffect(() => {
if (!enabled) return
const handler = (e: KeyboardEvent) => {
if (isEditable(e.target)) return
const action = shortcuts[e.key]
if (action) {
e.preventDefault()
action()
}
}
window.addEventListener('keydown', handler)
return () => window.removeEventListener('keydown', handler)
}, [shortcuts, enabled])
}