fix: auto-focus input fields in email composer for improved user experience #126
This commit is contained in:
@@ -332,6 +332,17 @@ export function EmailComposer({
|
|||||||
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
return () => window.removeEventListener('beforeunload', handleBeforeUnload);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Auto-focus the To field when composing a new email or forwarding
|
||||||
|
useEffect(() => {
|
||||||
|
if (mode === 'forward' || mode === 'compose') {
|
||||||
|
// Small delay to ensure the input is rendered
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
toInputRef.current?.focus();
|
||||||
|
}, 100);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [mode]);
|
||||||
|
|
||||||
const [autocompleteResults, setAutocompleteResults] = useState<Array<{ name: string; email: string }>>([]);
|
const [autocompleteResults, setAutocompleteResults] = useState<Array<{ name: string; email: string }>>([]);
|
||||||
const [activeAutoField, setActiveAutoField] = useState<'to' | 'cc' | 'bcc' | null>(null);
|
const [activeAutoField, setActiveAutoField] = useState<'to' | 'cc' | 'bcc' | null>(null);
|
||||||
const [autoSelectedIndex, setAutoSelectedIndex] = useState(-1);
|
const [autoSelectedIndex, setAutoSelectedIndex] = useState(-1);
|
||||||
@@ -339,10 +350,26 @@ export function EmailComposer({
|
|||||||
const toInputRef = useRef<HTMLInputElement>(null);
|
const toInputRef = useRef<HTMLInputElement>(null);
|
||||||
const ccInputRef = useRef<HTMLInputElement>(null);
|
const ccInputRef = useRef<HTMLInputElement>(null);
|
||||||
const bccInputRef = useRef<HTMLInputElement>(null);
|
const bccInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const subjectInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const bodyRef = useRef<HTMLTextAreaElement>(null);
|
||||||
|
const editorContainerRef = useRef<HTMLDivElement>(null);
|
||||||
const toDropdownRef = useRef<HTMLDivElement>(null);
|
const toDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const ccDropdownRef = useRef<HTMLDivElement>(null);
|
const ccDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
const bccDropdownRef = useRef<HTMLDivElement>(null);
|
const bccDropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const focusSubject = useCallback(() => {
|
||||||
|
subjectInputRef.current?.focus();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const focusBody = useCallback(() => {
|
||||||
|
if (plainTextMode) {
|
||||||
|
bodyRef.current?.focus();
|
||||||
|
} else {
|
||||||
|
const proseMirror = editorContainerRef.current?.querySelector('.ProseMirror') as HTMLElement | null;
|
||||||
|
proseMirror?.focus();
|
||||||
|
}
|
||||||
|
}, [plainTextMode]);
|
||||||
|
|
||||||
const handleAutocomplete = useCallback((value: string, field: 'to' | 'cc' | 'bcc') => {
|
const handleAutocomplete = useCallback((value: string, field: 'to' | 'cc' | 'bcc') => {
|
||||||
if (autocompleteTimeoutRef.current) {
|
if (autocompleteTimeoutRef.current) {
|
||||||
clearTimeout(autocompleteTimeoutRef.current);
|
clearTimeout(autocompleteTimeoutRef.current);
|
||||||
@@ -1071,6 +1098,7 @@ export function EmailComposer({
|
|||||||
onInsertAutocomplete={insertAutocomplete}
|
onInsertAutocomplete={insertAutocomplete}
|
||||||
validationError={validationErrors.to}
|
validationError={validationErrors.to}
|
||||||
validationMessage={t('validation.recipient_required')}
|
validationMessage={t('validation.recipient_required')}
|
||||||
|
onTab={focusSubject}
|
||||||
/>
|
/>
|
||||||
<div className="flex gap-0.5 shrink-0">
|
<div className="flex gap-0.5 shrink-0">
|
||||||
<Button
|
<Button
|
||||||
@@ -1140,6 +1168,7 @@ export function EmailComposer({
|
|||||||
<div className="flex items-center gap-2 px-4 py-2.5">
|
<div className="flex items-center gap-2 px-4 py-2.5">
|
||||||
<span className="text-sm text-muted-foreground w-12 md:w-16 shrink-0">{t('subject_label')}</span>
|
<span className="text-sm text-muted-foreground w-12 md:w-16 shrink-0">{t('subject_label')}</span>
|
||||||
<Input
|
<Input
|
||||||
|
ref={subjectInputRef}
|
||||||
type="text"
|
type="text"
|
||||||
placeholder={t('subject_placeholder')}
|
placeholder={t('subject_placeholder')}
|
||||||
value={subject}
|
value={subject}
|
||||||
@@ -1147,6 +1176,12 @@ export function EmailComposer({
|
|||||||
setSubject(e.target.value);
|
setSubject(e.target.value);
|
||||||
if (validationErrors.subject) setValidationErrors(prev => ({ ...prev, subject: false }));
|
if (validationErrors.subject) setValidationErrors(prev => ({ ...prev, subject: false }));
|
||||||
}}
|
}}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
if (e.key === 'Tab' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
focusBody();
|
||||||
|
}
|
||||||
|
}}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex-1 border-0 focus-visible:ring-0 h-8 px-0 text-sm",
|
"flex-1 border-0 focus-visible:ring-0 h-8 px-0 text-sm",
|
||||||
validationErrors.subject && "ring-2 ring-red-500 dark:ring-red-400"
|
validationErrors.subject && "ring-2 ring-red-500 dark:ring-red-400"
|
||||||
@@ -1159,6 +1194,7 @@ export function EmailComposer({
|
|||||||
{/* Body */}
|
{/* Body */}
|
||||||
{plainTextMode ? (
|
{plainTextMode ? (
|
||||||
<textarea
|
<textarea
|
||||||
|
ref={bodyRef}
|
||||||
value={body}
|
value={body}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setBody(e.target.value);
|
setBody(e.target.value);
|
||||||
@@ -1173,16 +1209,18 @@ export function EmailComposer({
|
|||||||
aria-invalid={validationErrors.body || undefined}
|
aria-invalid={validationErrors.body || undefined}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<RichTextEditor
|
<div ref={editorContainerRef}>
|
||||||
content={body}
|
<RichTextEditor
|
||||||
onChange={(html) => {
|
content={body}
|
||||||
setBody(html);
|
onChange={(html) => {
|
||||||
if (validationErrors.body) setValidationErrors(prev => ({ ...prev, body: false }));
|
setBody(html);
|
||||||
}}
|
if (validationErrors.body) setValidationErrors(prev => ({ ...prev, body: false }));
|
||||||
onImageUpload={handleImageUpload}
|
}}
|
||||||
placeholder={t('body_placeholder')}
|
onImageUpload={handleImageUpload}
|
||||||
hasError={validationErrors.body}
|
placeholder={t('body_placeholder')}
|
||||||
/>
|
hasError={validationErrors.body}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{plainTextMode ? (
|
{plainTextMode ? (
|
||||||
@@ -1514,6 +1552,7 @@ function RecipientChipInput({
|
|||||||
onInsertAutocomplete,
|
onInsertAutocomplete,
|
||||||
validationError,
|
validationError,
|
||||||
validationMessage,
|
validationMessage,
|
||||||
|
onTab,
|
||||||
}: {
|
}: {
|
||||||
value: string;
|
value: string;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
@@ -1530,6 +1569,7 @@ function RecipientChipInput({
|
|||||||
onInsertAutocomplete: (email: string, field: 'to' | 'cc' | 'bcc') => void;
|
onInsertAutocomplete: (email: string, field: 'to' | 'cc' | 'bcc') => void;
|
||||||
validationError?: boolean;
|
validationError?: boolean;
|
||||||
validationMessage?: string;
|
validationMessage?: string;
|
||||||
|
onTab?: () => void;
|
||||||
}) {
|
}) {
|
||||||
const allParts = value.split(',').map(s => s.trim()).filter(Boolean);
|
const allParts = value.split(',').map(s => s.trim()).filter(Boolean);
|
||||||
const hasTrailingComma = value.trimEnd().endsWith(',');
|
const hasTrailingComma = value.trimEnd().endsWith(',');
|
||||||
@@ -1563,7 +1603,18 @@ function RecipientChipInput({
|
|||||||
if ((e.key === ' ' || e.key === 'Enter' || e.key === 'Tab') && inputText.trim()) {
|
if ((e.key === ' ' || e.key === 'Enter' || e.key === 'Tab') && inputText.trim()) {
|
||||||
if (e.key !== 'Tab') e.preventDefault();
|
if (e.key !== 'Tab') e.preventDefault();
|
||||||
commitCurrentInput();
|
commitCurrentInput();
|
||||||
setTimeout(() => inputRef.current?.focus(), 0);
|
if (e.key === 'Tab' && onTab) {
|
||||||
|
e.preventDefault();
|
||||||
|
setTimeout(() => onTab(), 0);
|
||||||
|
} else {
|
||||||
|
setTimeout(() => inputRef.current?.focus(), 0);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.key === 'Tab' && !e.shiftKey && onTab) {
|
||||||
|
e.preventDefault();
|
||||||
|
onTab();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user