feat(fakemui): add email input components (addresses, body)

This commit is contained in:
2026-01-23 19:33:30 +00:00
parent 468a8bdf7a
commit 111236a17f
4 changed files with 168 additions and 0 deletions
@@ -0,0 +1,50 @@
// fakemui/react/components/email/inputs/BodyEditor.tsx
import React, { forwardRef } from 'react'
import { Box } from '../../layout/Box'
import { useAccessible } from '../../../../src/utils/useAccessible'
export interface BodyEditorProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {
mode?: 'plain' | 'html'
onModeChange?: (mode: 'plain' | 'html') => void
testId?: string
}
export const BodyEditor = forwardRef<HTMLTextAreaElement, BodyEditorProps>(
({ mode = 'plain', onModeChange, testId: customTestId, ...props }, ref) => {
const accessible = useAccessible({
feature: 'email',
component: 'body-editor',
identifier: customTestId || 'body'
})
return (
<Box className="body-editor">
<div className="body-editor-toolbar">
<button
type="button"
className={`mode-btn ${mode === 'plain' ? 'mode-btn--active' : ''}`}
onClick={() => onModeChange?.('plain')}
>
Plain Text
</button>
<button
type="button"
className={`mode-btn ${mode === 'html' ? 'mode-btn--active' : ''}`}
onClick={() => onModeChange?.('html')}
>
HTML
</button>
</div>
<textarea
ref={ref}
className="body-editor-textarea"
placeholder="Write your message here..."
{...accessible}
{...props}
/>
</Box>
)
}
)
BodyEditor.displayName = 'BodyEditor'
@@ -0,0 +1,48 @@
// fakemui/react/components/email/inputs/EmailAddressInput.tsx
import React, { forwardRef } from 'react'
import { TextField, TextFieldProps } from '../../inputs/TextField'
import { useAccessible } from '../../../../src/utils/useAccessible'
export interface EmailAddressInputProps extends Omit<TextFieldProps, 'type'> {
onValidate?: (valid: boolean) => void
allowMultiple?: boolean
}
export const EmailAddressInput = forwardRef<HTMLInputElement, EmailAddressInputProps>(
({ onValidate, allowMultiple = false, testId: customTestId, ...props }, ref) => {
const accessible = useAccessible({
feature: 'email',
component: 'email-input',
identifier: customTestId || 'email'
})
const validateEmail = (value: string) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (allowMultiple) {
const emails = value.split(',').map(e => e.trim())
return emails.every(e => emailRegex.test(e) || e === '')
}
return emailRegex.test(value) || value === ''
}
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const valid = validateEmail(e.target.value)
onValidate?.(valid)
props.onChange?.(e)
}
return (
<TextField
ref={ref}
type="email"
label={props.label || (allowMultiple ? 'Recipients' : 'Email Address')}
placeholder={allowMultiple ? 'user@example.com, another@example.com' : 'user@example.com'}
{...accessible}
{...props}
onChange={handleChange}
/>
)
}
)
EmailAddressInput.displayName = 'EmailAddressInput'
@@ -0,0 +1,67 @@
// 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 '../../../../src/utils/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
})
const handleAddRecipient = () => {
if (inputValue && inputValue.includes('@')) {
const newRecipients = [...recipients, inputValue.trim()]
onRecipientsChange?.(newRecipients)
setInputValue('')
}
}
const handleRemoveRecipient = (index: number) => {
const newRecipients = recipients.filter((_, i) => i !== index)
onRecipientsChange?.(newRecipients)
}
// Filter out incompatible HTML input attributes
const { size: _size, ...textFieldProps } = props
return (
<Box className="recipient-input">
<div className="recipient-chips">
{recipients.map((recipient, index) => (
<Chip
key={index}
onDelete={() => handleRemoveRecipient(index)}
>
{recipient}
</Chip>
))}
</div>
<TextField
ref={ref}
type="email"
placeholder={`Add ${recipientType} recipient...`}
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAddRecipient()}
{...accessible}
{...textFieldProps}
/>
</Box>
)
}
)
RecipientInput.displayName = 'RecipientInput'
@@ -0,0 +1,3 @@
export { EmailAddressInput, type EmailAddressInputProps } from './EmailAddressInput'
export { RecipientInput, type RecipientInputProps } from './RecipientInput'
export { BodyEditor, type BodyEditorProps } from './BodyEditor'