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.
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 linesKeyboard 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
- Reproduce the reported route and opening action with the keyboard.
- Identify the dialog implementation, browser targets, and existing test runner.
- Write the expected sequence before changing the implementation:
| Decision | Record for this dialog |
|---|---|
| Opening action | Trigger label and the key that activates it |
| Initial focus | Named control or introductory element, with the reason |
| Tab sequence | Expected controls, including first and last |
| Dismissal | Escape behavior and the visible close or cancel control |
| Return focus | Trigger, 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 regression | Observable expected outcome |
|---|---|
| A loading action replaces the last control | Tab uses the current boundary; focus does not land behind the modal |
| A validation error appears | The error is exposed and the user can correct it without losing dialog context |
| Completing the action removes its trigger | Focus reaches the documented successor, not a detached element |
| A nested modal is supported | Closing the inner dialog returns to the outer dialog; closing that returns to its opener |
| The dialog contains one active control | Both 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
Related Skills
Keyboard Navigation
Keyboard navigation patterns, focus order, and shortcut design for fully keyboard-accessible interfaces
Screen Reader Compat
Building web content that works correctly with screen readers like NVDA, JAWS, and VoiceOver
Wcag Guidelines
WCAG 2.2 compliance levels, success criteria, and implementation strategies for accessible web content
Accessible Forms
Accessible form design patterns including labels, validation, error handling, and multi-step forms
Aria Patterns
ARIA roles, states, and properties for building accessible custom widgets and UI components
Axe Testing
Automated accessibility testing with axe-core, including CI integration, custom rules, and result analysis