Repository navigation
USWDS - Bug: Modal and header leave the page aria-hidden after teardown #6830
Description
Describe the bug
When a modal or the mobile navigation opens, USWDS sets aria-hidden="true" on all other body content. This is correct. It stops screen readers from reading the page behind the open component.
The close path removes those attributes. The teardown path does not. If off() runs while the component is still open, the attributes stay. The whole page remains hidden from assistive technology, and there is no recovery except a page reload.
This affects two components:
usa-header:teardown()removes the resize listener and resetsnavActive. It does not callshowNonNavItems().usa-modal:teardown()callscleanUpModal(), which restores the modal to its original place in the DOM. It does not removearia-hiddenordata-modal-hiddenfrom non-modal elements.
For a screen reader user, the page reads as empty. This is a major functionality problem for that user, and it is silent for everyone else.
Workaround: close the component before calling off().
Steps to reproduce the bug
Save this as repro-short.js in the repo root and run node repro-short.js.
// Run from the repo root: node repro-short.js
const { JSDOM } = require("jsdom");
const dom = new JSDOM(`<!DOCTYPE html><body>
<div class="usa-overlay"></div>
<header class="usa-header">
<div class="usa-nav-container">
<button type="button" class="usa-menu-btn">Menu</button>
<nav class="usa-nav">
<button type="button" class="usa-nav__close">Close</button>
<ul class="usa-nav__primary"><li><a href="#one">One</a></li></ul>
</nav>
</div>
</header>
<main id="main-content"><h1>Page content</h1></main>
</body>`, { pretendToBeVisual: true });
for (const k of ["window", "document", "navigator", "HTMLElement", "Element", "Event", "getComputedStyle"]) {
global[k] = k === "window" ? dom.window : dom.window[k];
}
const navigation = require("./packages/usa-header/src/index.js");
const main = document.getElementById("main-content");
const show = (l) => console.log(`${l.padEnd(22)} aria-hidden=${main.getAttribute("aria-hidden")}`);
navigation.on(document.body);
navigation.toggleNav.call(navigation, true);
navigation.toggleNav.call(navigation, false);
show("closed normally"); // control: restored
navigation.toggleNav.call(navigation, true);
navigation.off(document.body);
show("off() while open"); // bug: still hiddenOutput:
closed normally aria-hidden=null
off() while open aria-hidden=true
The first line is the control. A normal close restores the page. The second line is the bug.
I also tested off() while the nav is already closed. That is harmless. So the problem is specific to teardown while the component is open.
The same test on usa-modal gives the same result. Open the modal, call modal.off(document.body), and non-modal content keeps aria-hidden="true".
Expected Behavior
teardown() should restore the page content that the component hid from assistive technology. Page content must never be left hidden after a component is removed.
usa-modal already does this correctly on close. The restore loop runs unconditionally, because the opener may be gone from the document by then. Teardown needs the same guarantee.
Only the assistive technology state needs restoring. packages/usa-nav/src/test/navigation.spec.js documents off() as removing event listeners without closing an open component, so visible state should be left alone.
Related code
packages/usa-header/src/index.js:258. teardown() never calls showNonNavItems() (line 63).
packages/usa-modal/src/index.js:392. teardown() calls cleanUpModal(), which restores the modal's original DOM location but never removes aria-hidden or data-modal-hidden. The working restore loop is at line 180, in the close path only.
Additional context
Calling off() in a component's destroy lifecycle is the normal pattern for framework users, and #5062 describes teams doing this in React, Vue, Angular, and Nuxt. An unmount or route change while a modal or the mobile menu is open triggers this.
Related to #6785 and PR #6786, which fixed the same class of problem on the modal close path. That fix does not cover teardown.
I have a fix for both components with regression tests and will open a PR shortly.
Metadata
Metadata
Assignees
Labels
Type
Projects
- StatusShow more project fieldsTriage
Activity