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:
parent
356e8eaa8a
commit
beaaad4982
2 changed files with 13 additions and 0 deletions
|
|
@ -216,6 +216,12 @@ describe("AppElicitationHost (#1854)", () => {
|
||||||
await flushAsync();
|
await flushAsync();
|
||||||
expect(screen.getAllByTestId("app-elicitation")).toHaveLength(2);
|
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}");
|
await user.keyboard("{Escape}");
|
||||||
// Exactly one request is dismissed — the topmost, which is the last one.
|
// Exactly one request is dismissed — the topmost, which is the last one.
|
||||||
expect(onFail).toHaveBeenCalledTimes(1);
|
expect(onFail).toHaveBeenCalledTimes(1);
|
||||||
|
|
|
||||||
|
|
@ -253,6 +253,13 @@ function AppElicitationFrame({
|
||||||
only the request id tells those two dialogs apart — for
|
only the request id tells those two dialogs apart — for
|
||||||
`landmark-unique` and for anyone navigating by screen reader. */}
|
`landmark-unique` and for anyone navigating by screen reader. */}
|
||||||
<Modal.Content
|
<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}`}
|
aria-label={`Elicitation ${entry.requestId} — ${entry.params.message} — rendered by ${entry.resourceUri}`}
|
||||||
data-app-elicitation-status={status}
|
data-app-elicitation-status={status}
|
||||||
data-testid="app-elicitation"
|
data-testid="app-elicitation"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue