Skip to content

Commit f684d20

Browse files
fix(core/ui): reset aria-expanded on outside click, track modal owner, add keyboard nav tests
Agent-Logs-Url: https://github.com/speced/respec/sessions/9e0d56a5-7a01-4bd6-b16f-2f421dc54807 Co-authored-by: marcoscaceres <870154+marcoscaceres@users.noreply.github.com>
1 parent 07e3515 commit f684d20

2 files changed

Lines changed: 96 additions & 2 deletions

File tree

src/core/ui.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ window.addEventListener("load", () => trapFocus(menu));
6060
let modal;
6161
/** @type {HTMLElement | null} */
6262
let overlay;
63+
/** @type {HTMLElement | null} */
64+
let modalOwner;
6365
/** @type {any[]} */
6466
const errors = [];
6567
/** @type {any[]} */
@@ -84,6 +86,7 @@ respecPill.addEventListener(
8486

8587
document.documentElement.addEventListener("click", () => {
8688
if (!menu.hidden) {
89+
respecPill.setAttribute("aria-expanded", "false");
8790
toggleMenu();
8891
}
8992
});
@@ -286,9 +289,11 @@ export const ui = {
286289
overlay = null;
287290
});
288291
}
289-
if (owner) {
290-
owner.setAttribute("aria-expanded", "false");
292+
const ownerElement = owner || modalOwner;
293+
if (ownerElement) {
294+
ownerElement.setAttribute("aria-expanded", "false");
291295
}
296+
modalOwner = null;
292297
if (!modal) return;
293298
modal.remove();
294299
modal = null;
@@ -302,6 +307,7 @@ export const ui = {
302307
freshModal(title, content, currentOwner) {
303308
if (modal) modal.remove();
304309
if (overlay) overlay.remove();
310+
modalOwner = currentOwner;
305311
overlay = html`<div id="respec-overlay" class="removeOnSave"></div>`;
306312
const id = `${currentOwner.id}-modal`;
307313
const headingId = `${id}-heading`;

tests/spec/core/ui-spec.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,94 @@ describe("Core - UI", () => {
3232
expect(window.getComputedStyle(menu).display).toBe("none");
3333
});
3434

35+
it("resets aria-expanded on pill when menu is closed by clicking outside", async () => {
36+
const doc = await makeRSDoc(makeStandardOps(), null, "display: block");
37+
const pill = doc.getElementById("respec-pill");
38+
const menu = doc.getElementById("respec-menu");
39+
40+
pill.click();
41+
await new Promise(resolve => setTimeout(resolve));
42+
expect(pill.getAttribute("aria-expanded")).toBe("true");
43+
44+
doc.body.click();
45+
await new Promise(resolve => setTimeout(resolve));
46+
expect(menu.hidden).toBe(true);
47+
expect(pill.getAttribute("aria-expanded")).toBe("false");
48+
});
49+
50+
it("closes menu via Escape key and resets aria-expanded", async () => {
51+
const doc = await makeRSDoc(makeStandardOps(), null, "display: block");
52+
const pill = doc.getElementById("respec-pill");
53+
const menu = doc.getElementById("respec-menu");
54+
55+
pill.click();
56+
await new Promise(resolve => setTimeout(resolve));
57+
expect(menu.hidden).toBe(false);
58+
59+
const escEvent = new doc.defaultView.KeyboardEvent("keydown", {
60+
key: "Escape",
61+
bubbles: true,
62+
});
63+
menu.dispatchEvent(escEvent);
64+
await new Promise(resolve => setTimeout(resolve));
65+
expect(menu.hidden).toBe(true);
66+
expect(pill.getAttribute("aria-expanded")).toBe("false");
67+
});
68+
69+
it("moves focus with ArrowDown and ArrowUp keys in menu", async () => {
70+
const doc = await makeRSDoc(makeStandardOps(), null, "display: block");
71+
const ui = doc.defaultView.respecUI;
72+
73+
const btn1 = ui.addCommand("Nav Command 1", () => {}, null, "");
74+
const btn2 = ui.addCommand("Nav Command 2", () => {}, null, "");
75+
76+
doc.getElementById("respec-pill").click();
77+
await new Promise(resolve => setTimeout(resolve));
78+
79+
btn1.focus();
80+
const downEvent = new doc.defaultView.KeyboardEvent("keydown", {
81+
key: "ArrowDown",
82+
bubbles: true,
83+
});
84+
btn1.dispatchEvent(downEvent);
85+
expect(doc.activeElement).toBe(btn2);
86+
87+
const upEvent = new doc.defaultView.KeyboardEvent("keydown", {
88+
key: "ArrowUp",
89+
bubbles: true,
90+
});
91+
btn2.dispatchEvent(upEvent);
92+
expect(doc.activeElement).toBe(btn1);
93+
});
94+
95+
it("moves focus to first/last item with Home/End keys in menu", async () => {
96+
const doc = await makeRSDoc(makeStandardOps(), null, "display: block");
97+
const ui = doc.defaultView.respecUI;
98+
99+
const btn1 = ui.addCommand("Home End Cmd A", () => {}, null, "");
100+
const btn2 = ui.addCommand("Home End Cmd B", () => {}, null, "");
101+
const btn3 = ui.addCommand("Home End Cmd C", () => {}, null, "");
102+
103+
doc.getElementById("respec-pill").click();
104+
await new Promise(resolve => setTimeout(resolve));
105+
106+
btn2.focus();
107+
const homeEvent = new doc.defaultView.KeyboardEvent("keydown", {
108+
key: "Home",
109+
bubbles: true,
110+
});
111+
btn2.dispatchEvent(homeEvent);
112+
expect(doc.activeElement).toBe(btn1);
113+
114+
btn2.focus();
115+
const endEvent = new doc.defaultView.KeyboardEvent("keydown", {
116+
key: "End",
117+
bubbles: true,
118+
});
119+
btn2.dispatchEvent(endEvent);
120+
expect(doc.activeElement).toBe(btn3);
121+
});
122+
35123
it("shows errors", async () => {
36124
const doc = await makeRSDoc(makeStandardOps({ group: "webapps" }));
37125
const ui = doc.defaultView.respecUI;

0 commit comments

Comments
 (0)