Skip to main content
Technology & EngineeringAccessibility117 lines

Keyboard Dialog Regression Test

Turn a modal dialog keyboard or focus bug into a reproducible browser regression test. Cover initial focus, tab boundaries, dismissal, and focus restoration, including removed triggers and dynamic controls when those states exist.

Quick Summary18 lines
Use this skill when a dialog opens but keyboard users lose their place, reach the
background page, cannot dismiss it, or return to the wrong control afterward.
Produce a failing reproduction, a focused repair, and evidence from the repaired
flow. A general accessibility scan alone does not demonstrate this behavior.

## Key Points

1. Reproduce the reported route and opening action with the keyboard.
2. Identify the dialog implementation, browser targets, and existing test runner.
3. Write the expected sequence before changing the implementation:
- Reach the trigger using Tab in one full keyboard journey; activating a locator
- Record the focused element after opening and after each boundary key press.
- Exercise both Tab directions, Escape, and the explicit close control.
- Verify the dialog's accessible name and the visible focus indicator.
- Reopen it once to catch stale listeners, saved focus, or leftover inert state.
- If controls appear, disappear, or become disabled while open, repeat the
skilldb get accessibility-skills/keyboard-dialog-regression-testFull skill: 117 lines
Paste into your CLAUDE.md or agent config

Keyboard Dialog Regression Test

Use this skill when a dialog opens but keyboard users lose their place, reach the background page, cannot dismiss it, or return to the wrong control afterward. Produce a failing reproduction, a focused repair, and evidence from the repaired flow. A general accessibility scan alone does not demonstrate this behavior.

Establish the dialog contract

  1. Reproduce the reported route and opening action with the keyboard.
  2. Identify the dialog implementation, browser targets, and existing test runner.
  3. Write the expected sequence before changing the implementation:
DecisionRecord for this dialog
Opening actionTrigger label and the key that activates it
Initial focusNamed control or introductory element, with the reason
Tab sequenceExpected controls, including first and last
DismissalEscape behavior and the visible close or cancel control
Return focusTrigger, or a named successor if the workflow removes it

Choose initial focus from the task. Long structured content may need a heading with tabindex="-1"; a destructive confirmation may start on the safer action. Closing normally restores the opener, but a removed opener or completed workflow needs a deliberate successor. These choices follow the W3C modal dialog pattern.

For native <dialog>, confirm that the modal path calls showModal(); show() and the open attribute create a non-modal presentation. Do not add tabindex to the native dialog element. For a custom dialog, check background interaction as well as ARIA: aria-modal="true" does not implement keyboard containment. See the MDN dialog reference.

Capture the regression

  • Reach the trigger using Tab in one full keyboard journey; activating a locator directly does not prove that the trigger is reachable.
  • Record the focused element after opening and after each boundary key press.
  • Exercise both Tab directions, Escape, and the explicit close control.
  • Verify the dialog's accessible name and the visible focus indicator.
  • Reopen it once to catch stale listeners, saved focus, or leftover inert state.
  • If controls appear, disappear, or become disabled while open, repeat the boundary checks after that change instead of relying on an initial snapshot.

Use real browser key events. In Playwright, locator.press() first focuses its target; use page.keyboard.press() when testing where focus already is. Prefer retrying assertions to fixed sleeps. The actions guide and assertion reference describe these APIs.

This example is a boundary test for a known Preferences fixture whose first control is Cancel and last control is Save. Adapt the contract and names to the actual component; the two direct focus calls deliberately isolate its boundaries.

import { test, expect } from '@playwright/test';

test('preferences retains and restores keyboard focus', async ({ page }) => {
  await page.goto('/preferences');
  const trigger = page.getByRole('button', { name: 'Edit preferences', exact: true });
  const dialog = page.getByRole('dialog', { name: 'Preferences', exact: true });
  const first = dialog.getByRole('button', { name: 'Cancel', exact: true });
  const last = dialog.getByRole('button', { name: 'Save', exact: true });

  await trigger.focus(); // Setup only; test Tab reachability separately.
  await page.keyboard.press('Enter');
  await expect(dialog).toBeVisible();
  await expect(first).toBeFocused();

  await last.focus();
  await page.keyboard.press('Tab');
  await expect(first).toBeFocused();
  await first.focus();
  await page.keyboard.press('Shift+Tab');
  await expect(last).toBeFocused();

  await page.keyboard.press('Escape');
  await expect(dialog).toBeHidden();
  await expect(trigger).toBeFocused();
});

Add cases that distinguish a repair from a passing snapshot

Existing state or regressionObservable expected outcome
A loading action replaces the last controlTab uses the current boundary; focus does not land behind the modal
A validation error appearsThe error is exposed and the user can correct it without losing dialog context
Completing the action removes its triggerFocus reaches the documented successor, not a detached element
A nested modal is supportedClosing the inner dialog returns to the outer dialog; closing that returns to its opener
The dialog contains one active controlBoth tab directions keep that control usable

Do not add unsupported nested-modal behavior merely to satisfy a test matrix. If the product intentionally intercepts Escape, document the reason and prove a keyboard-operable exit; do not report that exception as ordinary APG behavior.

In an isolated fixture or temporary patch, introduce one known fault and run the corresponding test again. Returning focus to the wrong element should fail the return-focus assertion; allowing background focus should fail its boundary check. Do not remove a redundant handler and assume native behavior will become broken. Restore the fixture afterward. A test that stays green has not caught that fault.

Deliver evidence and limits

Report the reproduced key sequence, failing assertion, repair, browser versions, and final result. Keep a trace or small video when focus movement is hard to see. Run the project's supported browser matrix and manually inspect the focus ring. State whether an actual screen-reader walkthrough was performed; DOM assertions and automated scans do not prove spoken output or complete WCAG conformance.

Install this skill directly: skilldb add accessibility-skills

Get CLI access →