Files
SkyArtShop/website/public/assets/js/archive/accessibility.js

221 lines
6.8 KiB
JavaScript
Raw Normal View History

2026-01-04 17:52:37 -06:00
/**
* Accessibility Enhancements
* WCAG 2.1 AA Compliance Utilities
*/
(function () {
"use strict";
class AccessibilityManager {
constructor() {
this.init();
}
init() {
this.addSkipLinks();
this.enhanceFocusManagement();
this.addARIALabels();
this.setupKeyboardNavigation();
this.announceChanges();
}
// Add skip link to main content
addSkipLinks() {
if (document.querySelector(".skip-link")) return;
const skipLink = document.createElement("a");
skipLink.href = "#main-content";
skipLink.className = "skip-link";
skipLink.textContent = "Skip to main content";
skipLink.addEventListener("click", (e) => {
e.preventDefault();
const main =
document.getElementById("main-content") ||
document.querySelector("main");
if (main) {
main.setAttribute("tabindex", "-1");
main.focus();
main.removeAttribute("tabindex");
}
});
document.body.insertBefore(skipLink, document.body.firstChild);
}
// Enhance focus management
enhanceFocusManagement() {
// Track focus for modal/dropdown management
let lastFocusedElement = null;
document.addEventListener("focusin", (e) => {
const dropdown = e.target.closest(
'[role="dialog"], .cart-dropdown, .wishlist-dropdown'
);
if (dropdown && dropdown.classList.contains("active")) {
if (!lastFocusedElement) {
lastFocusedElement = document.activeElement;
}
}
});
// Return focus when dropdowns close
const observeDropdowns = () => {
const dropdowns = document.querySelectorAll(
".cart-dropdown, .wishlist-dropdown"
);
dropdowns.forEach((dropdown) => {
const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => {
if (mutation.attributeName === "class") {
if (
!dropdown.classList.contains("active") &&
lastFocusedElement
) {
lastFocusedElement.focus();
lastFocusedElement = null;
}
}
});
});
observer.observe(dropdown, { attributes: true });
});
};
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", observeDropdowns);
} else {
observeDropdowns();
}
}
// Add missing ARIA labels
addARIALabels() {
// Add labels to buttons without text
document
.querySelectorAll("button:not([aria-label]):not([aria-labelledby])")
.forEach((button) => {
if (button.textContent.trim() === "") {
const icon = button.querySelector('i[class*="bi-"]');
if (icon) {
const iconClass = Array.from(icon.classList).find((c) =>
c.startsWith("bi-")
);
if (iconClass) {
const label = iconClass.replace("bi-", "").replace(/-/g, " ");
button.setAttribute("aria-label", label);
}
}
}
});
// Ensure all images have alt text
document.querySelectorAll("img:not([alt])").forEach((img) => {
img.setAttribute("alt", "");
});
// Add role to navigation landmarks
document.querySelectorAll(".navbar, .modern-navbar").forEach((nav) => {
if (!nav.getAttribute("role")) {
nav.setAttribute("role", "navigation");
}
});
}
// Setup keyboard navigation
setupKeyboardNavigation() {
// Escape key closes dropdowns
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
const activeDropdown = document.querySelector(
".cart-dropdown.active, .wishlist-dropdown.active"
);
if (activeDropdown) {
const closeBtn = activeDropdown.querySelector(
".close-btn, [data-close]"
);
if (closeBtn) closeBtn.click();
}
}
});
// Tab trap in modal/dropdowns
document
.querySelectorAll(".cart-dropdown, .wishlist-dropdown")
.forEach((dropdown) => {
dropdown.addEventListener("keydown", (e) => {
if (e.key === "Tab" && dropdown.classList.contains("active")) {
const focusableElements = dropdown.querySelectorAll(
'a[href], button:not([disabled]), input:not([disabled]), [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement =
focusableElements[focusableElements.length - 1];
if (e.shiftKey && document.activeElement === firstElement) {
e.preventDefault();
lastElement.focus();
} else if (
!e.shiftKey &&
document.activeElement === lastElement
) {
e.preventDefault();
firstElement.focus();
}
}
});
});
}
// Announce dynamic changes to screen readers
announceChanges() {
// Create live region if it doesn't exist
let liveRegion = document.getElementById("aria-live-region");
if (!liveRegion) {
liveRegion = document.createElement("div");
liveRegion.id = "aria-live-region";
liveRegion.setAttribute("role", "status");
liveRegion.setAttribute("aria-live", "polite");
liveRegion.setAttribute("aria-atomic", "true");
liveRegion.className = "sr-only";
document.body.appendChild(liveRegion);
}
// Listen for cart/wishlist updates
window.addEventListener("cart-updated", () => {
const count = window.AppState?.getCartCount?.() || 0;
this.announce(
`Cart updated. ${count} item${count !== 1 ? "s" : ""} in cart.`
);
});
window.addEventListener("wishlist-updated", () => {
const count = window.AppState?.wishlist?.length || 0;
this.announce(
`Wishlist updated. ${count} item${
count !== 1 ? "s" : ""
} in wishlist.`
);
});
}
// Announce message to screen readers
announce(message) {
const liveRegion = document.getElementById("aria-live-region");
if (liveRegion) {
liveRegion.textContent = "";
setTimeout(() => {
liveRegion.textContent = message;
}, 100);
}
}
}
// Initialize accessibility manager
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
window.A11y = new AccessibilityManager();
});
} else {
window.A11y = new AccessibilityManager();
}
})();