From ca0a3f05277b5dd04f4b2fbc85a79bdb0de5b882 Mon Sep 17 00:00:00 2001 From: andreivieru Date: Wed, 4 Nov 2026 22:51:27 -0500 Subject: [PATCH] Restore tracked aria-hidden attributes when disposing modalizer Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 37912007-07c2-40e4-81bd-df708aa99fe2 --- src/Modalizer.ts | 7 ++ tests/ModalizerDispose.test.tsx | 157 ++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 tests/ModalizerDispose.test.tsx diff --git a/src/Modalizer.ts b/src/Modalizer.ts index 9fcf3d18..7ee65168 100644 --- a/src/Modalizer.ts +++ b/src/Modalizer.ts @@ -401,6 +401,13 @@ export class ModalizerAPI implements Types.ModalizerAPI { delete this.activeId; this.activeElements = []; + this._aug.forEach((reference) => { + const element = reference.get(); + if (element) { + augmentAttribute(this._tabster, element, "aria-hidden"); + } + }); + this._augMap = new WeakMap(); this._aug = []; diff --git a/tests/ModalizerDispose.test.tsx b/tests/ModalizerDispose.test.tsx new file mode 100644 index 00000000..59b06de3 --- /dev/null +++ b/tests/ModalizerDispose.test.tsx @@ -0,0 +1,157 @@ +/*! + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import * as React from "react"; +import { getTabsterAttribute } from "tabster"; +import * as BroTest from "./utils/BroTest"; + +describe("Modalizer disposal", () => { + beforeEach(async () => { + await BroTest.bootstrapTabsterPage({ modalizer: true }); + }); + + const fixture = (original?: "false" | "true") => ( +
+
+ +
+
+ +
+
+ ); + + it.each([undefined, "false", "true"] as const)( + "restores the original aria-hidden value on last-wrapper disposal (%s)", + async (original) => { + await new BroTest.BroTest(fixture(original)) + .focusElement("#modal-button") + .wait(300) + .eval(() => { + const vars = getTabsterTestVariables(); + const outside = vars.dom!.getElementById( + document, + "outside" + )!; + const handle = vars.core!; + const borrowed = vars.getTabster!(window)!; + const hidden = outside.getAttribute("aria-hidden"); + + vars.disposeTabster!(handle); + + return { + hidden, + restored: outside.getAttribute("aria-hidden"), + borrowedSharesCore: borrowed.core === handle.core, + newBorrowedHandle: !!vars.getTabster!(window), + }; + }) + .check((result) => { + expect(result).toEqual({ + hidden: "true", + restored: original ?? null, + borrowedSharesCore: true, + newBorrowedHandle: false, + }); + }); + } + ); + + it.each([undefined, "false"] as const)( + "does not restore until the final counted wrapper is disposed (%s)", + async (original) => { + await new BroTest.BroTest(fixture(original)) + .focusElement("#modal-button") + .wait(300) + .eval(() => { + const vars = getTabsterTestVariables(); + const outside = vars.dom!.getElementById( + document, + "outside" + )!; + const keeper = vars.createTabster!(window); + vars.disposeTabster!(vars.core!); + const retained = outside.getAttribute("aria-hidden"); + const coreRetained = !!vars.getTabster!(window); + + vars.disposeTabster!(keeper); + + return { + retained, + coreRetained, + restored: outside.getAttribute("aria-hidden"), + coreGone: !vars.getTabster!(window), + }; + }) + .check((result) => { + expect(result).toEqual({ + retained: "true", + coreRetained: true, + restored: original ?? null, + coreGone: true, + }); + }); + } + ); + + it.each([undefined, "false"] as const)( + "restores a still-live detached element before storage is discarded (%s)", + async (original) => { + await new BroTest.BroTest(fixture(original)) + .focusElement("#modal-button") + .wait(300) + .eval(() => { + const vars = getTabsterTestVariables(); + const outside = vars.dom!.getElementById( + document, + "outside" + )!; + const hidden = outside.getAttribute("aria-hidden"); + outside.remove(); + vars.disposeTabster!(vars.core!); + + return { + hidden, + connected: outside.isConnected, + restored: outside.getAttribute("aria-hidden"), + }; + }) + .check((result) => { + expect(result).toEqual({ + hidden: "true", + connected: false, + restored: original ?? null, + }); + }); + } + ); + + it("preserves normal deactivation before disposal", async () => { + await new BroTest.BroTest(fixture("false")) + .focusElement("#modal-button") + .wait(300) + .eval(() => { + getTabsterTestVariables().modalizer!.setActive(undefined); + }) + .wait(300) + .eval(() => { + const vars = getTabsterTestVariables(); + const outside = vars.dom!.getElementById(document, "outside")!; + const deactivated = outside.getAttribute("aria-hidden"); + vars.disposeTabster!(vars.core!); + + return { + deactivated, + disposed: outside.getAttribute("aria-hidden"), + }; + }) + .check((result) => { + expect(result).toEqual({ + deactivated: "false", + disposed: "false", + }); + }); + }); +});