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:
@@ -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()}
|
||||
|
||||
@@ -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, {
|
||||
|
||||
Reference in New Issue
Block a user