mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-11-04 11:52:25 +00:00
Merge pull request #597 from PR0M3TH3AN/codex/extend-popoverengine-accessibility-features
Improve PopoverEngine menu accessibility and keyboard navigation
This commit is contained in:
2 files changed
+653
No files matched your search
@@ -9,9 +9,48 @@ import { ensureOverlayRoot } from "./overlayRoot.js";
|
||||
const DEFAULT_PLACEMENT = "bottom-start";
|
||||
const DEFAULT_STRATEGY = "fixed";
|
||||
const DEFAULT_MAX_WIDTH_TOKEN = "--popover-inline-safe-max";
|
||||
const MENU_TYPEAHEAD_RESET_MS = 500;
|
||||
|
||||
let popoverPanelIdCounter = 0;
|
||||
|
||||
let activePopoverInstance = null;
|
||||
|
||||
function generatePanelId() {
|
||||
popoverPanelIdCounter += 1;
|
||||
return `popover-panel-${popoverPanelIdCounter}`;
|
||||
}
|
||||
|
||||
function isDisabled(element) {
|
||||
if (!element) {
|
||||
return true;
|
||||
}
|
||||
if (element.hasAttribute("disabled")) {
|
||||
return true;
|
||||
}
|
||||
const ariaDisabled = element.getAttribute("aria-disabled");
|
||||
if (typeof ariaDisabled === "string" && ariaDisabled.toLowerCase() === "true") {
|
||||
return true;
|
||||
}
|
||||
if (element.hidden || element.getAttribute("aria-hidden") === "true") {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function getItemLabel(element) {
|
||||
if (!element) {
|
||||
return "";
|
||||
}
|
||||
const ariaLabel = element.getAttribute("aria-label");
|
||||
if (ariaLabel) {
|
||||
return ariaLabel.trim();
|
||||
}
|
||||
if (typeof element.textContent === "string") {
|
||||
return element.textContent.trim();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function isElement(node) {
|
||||
return Boolean(node && typeof node === "object" && node.nodeType === 1);
|
||||
}
|
||||
@@ -225,6 +264,11 @@ export function createPopover(trigger, render, options = {}) {
|
||||
const anchor = isElement(trigger) ? trigger : null;
|
||||
const documentRef = resolveDocument(anchor, options.document);
|
||||
|
||||
if (anchor && typeof anchor.setAttribute === "function") {
|
||||
anchor.setAttribute("aria-haspopup", "menu");
|
||||
anchor.setAttribute("aria-expanded", "false");
|
||||
}
|
||||
|
||||
const portal = documentRef ? documentRef.createElement("div") : null;
|
||||
if (portal) {
|
||||
portal.dataset.popoverPortal = "true";
|
||||
@@ -258,6 +302,344 @@ export function createPopover(trigger, render, options = {}) {
|
||||
let autoUpdateCleanup = null;
|
||||
let isOpen = false;
|
||||
let previousActiveElement = null;
|
||||
let menuItemIdCounter = 0;
|
||||
const menuHandlers = {
|
||||
keydown: null,
|
||||
focusin: null,
|
||||
};
|
||||
const menuState = {
|
||||
panel: null,
|
||||
items: [],
|
||||
activeIndex: -1,
|
||||
typeaheadBuffer: "",
|
||||
typeaheadTimeout: null,
|
||||
};
|
||||
|
||||
function getView() {
|
||||
return documentRef?.defaultView || globalThis;
|
||||
}
|
||||
|
||||
function resetTypeaheadBuffer() {
|
||||
const view = getView();
|
||||
if (menuState.typeaheadTimeout) {
|
||||
view.clearTimeout(menuState.typeaheadTimeout);
|
||||
}
|
||||
menuState.typeaheadTimeout = null;
|
||||
menuState.typeaheadBuffer = "";
|
||||
}
|
||||
|
||||
function scheduleTypeaheadReset() {
|
||||
const view = getView();
|
||||
if (!view || typeof view.setTimeout !== "function") {
|
||||
return;
|
||||
}
|
||||
if (menuState.typeaheadTimeout) {
|
||||
view.clearTimeout(menuState.typeaheadTimeout);
|
||||
}
|
||||
menuState.typeaheadTimeout = view.setTimeout(() => {
|
||||
menuState.typeaheadTimeout = null;
|
||||
menuState.typeaheadBuffer = "";
|
||||
}, MENU_TYPEAHEAD_RESET_MS);
|
||||
}
|
||||
|
||||
function ensurePanelId(panelElement) {
|
||||
if (!panelElement.id) {
|
||||
panelElement.id = generatePanelId();
|
||||
}
|
||||
return panelElement.id;
|
||||
}
|
||||
|
||||
function findMenuItems(panelElement) {
|
||||
if (!panelElement) {
|
||||
return [];
|
||||
}
|
||||
const candidates = Array.from(
|
||||
panelElement.querySelectorAll("[role=\"menuitem\"], .menu__item"),
|
||||
).filter((node) => isElement(node));
|
||||
|
||||
candidates.forEach((item) => {
|
||||
if (!item.hasAttribute("role")) {
|
||||
item.setAttribute("role", "menuitem");
|
||||
}
|
||||
if (!item.id) {
|
||||
menuItemIdCounter += 1;
|
||||
item.id = `${ensurePanelId(panelElement)}-item-${menuItemIdCounter}`;
|
||||
}
|
||||
item.setAttribute("tabindex", "-1");
|
||||
});
|
||||
|
||||
return candidates;
|
||||
}
|
||||
|
||||
function updateAriaActiveDescendant(panelElement, activeItem) {
|
||||
if (!panelElement) {
|
||||
return;
|
||||
}
|
||||
if (activeItem && activeItem.id) {
|
||||
panelElement.setAttribute("aria-activedescendant", activeItem.id);
|
||||
} else {
|
||||
panelElement.removeAttribute("aria-activedescendant");
|
||||
}
|
||||
}
|
||||
|
||||
function setActiveItem(index, { focus = true } = {}) {
|
||||
const panelElement = menuState.panel;
|
||||
if (!panelElement || !menuState.items.length) {
|
||||
menuState.activeIndex = -1;
|
||||
updateAriaActiveDescendant(panelElement, null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (index < 0 || index >= menuState.items.length) {
|
||||
menuState.activeIndex = -1;
|
||||
menuState.items.forEach((item) => {
|
||||
item.setAttribute("tabindex", "-1");
|
||||
});
|
||||
updateAriaActiveDescendant(panelElement, null);
|
||||
return;
|
||||
}
|
||||
|
||||
const target = menuState.items[index];
|
||||
if (!target || isDisabled(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
menuState.items.forEach((item, itemIndex) => {
|
||||
const value = itemIndex === index ? "0" : "-1";
|
||||
item.setAttribute("tabindex", value);
|
||||
});
|
||||
|
||||
menuState.activeIndex = index;
|
||||
updateAriaActiveDescendant(panelElement, target);
|
||||
|
||||
if (focus) {
|
||||
try {
|
||||
target.focus({ preventScroll: true });
|
||||
} catch (error) {
|
||||
target.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function focusFirstEnabledItem({ focusPanelFallback = true } = {}) {
|
||||
const panelElement = menuState.panel;
|
||||
if (!panelElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const items = menuState.items;
|
||||
const index = items.findIndex((item) => !isDisabled(item));
|
||||
if (index >= 0) {
|
||||
setActiveItem(index);
|
||||
return;
|
||||
}
|
||||
|
||||
menuState.activeIndex = -1;
|
||||
updateAriaActiveDescendant(panelElement, null);
|
||||
|
||||
if (focusPanelFallback) {
|
||||
panelElement.setAttribute("tabindex", panelElement.getAttribute("tabindex") || "-1");
|
||||
try {
|
||||
panelElement.focus({ preventScroll: true });
|
||||
} catch (error) {
|
||||
panelElement.focus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findNextEnabled(currentIndex, direction) {
|
||||
const { items } = menuState;
|
||||
if (!items.length) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
const total = items.length;
|
||||
let steps = 0;
|
||||
let index = currentIndex;
|
||||
|
||||
while (steps < total) {
|
||||
index = (index + direction + total) % total;
|
||||
const candidate = items[index];
|
||||
if (candidate && !isDisabled(candidate)) {
|
||||
return index;
|
||||
}
|
||||
steps += 1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function moveFocus(direction) {
|
||||
const startIndex = menuState.activeIndex >= 0 ? menuState.activeIndex : 0;
|
||||
const nextIndex = findNextEnabled(startIndex, direction);
|
||||
if (nextIndex >= 0) {
|
||||
setActiveItem(nextIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function focusEdge(direction) {
|
||||
const items = menuState.items;
|
||||
if (!items.length) {
|
||||
return;
|
||||
}
|
||||
const iterate = direction > 0 ? items : [...items].reverse();
|
||||
for (let idx = 0; idx < iterate.length; idx += 1) {
|
||||
const item = iterate[idx];
|
||||
if (!isDisabled(item)) {
|
||||
const originalIndex = items.indexOf(item);
|
||||
if (originalIndex >= 0) {
|
||||
setActiveItem(originalIndex);
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findMatchFrom(query, startIndex) {
|
||||
const { items } = menuState;
|
||||
if (!items.length || !query) {
|
||||
return -1;
|
||||
}
|
||||
const total = items.length;
|
||||
for (let offset = 0; offset < total; offset += 1) {
|
||||
const index = (startIndex + offset) % total;
|
||||
const item = items[index];
|
||||
if (!item || isDisabled(item)) {
|
||||
continue;
|
||||
}
|
||||
const label = getItemLabel(item).toLowerCase();
|
||||
if (label && label.startsWith(query)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
function handleTypeahead(char) {
|
||||
if (!char) {
|
||||
return;
|
||||
}
|
||||
menuState.typeaheadBuffer += char;
|
||||
scheduleTypeaheadReset();
|
||||
|
||||
const startIndex = menuState.activeIndex >= 0 ? menuState.activeIndex + 1 : 0;
|
||||
let matchIndex = findMatchFrom(menuState.typeaheadBuffer, startIndex);
|
||||
|
||||
if (matchIndex < 0 && menuState.typeaheadBuffer.length > 1) {
|
||||
matchIndex = findMatchFrom(char, startIndex);
|
||||
}
|
||||
|
||||
if (matchIndex >= 0) {
|
||||
setActiveItem(matchIndex);
|
||||
}
|
||||
}
|
||||
|
||||
function handleMenuKeydown(event) {
|
||||
if (!menuState.panel || !containsTarget(menuState.panel, event.target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
menuState.items = findMenuItems(menuState.panel);
|
||||
|
||||
switch (event.key) {
|
||||
case "ArrowDown":
|
||||
case "ArrowRight":
|
||||
event.preventDefault();
|
||||
moveFocus(1);
|
||||
return;
|
||||
case "ArrowUp":
|
||||
case "ArrowLeft":
|
||||
event.preventDefault();
|
||||
moveFocus(-1);
|
||||
return;
|
||||
case "Home":
|
||||
event.preventDefault();
|
||||
focusEdge(1);
|
||||
return;
|
||||
case "End":
|
||||
event.preventDefault();
|
||||
focusEdge(-1);
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
if (
|
||||
event.key &&
|
||||
event.key.length === 1 &&
|
||||
!event.altKey &&
|
||||
!event.ctrlKey &&
|
||||
!event.metaKey
|
||||
) {
|
||||
const char = event.key.toLowerCase();
|
||||
if (char.trim()) {
|
||||
event.preventDefault();
|
||||
handleTypeahead(char);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function handleMenuFocusIn(event) {
|
||||
if (!menuState.panel || !containsTarget(menuState.panel, event.target)) {
|
||||
return;
|
||||
}
|
||||
menuState.items = findMenuItems(menuState.panel);
|
||||
const index = menuState.items.indexOf(event.target);
|
||||
if (index >= 0 && !isDisabled(event.target)) {
|
||||
setActiveItem(index, { focus: false });
|
||||
}
|
||||
}
|
||||
|
||||
function ensureMenu(panelElement) {
|
||||
if (!panelElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (menuState.panel && menuState.panel !== panelElement) {
|
||||
if (menuHandlers.keydown) {
|
||||
menuState.panel.removeEventListener("keydown", menuHandlers.keydown);
|
||||
}
|
||||
if (menuHandlers.focusin) {
|
||||
menuState.panel.removeEventListener("focusin", menuHandlers.focusin);
|
||||
}
|
||||
}
|
||||
|
||||
menuState.panel = panelElement;
|
||||
ensurePanelId(panelElement);
|
||||
|
||||
panelElement.setAttribute("role", "menu");
|
||||
if (!panelElement.hasAttribute("tabindex")) {
|
||||
panelElement.setAttribute("tabindex", "-1");
|
||||
}
|
||||
|
||||
menuState.items = findMenuItems(panelElement);
|
||||
menuState.activeIndex = -1;
|
||||
updateAriaActiveDescendant(panelElement, null);
|
||||
resetTypeaheadBuffer();
|
||||
|
||||
if (!menuHandlers.keydown) {
|
||||
menuHandlers.keydown = (event) => handleMenuKeydown(event);
|
||||
}
|
||||
if (!menuHandlers.focusin) {
|
||||
menuHandlers.focusin = (event) => handleMenuFocusIn(event);
|
||||
}
|
||||
|
||||
panelElement.addEventListener("keydown", menuHandlers.keydown);
|
||||
panelElement.addEventListener("focusin", menuHandlers.focusin);
|
||||
}
|
||||
|
||||
function teardownMenu() {
|
||||
const panelElement = menuState.panel;
|
||||
if (panelElement && menuHandlers.keydown) {
|
||||
panelElement.removeEventListener("keydown", menuHandlers.keydown);
|
||||
}
|
||||
if (panelElement && menuHandlers.focusin) {
|
||||
panelElement.removeEventListener("focusin", menuHandlers.focusin);
|
||||
}
|
||||
resetTypeaheadBuffer();
|
||||
menuState.panel = null;
|
||||
menuState.items = [];
|
||||
menuState.activeIndex = -1;
|
||||
}
|
||||
|
||||
function applyPanelTokens() {
|
||||
if (!panel) {
|
||||
@@ -317,6 +699,13 @@ export function createPopover(trigger, render, options = {}) {
|
||||
portal.appendChild(panel);
|
||||
}
|
||||
|
||||
ensurePanelId(panel);
|
||||
if (anchor && typeof anchor.setAttribute === "function") {
|
||||
anchor.setAttribute("aria-controls", panel.id);
|
||||
}
|
||||
|
||||
ensureMenu(panel);
|
||||
|
||||
arrowElement = resolveArrow(options.arrow, panel) || arrowElement;
|
||||
|
||||
applyPanelTokens();
|
||||
@@ -529,6 +918,9 @@ export function createPopover(trigger, render, options = {}) {
|
||||
|
||||
await updatePosition();
|
||||
|
||||
ensureMenu(panelElement);
|
||||
focusFirstEnabledItem();
|
||||
|
||||
isOpen = true;
|
||||
activePopoverInstance = api;
|
||||
return true;
|
||||
@@ -554,9 +946,16 @@ export function createPopover(trigger, render, options = {}) {
|
||||
panel.setAttribute("aria-hidden", "true");
|
||||
panel.hidden = true;
|
||||
panel.dataset.popoverState = "closed";
|
||||
updateAriaActiveDescendant(panel, null);
|
||||
if (menuState.items.length) {
|
||||
menuState.items.forEach((item) => {
|
||||
item.setAttribute("tabindex", "-1");
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
setExpandedAttribute(anchor, false);
|
||||
resetTypeaheadBuffer();
|
||||
|
||||
if (restoreFocus && restoreFocusOnClose) {
|
||||
restoreTriggerFocus();
|
||||
@@ -601,6 +1000,8 @@ export function createPopover(trigger, render, options = {}) {
|
||||
function destroy() {
|
||||
close({ restoreFocus: false });
|
||||
|
||||
teardownMenu();
|
||||
|
||||
if (panel && panel.parentNode === portal) {
|
||||
portal.removeChild(panel);
|
||||
}
|
||||
@@ -610,6 +1011,12 @@ export function createPopover(trigger, render, options = {}) {
|
||||
|
||||
panel = null;
|
||||
arrowElement = null;
|
||||
|
||||
if (anchor) {
|
||||
anchor.removeAttribute("aria-controls");
|
||||
anchor.removeAttribute("aria-expanded");
|
||||
anchor.removeAttribute("aria-haspopup");
|
||||
}
|
||||
}
|
||||
|
||||
const api = {
|
||||
|
||||
@@ -110,6 +110,9 @@ test("opens a popover in the overlay root and positions the panel", async () =>
|
||||
{ document: documentRef },
|
||||
);
|
||||
|
||||
assert.equal(trigger.getAttribute("aria-haspopup"), "menu");
|
||||
assert.equal(trigger.getAttribute("aria-expanded"), "false");
|
||||
|
||||
await popover.open();
|
||||
|
||||
const overlayRoot = documentRef.getElementById("uiOverlay");
|
||||
@@ -119,11 +122,14 @@ test("opens a popover in the overlay root and positions the panel", async () =>
|
||||
|
||||
const panel = documentRef.getElementById("popover-panel");
|
||||
assert.ok(panel, "panel should be rendered");
|
||||
assert.equal(trigger.getAttribute("aria-controls"), panel.id);
|
||||
assert.equal(panel.dataset.popoverState, "open");
|
||||
assert.equal(panel.dataset.popoverPlacement, "bottom-start");
|
||||
assert.equal(panel.style.position, "fixed");
|
||||
assert.equal(panel.style.left, "100px");
|
||||
assert.equal(panel.style.top, "148px");
|
||||
assert.equal(panel.getAttribute("role"), "menu");
|
||||
assert.equal(documentRef.activeElement, panel);
|
||||
assert.equal(trigger.getAttribute("aria-expanded"), "true");
|
||||
|
||||
popover.close();
|
||||
@@ -186,6 +192,246 @@ test("closes on outside pointer events and restores focus", async () => {
|
||||
popover.destroy();
|
||||
});
|
||||
|
||||
test("supports roving focus, home/end navigation, and typeahead", async () => {
|
||||
const trigger = documentRef.getElementById("trigger");
|
||||
trigger.setAttribute("tabindex", "0");
|
||||
|
||||
setupBoundingClientRect(trigger, {
|
||||
x: 40,
|
||||
y: 40,
|
||||
top: 40,
|
||||
bottom: 80,
|
||||
left: 40,
|
||||
right: 80,
|
||||
width: 40,
|
||||
height: 40,
|
||||
});
|
||||
|
||||
const popover = createPopover(
|
||||
trigger,
|
||||
({ container }) => {
|
||||
const panel = documentRef.createElement("div");
|
||||
panel.className = "popover__panel";
|
||||
setupBoundingClientRect(panel, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
top: 0,
|
||||
bottom: 120,
|
||||
left: 0,
|
||||
right: 200,
|
||||
width: 200,
|
||||
height: 120,
|
||||
});
|
||||
|
||||
const list = documentRef.createElement("div");
|
||||
panel.appendChild(list);
|
||||
|
||||
const disabled = documentRef.createElement("button");
|
||||
disabled.className = "menu__item";
|
||||
disabled.type = "button";
|
||||
disabled.textContent = "Disabled";
|
||||
disabled.disabled = true;
|
||||
list.appendChild(disabled);
|
||||
|
||||
const copy = documentRef.createElement("button");
|
||||
copy.className = "menu__item";
|
||||
copy.type = "button";
|
||||
copy.textContent = "Copy link";
|
||||
list.appendChild(copy);
|
||||
|
||||
const deleteBtn = documentRef.createElement("button");
|
||||
deleteBtn.className = "menu__item";
|
||||
deleteBtn.type = "button";
|
||||
deleteBtn.textContent = "Delete video";
|
||||
list.appendChild(deleteBtn);
|
||||
|
||||
const share = documentRef.createElement("button");
|
||||
share.className = "menu__item";
|
||||
share.type = "button";
|
||||
share.textContent = "Share";
|
||||
list.appendChild(share);
|
||||
|
||||
container.appendChild(panel);
|
||||
return panel;
|
||||
},
|
||||
{ document: documentRef },
|
||||
);
|
||||
|
||||
await popover.open();
|
||||
|
||||
const panel = documentRef.querySelector(".popover__panel");
|
||||
const items = panel.querySelectorAll(".menu__item");
|
||||
const [disabled, copy, deleteBtn, share] = items;
|
||||
|
||||
assert.equal(documentRef.activeElement, copy);
|
||||
assert.equal(panel.getAttribute("aria-activedescendant"), copy.id);
|
||||
assert.equal(copy.getAttribute("tabindex"), "0");
|
||||
assert.equal(disabled.getAttribute("tabindex"), "-1");
|
||||
|
||||
copy.dispatchEvent(
|
||||
new windowRef.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
|
||||
);
|
||||
assert.equal(documentRef.activeElement, deleteBtn);
|
||||
assert.equal(panel.getAttribute("aria-activedescendant"), deleteBtn.id);
|
||||
|
||||
deleteBtn.dispatchEvent(
|
||||
new windowRef.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
|
||||
);
|
||||
assert.equal(documentRef.activeElement, share);
|
||||
|
||||
share.dispatchEvent(
|
||||
new windowRef.KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true }),
|
||||
);
|
||||
assert.equal(documentRef.activeElement, copy);
|
||||
|
||||
copy.dispatchEvent(
|
||||
new windowRef.KeyboardEvent("keydown", { key: "ArrowUp", bubbles: true }),
|
||||
);
|
||||
assert.equal(documentRef.activeElement, share);
|
||||
|
||||
share.dispatchEvent(
|
||||
new windowRef.KeyboardEvent("keydown", { key: "Home", bubbles: true }),
|
||||
);
|
||||
assert.equal(documentRef.activeElement, copy);
|
||||
|
||||
copy.dispatchEvent(
|
||||
new windowRef.KeyboardEvent("keydown", { key: "End", bubbles: true }),
|
||||
);
|
||||
assert.equal(documentRef.activeElement, share);
|
||||
|
||||
share.dispatchEvent(
|
||||
new windowRef.KeyboardEvent("keydown", { key: "d", bubbles: true }),
|
||||
);
|
||||
assert.equal(documentRef.activeElement, deleteBtn);
|
||||
assert.equal(panel.getAttribute("aria-activedescendant"), deleteBtn.id);
|
||||
|
||||
popover.destroy();
|
||||
});
|
||||
|
||||
test("escape closes the popover and restores trigger focus", async () => {
|
||||
const trigger = documentRef.getElementById("trigger");
|
||||
trigger.setAttribute("tabindex", "0");
|
||||
|
||||
setupBoundingClientRect(trigger, {
|
||||
x: 60,
|
||||
y: 60,
|
||||
top: 60,
|
||||
bottom: 100,
|
||||
left: 60,
|
||||
right: 100,
|
||||
width: 40,
|
||||
height: 40,
|
||||
});
|
||||
|
||||
const popover = createPopover(
|
||||
trigger,
|
||||
({ container }) => {
|
||||
const panel = documentRef.createElement("div");
|
||||
panel.className = "popover__panel";
|
||||
setupBoundingClientRect(panel, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
top: 0,
|
||||
bottom: 90,
|
||||
left: 0,
|
||||
right: 180,
|
||||
width: 180,
|
||||
height: 90,
|
||||
});
|
||||
|
||||
const first = documentRef.createElement("button");
|
||||
first.className = "menu__item";
|
||||
first.type = "button";
|
||||
first.textContent = "First";
|
||||
panel.appendChild(first);
|
||||
|
||||
const second = documentRef.createElement("button");
|
||||
second.className = "menu__item";
|
||||
second.type = "button";
|
||||
second.textContent = "Second";
|
||||
panel.appendChild(second);
|
||||
|
||||
container.appendChild(panel);
|
||||
return panel;
|
||||
},
|
||||
{ document: documentRef },
|
||||
);
|
||||
|
||||
await popover.open();
|
||||
|
||||
const panel = documentRef.querySelector(".popover__panel");
|
||||
const first = panel.querySelector(".menu__item");
|
||||
assert.equal(documentRef.activeElement, first);
|
||||
|
||||
first.dispatchEvent(
|
||||
new windowRef.KeyboardEvent("keydown", { key: "Escape", bubbles: true }),
|
||||
);
|
||||
|
||||
assert.equal(popover.isOpen(), false);
|
||||
assert.equal(panel.dataset.popoverState, "closed");
|
||||
assert.equal(panel.getAttribute("aria-activedescendant"), null);
|
||||
assert.equal(documentRef.activeElement, trigger);
|
||||
|
||||
popover.destroy();
|
||||
});
|
||||
|
||||
test("close respects restoreFocus option for contextual menus", async () => {
|
||||
const trigger = documentRef.getElementById("trigger");
|
||||
trigger.setAttribute("tabindex", "0");
|
||||
|
||||
setupBoundingClientRect(trigger, {
|
||||
x: 80,
|
||||
y: 80,
|
||||
top: 80,
|
||||
bottom: 120,
|
||||
left: 80,
|
||||
right: 120,
|
||||
width: 40,
|
||||
height: 40,
|
||||
});
|
||||
|
||||
const popover = createPopover(
|
||||
trigger,
|
||||
({ container }) => {
|
||||
const panel = documentRef.createElement("div");
|
||||
panel.className = "popover__panel";
|
||||
setupBoundingClientRect(panel, {
|
||||
x: 0,
|
||||
y: 0,
|
||||
top: 0,
|
||||
bottom: 90,
|
||||
left: 0,
|
||||
right: 180,
|
||||
width: 180,
|
||||
height: 90,
|
||||
});
|
||||
|
||||
const action = documentRef.createElement("button");
|
||||
action.className = "menu__item";
|
||||
action.type = "button";
|
||||
action.textContent = "Action";
|
||||
panel.appendChild(action);
|
||||
|
||||
container.appendChild(panel);
|
||||
return panel;
|
||||
},
|
||||
{ document: documentRef },
|
||||
);
|
||||
|
||||
await popover.open();
|
||||
|
||||
const action = documentRef.querySelector(".menu__item");
|
||||
assert.equal(documentRef.activeElement, action);
|
||||
|
||||
popover.close({ restoreFocus: false });
|
||||
|
||||
assert.equal(popover.isOpen(), false);
|
||||
assert.notEqual(documentRef.activeElement, trigger);
|
||||
assert.equal(documentRef.activeElement, action);
|
||||
|
||||
popover.destroy();
|
||||
});
|
||||
|
||||
test("ensures only one popover is open at a time", async () => {
|
||||
const trigger = documentRef.getElementById("trigger");
|
||||
trigger.setAttribute("tabindex", "0");
|
||||
|
||||
Reference in new issue
Block a user