
Chris RodriguezBuild a small Chromium fixture that distinguishes native dialog inertness, keyboard movement, and focus restoration without assuming immediate Tab wrapping.
A native dialog can make the background inert without immediately wrapping every boundary Tab press between its first and last controls. Browser controls can participate in navigation. Test the page-level invariant directly, and keep positive assertions for movement between internal controls.
Save this as main.mjs:
export const html = `<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Native dialog focus fixture</title>
</head>
<body>
<button id="background">Background action</button>
<button id="opener">Open dialog</button>
<dialog id="dlg" aria-labelledby="dialog-title">
<h2 id="dialog-title">Example settings</h2>
<form method="dialog">
<button id="close" autofocus>Close</button>
</form>
<label for="in1">Display name</label>
<input id="in1">
<button id="in2">Another action</button>
</dialog>
<script>
const dialog = document.getElementById('dlg');
const opener = document.getElementById('opener');
opener.addEventListener('click', () => dialog.showModal());
// Native restoration handles a connected opener; the app owns this fallback.
dialog.addEventListener('close', () => {
if (!opener.isConnected) document.getElementById('background').focus();
});
</script>
</body>
</html>`;
Close, the input, and the final button are three internal keyboard stops. Count Close when predicting the order. The accessible name and input label make the fixture understandable, but their presence is not a full accessibility audit.
Open with Enter and check :modal and the declared autofocus target. Verify Tab moves Close to input to final button, and reverse navigation moves back through those controls. At the boundaries, exclude both the opener and the background button rather than requiring an immediate wrap. Challenge inertness with programmatic focus attempts too.
A separate show() case must allow background focus. That negative control exposes a background button that was accidentally unfocusable for some unrelated reason. Test both Escape and the Close button returning to a connected opener. When the opener is removed, wait for the application close listener to focus its explicit fallback; open=false alone is too early to establish that asynchronous result.
Use Node 22, npm init -y, npm install --save-dev playwright@1.63.0, and npx playwright install chromium. The full article supplies main.test.mjs; run node --test main.test.mjs. Tests load local HTML, block network requests, and close each browser in finally.
A Chromium result is not cross-browser or screen-reader certification. Keep the browser version with the evidence, and investigate assertion failures separately from installation errors.
Full test file and limitations: https://chriseugenerodriguez.com/blog/smallest-modal-focus-fixture