feat: move a recipient between To, Cc and Bcc from the chip's menu

This commit is contained in:
Linus Rath
2026-10-29 17:53:39 +01:00
parent ee486077de
commit 0aa751a3a0
31 changed files with 71 additions and 0 deletions
@@ -299,3 +299,29 @@ describe('recipient fields (SR-06, CV-02)', () => {
expect(chip).toHaveTextContent('invalid_recipient');
});
});
describe('moving a recipient by keyboard (MO-07)', () => {
it('moves a chip to Cc from its menu and announces it', async () => {
render(<EmailComposer initialData={{ ...DRAFT, to: 'Anna Berg <anna@example.com>' }} />);
const chip = within(screen.getByRole('list', { name: 'to:' })).getByRole('listitem');
chip.focus();
fireEvent.keyDown(chip, { key: 'Enter' });
const move = await screen.findByTestId('recipient-move-cc');
expect(move).toHaveTextContent('recipient_move_to:cc');
expect(screen.queryByTestId('recipient-move-to')).toBeNull();
fireEvent.click(move);
await waitFor(() => expect(screen.queryByRole('list', { name: 'to:' })).toBeNull());
expect(within(screen.getByRole('list', { name: 'cc_label' })).getByRole('listitem')).toHaveTextContent('Anna Berg');
expect(liveRegion()).toHaveTextContent('recipient_moved:Anna Berg|cc');
});
it('opens the chip menu with Shift+F10 too', async () => {
render(<EmailComposer initialData={{ ...DRAFT, to: 'anna@example.com' }} />);
const chip = within(screen.getByRole('list', { name: 'to:' })).getByRole('listitem');
chip.focus();
fireEvent.keyDown(chip, { key: 'F10', shiftKey: true });
expect(await screen.findByTestId('recipient-move-bcc')).toBeInTheDocument();
});
});
+16
View File
@@ -5174,6 +5174,7 @@ export function RecipientChipInput({
const chipRefs = useRef<Array<HTMLElement | null>>([]);
const errorId = `${labelId}-error`;
const chipHintId = `${labelId}-chip-hint`;
const fieldNames: Record<'to' | 'cc' | 'bcc', string> = { to: t('to'), cc: t('cc'), bcc: t('bcc') };
const tabChip = chips.length === 0 ? -1 : Math.min(rovingChip ?? chips.length - 1, chips.length - 1);
const focusChip = (index: number) => {
@@ -5749,6 +5750,21 @@ export function RecipientChipInput({
)}
<ContextMenuItem label={t('recipient_edit_name')} onClick={handleEditName} />
<ContextMenuSeparator />
{/* Moving between fields without dragging (WCAG 2.5.7). */}
{(['to', 'cc', 'bcc'] as const).filter((target) => target !== field).map((target) => (
<ContextMenuItem
key={target}
testId={`recipient-move-${target}`}
label={t('recipient_move_to', { field: fieldNames[target] })}
onClick={() => {
if (contextMenu.data) onMoveChip(contextMenu.data.recipient, field, target);
closeContextMenu();
setRovingChip(null);
setTimeout(() => inputRef.current?.focus(), 0);
}}
/>
))}
<ContextMenuSeparator />
<ContextMenuItem label={t('recipient_remove')} onClick={() => {
if (contextMenu.data) {
handleChipRemove(contextMenu.data.index, { stopPropagation: () => {} } as React.MouseEvent);
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "نسخ",
"recipient_remove_named": "إزالة {name}",
"recipient_chip_hint": "يزيله مفتاح Delete، ويفتح Enter خيارات أخرى",
"recipient_move_to": "نقل إلى {field}",
"recipient_added": "تمت إضافة {name} إلى {field}",
"recipient_removed": "تمت إزالة {name} من {field}",
"recipient_moved": "تم نقل {name} إلى {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Copia",
"recipient_remove_named": "Treu {name}",
"recipient_chip_hint": "Supr el treu, Retorn obre més opcions",
"recipient_move_to": "Mou a {field}",
"recipient_added": "S'ha afegit {name} a {field}",
"recipient_removed": "S'ha tret {name} de {field}",
"recipient_moved": "S'ha mogut {name} a {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Kopírovat",
"recipient_remove_named": "Odebrat {name}",
"recipient_chip_hint": "Delete příjemce odebere, Enter otevře další možnosti",
"recipient_move_to": "Přesunout do {field}",
"recipient_added": "{name} přidán do {field}",
"recipient_removed": "{name} odebrán z {field}",
"recipient_moved": "{name} přesunut do {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Kopiér",
"recipient_remove_named": "Fjern {name}",
"recipient_chip_hint": "Delete fjerner modtageren, Enter åbner flere muligheder",
"recipient_move_to": "Flyt til {field}",
"recipient_added": "{name} tilføjet til {field}",
"recipient_removed": "{name} fjernet fra {field}",
"recipient_moved": "{name} flyttet til {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Kopieren",
"recipient_remove_named": "{name} entfernen",
"recipient_chip_hint": "Entf entfernt den Empfänger, Eingabe öffnet weitere Optionen",
"recipient_move_to": "Nach {field} verschieben",
"recipient_added": "{name} zu {field} hinzugefügt",
"recipient_removed": "{name} aus {field} entfernt",
"recipient_moved": "{name} nach {field} verschoben",
+1
View File
@@ -1003,6 +1003,7 @@
"send_guard_failed": "Not sent - a plugin that checks messages before sending failed or did not answer.",
"recipient_remove_named": "Remove {name}",
"recipient_chip_hint": "Delete removes it, Enter opens more options",
"recipient_move_to": "Move to {field}",
"recipient_added": "Added {name} to {field}",
"recipient_removed": "Removed {name} from {field}",
"recipient_moved": "Moved {name} to {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Copiar",
"recipient_remove_named": "Quitar a {name}",
"recipient_chip_hint": "Supr lo quita, Intro abre más opciones",
"recipient_move_to": "Mover a {field}",
"recipient_added": "Se añadió a {name} en {field}",
"recipient_removed": "Se quitó a {name} de {field}",
"recipient_moved": "Se movió a {name} a {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Kopeeri",
"recipient_remove_named": "Eemalda {name}",
"recipient_chip_hint": "Delete eemaldab saaja, Enter avab rohkem valikuid",
"recipient_move_to": "Teisalda väljale {field}",
"recipient_added": "{name} lisati väljale {field}",
"recipient_removed": "{name} eemaldati väljalt {field}",
"recipient_moved": "{name} teisaldati väljale {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "کپی",
"recipient_remove_named": "حذف {name}",
"recipient_chip_hint": "کلید Delete آن را حذف می‌کند و Enter گزینه‌های بیشتری باز می‌کند",
"recipient_move_to": "انتقال به {field}",
"recipient_added": "{name} به {field} افزوده شد",
"recipient_removed": "{name} از {field} حذف شد",
"recipient_moved": "{name} به {field} منتقل شد",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Copier",
"recipient_remove_named": "Retirer {name}",
"recipient_chip_hint": "Suppr le retire, Entrée ouvre d'autres options",
"recipient_move_to": "Déplacer vers {field}",
"recipient_added": "{name} ajouté à {field}",
"recipient_removed": "{name} retiré de {field}",
"recipient_moved": "{name} déplacé vers {field}",
+1
View File
@@ -968,6 +968,7 @@
"copy": "העתק",
"recipient_remove_named": "הסרת {name}",
"recipient_chip_hint": "Delete מסיר את הנמען, Enter פותח אפשרויות נוספות",
"recipient_move_to": "העברה אל {field}",
"recipient_added": "{name} נוסף אל {field}",
"recipient_removed": "{name} הוסר מ{field}",
"recipient_moved": "{name} הועבר אל {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Másolás",
"recipient_remove_named": "{name} eltávolítása",
"recipient_chip_hint": "A Delete eltávolítja, az Enter további lehetőségeket nyit meg",
"recipient_move_to": "Áthelyezés ide: {field}",
"recipient_added": "{name} hozzáadva ide: {field}",
"recipient_removed": "{name} eltávolítva innen: {field}",
"recipient_moved": "{name} áthelyezve ide: {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Copia",
"recipient_remove_named": "Rimuovi {name}",
"recipient_chip_hint": "Canc lo rimuove, Invio apre altre opzioni",
"recipient_move_to": "Sposta in {field}",
"recipient_added": "{name} aggiunto a {field}",
"recipient_removed": "{name} rimosso da {field}",
"recipient_moved": "{name} spostato in {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "コピー",
"recipient_remove_named": "{name} を削除",
"recipient_chip_hint": "Delete キーで削除、Enter キーでその他のオプション",
"recipient_move_to": "{field} に移動",
"recipient_added": "{name} を {field} に追加しました",
"recipient_removed": "{name} を {field} から削除しました",
"recipient_moved": "{name} を {field} に移動しました",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "복사",
"recipient_remove_named": "{name} 제거",
"recipient_chip_hint": "Delete 키로 제거하고 Enter 키로 다른 옵션을 열 수 있어요",
"recipient_move_to": "{field}(으)로 이동",
"recipient_added": "{name}을(를) {field}에 추가했어요",
"recipient_removed": "{name}을(를) {field}에서 제거했어요",
"recipient_moved": "{name}을(를) {field}(으)로 옮겼어요",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Kopēt",
"recipient_remove_named": "Noņemt {name}",
"recipient_chip_hint": "Delete to noņem, Enter atver citas iespējas",
"recipient_move_to": "Pārvietot uz {field}",
"recipient_added": "{name} pievienots laukam {field}",
"recipient_removed": "{name} noņemts no lauka {field}",
"recipient_moved": "{name} pārvietots uz {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Хуулах",
"recipient_remove_named": "{name}-г хасах",
"recipient_chip_hint": "Delete товч хасна, Enter товч бусад сонголтыг нээнэ",
"recipient_move_to": "{field} руу зөөх",
"recipient_added": "{name}-г {field}-д нэмлээ",
"recipient_removed": "{name}-г {field}-ээс хаслаа",
"recipient_moved": "{name}-г {field} руу зөөлөө",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Kopier",
"recipient_remove_named": "Fjern {name}",
"recipient_chip_hint": "Delete fjerner mottakeren, Enter åpner flere valg",
"recipient_move_to": "Flytt til {field}",
"recipient_added": "{name} lagt til i {field}",
"recipient_removed": "{name} fjernet fra {field}",
"recipient_moved": "{name} flyttet til {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Kopiëren",
"recipient_remove_named": "{name} verwijderen",
"recipient_chip_hint": "Delete verwijdert de ontvanger, Enter opent meer opties",
"recipient_move_to": "Verplaatsen naar {field}",
"recipient_added": "{name} toegevoegd aan {field}",
"recipient_removed": "{name} verwijderd uit {field}",
"recipient_moved": "{name} verplaatst naar {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Kopiuj",
"recipient_remove_named": "Usuń {name}",
"recipient_chip_hint": "Delete usuwa odbiorcę, Enter otwiera więcej opcji",
"recipient_move_to": "Przenieś do {field}",
"recipient_added": "Dodano {name} do {field}",
"recipient_removed": "Usunięto {name} z {field}",
"recipient_moved": "Przeniesiono {name} do {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Copiar",
"recipient_remove_named": "Remover {name}",
"recipient_chip_hint": "Delete remove o destinatário, Enter abre mais opções",
"recipient_move_to": "Mover para {field}",
"recipient_added": "{name} adicionado a {field}",
"recipient_removed": "{name} removido de {field}",
"recipient_moved": "{name} movido para {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Copiază",
"recipient_remove_named": "Elimină {name}",
"recipient_chip_hint": "Delete îl elimină, Enter deschide mai multe opțiuni",
"recipient_move_to": "Mută la {field}",
"recipient_added": "{name} a fost adăugat la {field}",
"recipient_removed": "{name} a fost eliminat din {field}",
"recipient_moved": "{name} a fost mutat la {field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Копировать",
"recipient_remove_named": "Убрать {name}",
"recipient_chip_hint": "Delete убирает получателя, Enter открывает другие действия",
"recipient_move_to": "Переместить в {field}",
"recipient_added": "{name}: добавлен в «{field}»",
"recipient_removed": "{name}: убран из «{field}»",
"recipient_moved": "{name}: перемещён в «{field}»",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Kopírovať",
"recipient_remove_named": "Odstrániť {name}",
"recipient_chip_hint": "Delete príjemcu odstráni, Enter otvorí ďalšie možnosti",
"recipient_move_to": "Presunúť do {field}",
"recipient_added": "{name} pridaný do {field}",
"recipient_removed": "{name} odstránený z {field}",
"recipient_moved": "{name} presunutý do {field}",
+1
View File
@@ -1251,6 +1251,7 @@
"signature_edit_hint": "ดับเบิลคลิกเพื่อแก้ไขลายเซ็น",
"recipient_remove_named": "นำ {name} ออก",
"recipient_chip_hint": "กด Delete เพื่อนำออก กด Enter เพื่อเปิดตัวเลือกเพิ่มเติม",
"recipient_move_to": "ย้ายไปที่ {field}",
"recipient_added": "เพิ่ม {name} ใน {field} แล้ว",
"recipient_removed": "นำ {name} ออกจาก {field} แล้ว",
"recipient_moved": "ย้าย {name} ไปที่ {field} แล้ว",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Kopyala",
"recipient_remove_named": "{name} kişisini kaldır",
"recipient_chip_hint": "Delete kaldırır, Enter diğer seçenekleri açar",
"recipient_move_to": "{field} alanına taşı",
"recipient_added": "{name}, {field} alanına eklendi",
"recipient_removed": "{name}, {field} alanından kaldırıldı",
"recipient_moved": "{name}, {field} alanına taşındı",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "Копіювати",
"recipient_remove_named": "Прибрати {name}",
"recipient_chip_hint": "Delete прибирає одержувача, Enter відкриває інші дії",
"recipient_move_to": "Перемістити в {field}",
"recipient_added": "{name}: додано в «{field}»",
"recipient_removed": "{name}: прибрано з «{field}»",
"recipient_moved": "{name}: переміщено в «{field}»",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "複製",
"recipient_remove_named": "移除 {name}",
"recipient_chip_hint": "按 Delete 移除,按 Enter 開啟更多選項",
"recipient_move_to": "移到{field}",
"recipient_added": "已將 {name} 新增到{field}",
"recipient_removed": "已將 {name} 從{field}移除",
"recipient_moved": "已將 {name} 移到{field}",
+1
View File
@@ -1003,6 +1003,7 @@
"copy": "复制",
"recipient_remove_named": "移除 {name}",
"recipient_chip_hint": "按 Delete 移除,按 Enter 打开更多选项",
"recipient_move_to": "移到{field}",
"recipient_added": "已将 {name} 添加到{field}",
"recipient_removed": "已将 {name} 从{field}移除",
"recipient_moved": "已将 {name} 移到{field}",