fix(journey-editor): remove-confirm focus, role=group, Escape key

Focus moves to Cancel when confirm appears (no focus-drops-to-body),
confirm area has role=group with aria-label, and Escape dismisses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Marcel
2026-06-10 19:32:16 +02:00
parent 585244d65b
commit 74b94ccd84
2 changed files with 61 additions and 2 deletions

View File

@@ -74,9 +74,11 @@ async function handleNoteRemove() {
}
}
function handleRemoveClick() {
async function handleRemoveClick() {
if (needsConfirmOnRemove) {
showRemoveConfirm = true;
await tick();
rootEl?.querySelector<HTMLElement>('[data-remove-confirm-cancel]')?.focus();
} else {
onRemove();
}
@@ -167,18 +169,21 @@ async function handleRemoveCancel() {
{m.journey_item_pending_remove()}
</span>
{:else if showRemoveConfirm}
<div class="flex items-center gap-2">
<div role="group" aria-label={m.journey_remove_confirm()} class="flex items-center gap-2">
<span class="font-sans text-xs text-ink-2">{m.journey_remove_confirm()}</span>
<button
type="button"
onclick={handleRemoveConfirm}
onkeydown={(e) => e.key === 'Escape' && handleRemoveCancel()}
class="inline-flex min-h-[44px] items-center rounded bg-danger px-3 font-sans text-xs font-medium text-white hover:opacity-90 focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
{m.journey_remove_confirm_yes()}
</button>
<button
type="button"
data-remove-confirm-cancel
onclick={handleRemoveCancel}
onkeydown={(e) => e.key === 'Escape' && handleRemoveCancel()}
class="inline-flex min-h-[44px] items-center rounded border border-line px-3 font-sans text-xs font-medium text-ink hover:bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
{m.journey_remove_confirm_cancel()}

View File

@@ -221,6 +221,60 @@ describe('JourneyItemRow — remove confirm', () => {
});
});
describe('JourneyItemRow — remove confirm a11y', () => {
it('confirm area is wrapped in role=group with an accessible label', async () => {
render(JourneyItemRow, {
item: docItem({ note: 'Wichtige Notiz' }),
...defaultProps()
});
await userEvent.click(page.getByRole('button', { name: m.journey_remove_item_aria() }));
const group = document.querySelector('[role="group"]');
expect(group).toBeTruthy();
expect(group!.getAttribute('aria-label')).toBeTruthy();
});
it('keyboard focus moves to Cancel button when confirm appears', async () => {
render(JourneyItemRow, {
item: docItem({ note: 'Wichtige Notiz' }),
...defaultProps()
});
await userEvent.click(page.getByRole('button', { name: m.journey_remove_item_aria() }));
await vi.waitFor(() => {
const cancelBtn = page
.getByRole('button', { name: m.journey_remove_confirm_cancel() })
.element();
expect(document.activeElement).toBe(cancelBtn);
});
});
it('pressing Escape while confirm is open hides confirm and refocuses remove button', async () => {
render(JourneyItemRow, {
item: docItem({ note: 'Wichtige Notiz' }),
...defaultProps()
});
await userEvent.click(page.getByRole('button', { name: m.journey_remove_item_aria() }));
await vi.waitFor(() => {
const cancelBtn = page
.getByRole('button', { name: m.journey_remove_confirm_cancel() })
.element();
expect(document.activeElement).toBe(cancelBtn);
});
await userEvent.keyboard('{Escape}');
await vi.waitFor(() => {
const removeBtn = page.getByRole('button', { name: m.journey_remove_item_aria() }).element();
expect(document.activeElement).toBe(removeBtn);
});
await expect.element(page.getByText(m.journey_remove_confirm())).not.toBeInTheDocument();
});
});
describe('JourneyItemRow — pending remove state', () => {
it('renders dimmed with the pending text and without a remove button', async () => {
render(JourneyItemRow, {