221 lines
6.8 KiB
JavaScript
221 lines
6.8 KiB
JavaScript
/**
|
|
* 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();
|
|
}
|
|
})();
|