fix(apps): make a covered elicitation dialog inert

Only the top modal owned the focus trap, but every lower `Modal.Content`
remained an exposed `aria-modal` dialog with reachable controls and a live
iframe — so browse navigation could land in a dialog that is visually covered
and that itself claims the rest of the page is inert.

Non-top contents are now `inert`: out of the accessibility tree and out of
focus order, without unmounting, so each app keeps its bridge and its handshake
and is live the moment it becomes top.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-23 00:02:37 -04:00
commit beaaad4982
2 changed files with 13 additions and 0 deletions

View file

@ -216,6 +216,12 @@ describe("AppElicitationHost (#1854)", () => {
await flushAsync();
expect(screen.getAllByTestId("app-elicitation")).toHaveLength(2);
// The covered dialog is inert: out of the a11y tree and out of focus
// order, but still mounted — its app keeps its bridge and handshake.
const [lower, top] = screen.getAllByTestId("app-elicitation");
expect(lower.hasAttribute("inert")).toBe(true);
expect(top.hasAttribute("inert")).toBe(false);
await user.keyboard("{Escape}");
// Exactly one request is dismissed — the topmost, which is the last one.
expect(onFail).toHaveBeenCalledTimes(1);

View file

@ -253,6 +253,13 @@ function AppElicitationFrame({
only the request id tells those two dialogs apart — for
`landmark-unique` and for anyone navigating by screen reader. */}
<Modal.Content
// A lower dialog is visually covered but would otherwise stay an
// exposed `aria-modal` dialog with reachable controls and an iframe —
// so browse navigation could land in a dialog that itself claims the
// rest of the page is inert. `inert` takes it out of the a11y tree and
// out of focus order WITHOUT unmounting, so its app keeps its bridge
// and its handshake and is live the moment it becomes top.
inert={!isTop}
aria-label={`Elicitation ${entry.requestId} — ${entry.params.message} — rendered by ${entry.resourceUri}`}
data-app-elicitation-status={status}
data-testid="app-elicitation"