Files
metabuilder/components/fakemui/email/inputs/RecipientInput.tsx
johndoe6345789 f22caa6e16 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>
2026-03-19 15:51:54 +00:00

85 lines
2.1 KiB
TypeScript

// fakemui/react/components/email/inputs/RecipientInput.tsx
import React, { forwardRef, useState } from 'react'
import { Box } from '../../layout/Box'
import { TextField } from '../../inputs/TextField'
import { Chip } from '../../data-display/Chip'
import { useAccessible } from '../../../../hooks/useAccessible'
export interface RecipientInputProps
extends React.InputHTMLAttributes<HTMLInputElement> {
recipients?: string[]
onRecipientsChange?: (recipients: string[]) => void
recipientType?: 'to' | 'cc' | 'bcc'
testId?: string
}
export const RecipientInput = forwardRef<
HTMLInputElement,
RecipientInputProps
>(({
recipients = [],
onRecipientsChange,
recipientType = 'to',
testId: customTestId,
...props
}, ref) => {
const [inputValue, setInputValue] = useState('')
const accessible = useAccessible({
feature: 'email',
component: 'recipient-input',
identifier: customTestId || recipientType,
ariaLabel: `Add ${recipientType} recipient`
})
const addRecipient = () => {
if (inputValue && inputValue.includes('@')) {
onRecipientsChange?.([
...recipients, inputValue.trim()
])
setInputValue('')
}
}
const removeRecipient = (index: number) => {
onRecipientsChange?.(
recipients.filter((_, i) => i !== index)
)
}
const { size: _size, ...textFieldProps } = props
return (
<Box
className="recipient-input"
role="group"
aria-label={`${recipientType.toUpperCase()} recipients`}
>
<div className="recipient-chips">
{recipients.map((r, i) => (
<Chip
key={i}
onDelete={() => removeRecipient(i)}
aria-label={`Remove ${r}`}
>
{r}
</Chip>
))}
</div>
<TextField
ref={ref}
type="email"
placeholder={`Add ${recipientType} recipient...`}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={(e) =>
e.key === 'Enter' && addRecipient()
}
{...accessible}
{...textFieldProps}
/>
</Box>
)
})
RecipientInput.displayName = 'RecipientInput'