UNPKG

@cocreate/aria

Version:

Chain multiple component executions to generate your desired logic, when one action is complete next one will start. The sequence goes until all aria have been completed. Vanilla javascript, easily configured using HTML5 attributes and/or JavaScript API.

204 lines (181 loc) 5.85 kB
function init(elements) { if (!elements) { elements = document.querySelectorAll("[aria-controls]"); // initDocument(); } initElement(elements); } let popupListener = null; function addPopupListener() { if (!popupListener) { popupListener = function (event) { const hasPopUps = document.querySelectorAll( '[aria-controls][aria-haspopup][aria-expanded="true"]' ); let skipControlledId = null; for (let hasPopUp of hasPopUps) { const controlledId = hasPopUp.getAttribute("aria-controls"); if (skipControlledId === controlledId) { continue; // Skip this controlledId if it was already processed } if (hasPopUp.contains(event.target)) { skipControlledId = controlledId; continue; // Ignore clicks inside the popup } const controlledElement = document.getElementById(controlledId); let closeOn = controlledElement.getAttribute("aria-close-on"); let closeOnEl = controlledElement; if (!closeOn) { closeOnEl = event.target.closest( `#${controlledId} [aria-close-on]` ); if (closeOnEl) { closeOn = closeOnEl.getAttribute("aria-close-on"); } } if (closeOn === "outside") { if (closeOnEl.contains(event.target)) continue; } else if (closeOn === "button" || closeOn === "btn") { continue; } controlledElement.classList.remove("show"); updateAllControls(controlledId, "false"); } // Remove listener if no popups remain open if ( !document.querySelector( '[aria-controls][aria-haspopup][aria-expanded="true"]' ) ) { document.removeEventListener("click", popupListener, true); popupListener = null; } }; document.addEventListener("click", popupListener, true); } } function removePopupListener() { if (popupListener) { document.removeEventListener("click", popupListener, true); popupListener = null; } } const initialized = new Set(); function initElement(elements) { if ( !Array.isArray(elements) && !(elements instanceof NodeList) && !(elements instanceof HTMLCollection) ) { elements = [elements]; } if (elements.length === 0) { return; } for (let control of elements) { if (initialized.has(control)) continue; initialized.add(control); initEscapeKey(control); control.addEventListener("click", function (event) { event.preventDefault(); // Prevent default link behavior for <a> tags const controlledId = this.getAttribute("aria-controls"); const controlledElement = document.getElementById(controlledId); if (!controlledElement) { console.warn( `ARIA Controls: No element found with ID "${controlledId}" controlled by`, this ); return; } const closeOn = controlledElement.getAttribute("aria-close-on"); const role = this.getAttribute("role"); const hasAriaOpen = this.hasAttribute("aria-open"); const hasAriaClose = this.hasAttribute("aria-close"); const expanded = this.getAttribute("aria-expanded"); // Apply aria-open and aria-close logic globally, before any role-specific logic if (hasAriaOpen && expanded === "true") { // Do nothing if already open and aria-open is set return; } if (hasAriaClose && expanded !== "true") { // Do nothing if already closed and aria-close is set return; } if (role === "tab") { const tablist = this.closest("[role='tablist']"); const tabs = tablist.querySelectorAll('[role="tab"]'); for (let tab of tabs) { const tabControlledId = tab.getAttribute("aria-controls"); const tabControlledEl = document.getElementById(tabControlledId); if (this === tab) { tab.setAttribute("aria-selected", "true"); tabControlledEl.classList.add("show"); } else { tab.setAttribute("aria-selected", "false"); tabControlledEl.classList.remove("show"); } } } else { // Default toggle logic if (expanded === "true") { controlledElement.classList.remove("show"); updateAllControls(controlledId, "false"); removePopupListener(); } else { controlledElement.classList.add("show"); updateAllControls(controlledId, "true"); if (closeOn !== "btn" && closeOn !== "button") { addPopupListener(); } } } }); } } function updateAllControls(controlledId, state) { const allControls = document.querySelectorAll( `[aria-controls="${controlledId}"]` ); allControls.forEach((ctrl) => ctrl.setAttribute("aria-expanded", state)); } function initEscapeKey(control) { const controlledId = control.getAttribute("aria-controls"); const controlledElement = document.getElementById(controlledId); if (controlledElement) { control.addEventListener("keydown", handleEscapeKey); controlledElement.addEventListener("keydown", handleEscapeKey); } } function handleEscapeKey(event) { if (event.key === "Escape") { // Use currentTarget to reference the element the listener is attached to const toggleButton = event.currentTarget.matches("[aria-controls]") ? event.currentTarget : document.querySelector( `[aria-controls="${event.currentTarget.id}"]` ); if (toggleButton) { toggleButton.click(); } } } // Attach Escape key handler globally document.addEventListener("keydown", handleEscapeKey); CoCreate.observer.init({ name: "aria", types: ["addedNodes"], selector: "[aria-controls]", callback: function (mutation) { initElement(mutation.target); } }); // CoCreate.observer.init({ // name: "aria-attributes", // types: ["attributes"], // attributeFilters: ["aria-selected"], // callback: function (mutation) { // initElement(mutation.target); // } // }); init();