Files
SkyArtShop/website/public/assets/js/accessibility-enhanced.js

288 lines
9.0 KiB
JavaScript
Raw Normal View History

/**
* Accessibility Enhancements
* WCAG 2.1 AA Compliant
*/
(function () {
"use strict";
const A11y = {
init() {
this.addSkipLink();
this.enhanceFocusManagement();
this.addARIALabels();
this.improveKeyboardNav();
this.addLiveRegions();
this.enhanceFormAccessibility();
console.log("[A11y] Accessibility enhancements loaded");
},
// Add skip to main content link
addSkipLink() {
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.querySelector("#main-content, main");
if (main) {
main.setAttribute("tabindex", "-1");
main.focus();
}
});
document.body.insertBefore(skipLink, document.body.firstChild);
},
// Enhance focus management
enhanceFocusManagement() {
// Trap focus in modals
document.addEventListener("keydown", (e) => {
if (e.key !== "Tab") return;
const modal = document.querySelector(
'.modal.active, .dropdown[style*="display: flex"]'
);
if (!modal) return;
const focusable = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
if (focusable.length === 0) return;
const first = focusable[0];
const last = focusable[focusable.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
});
// Focus visible styles
const style = document.createElement("style");
style.textContent = `
*:focus-visible {
outline: 3px solid #667eea !important;
outline-offset: 2px !important;
}
button:focus-visible,
a:focus-visible,
input:focus-visible,
select:focus-visible,
textarea:focus-visible {
outline: 3px solid #667eea !important;
outline-offset: 2px !important;
}
`;
document.head.appendChild(style);
},
// Add ARIA labels to interactive elements
addARIALabels() {
// Cart button
const cartBtn = document.querySelector("#cart-btn");
if (cartBtn && !cartBtn.hasAttribute("aria-label")) {
cartBtn.setAttribute("aria-label", "Shopping cart");
cartBtn.setAttribute("aria-haspopup", "true");
}
// Wishlist button
const wishlistBtn = document.querySelector("#wishlist-btn");
if (wishlistBtn && !wishlistBtn.hasAttribute("aria-label")) {
wishlistBtn.setAttribute("aria-label", "Wishlist");
wishlistBtn.setAttribute("aria-haspopup", "true");
}
// Mobile menu toggle
const menuToggle = document.querySelector(".mobile-menu-toggle");
if (menuToggle && !menuToggle.hasAttribute("aria-label")) {
menuToggle.setAttribute("aria-label", "Open navigation menu");
menuToggle.setAttribute("aria-expanded", "false");
}
// Add ARIA labels to product cards
document.querySelectorAll(".product-card").forEach((card, index) => {
if (!card.hasAttribute("role")) {
card.setAttribute("role", "article");
}
const title = card.querySelector("h3, .product-title");
if (title && !title.id) {
title.id = `product-title-${index}`;
card.setAttribute("aria-labelledby", title.id);
}
});
// Add labels to icon-only buttons
document.querySelectorAll("button:not([aria-label])").forEach((btn) => {
const icon = btn.querySelector('i[class*="bi-"]');
if (icon && !btn.textContent.trim()) {
const iconClass = icon.className;
let label = "Button";
if (iconClass.includes("cart")) label = "Add to cart";
else if (iconClass.includes("heart")) label = "Add to wishlist";
else if (iconClass.includes("trash")) label = "Remove";
else if (iconClass.includes("plus")) label = "Increase";
else if (iconClass.includes("minus") || iconClass.includes("dash"))
label = "Decrease";
else if (iconClass.includes("close") || iconClass.includes("x"))
label = "Close";
btn.setAttribute("aria-label", label);
}
});
},
// Improve keyboard navigation
improveKeyboardNav() {
// Dropdown keyboard support
document.querySelectorAll("[data-dropdown-toggle]").forEach((toggle) => {
toggle.addEventListener("keydown", (e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
toggle.click();
}
});
});
// Product card keyboard navigation
document.querySelectorAll(".product-card").forEach((card) => {
const link = card.querySelector("a");
if (link) {
card.addEventListener("keydown", (e) => {
if (e.key === "Enter" && e.target === card) {
link.click();
}
});
}
});
// Quantity input keyboard support
document.querySelectorAll(".quantity-input").forEach((input) => {
input.addEventListener("keydown", (e) => {
if (e.key === "ArrowUp") {
e.preventDefault();
const newValue = parseInt(input.value || 1) + 1;
if (newValue <= 99) {
input.value = newValue;
input.dispatchEvent(new Event("change"));
}
} else if (e.key === "ArrowDown") {
e.preventDefault();
const newValue = parseInt(input.value || 1) - 1;
if (newValue >= 1) {
input.value = newValue;
input.dispatchEvent(new Event("change"));
}
}
});
});
},
// Add live regions for dynamic content
addLiveRegions() {
// Create announcement region
if (!document.querySelector("#a11y-announcements")) {
const announcer = document.createElement("div");
announcer.id = "a11y-announcements";
announcer.setAttribute("role", "status");
announcer.setAttribute("aria-live", "polite");
announcer.setAttribute("aria-atomic", "true");
announcer.className = "sr-only";
document.body.appendChild(announcer);
}
// Announce cart/wishlist updates
window.addEventListener("cart-updated", (e) => {
this.announce(`Cart updated. ${e.detail.length} items in cart.`);
});
window.addEventListener("wishlist-updated", (e) => {
this.announce(
`Wishlist updated. ${e.detail.length} items in wishlist.`
);
});
},
announce(message) {
const announcer = document.querySelector("#a11y-announcements");
if (announcer) {
announcer.textContent = "";
setTimeout(() => {
announcer.textContent = message;
}, 100);
}
},
// Enhance form accessibility
enhanceFormAccessibility() {
// Add required indicators
document
.querySelectorAll(
"input[required], select[required], textarea[required]"
)
.forEach((field) => {
const label = document.querySelector(`label[for="${field.id}"]`);
if (label && !label.querySelector(".required-indicator")) {
const indicator = document.createElement("span");
indicator.className = "required-indicator";
indicator.textContent = " *";
indicator.setAttribute("aria-label", "required");
label.appendChild(indicator);
}
});
// Add error message associations
document.querySelectorAll(".error-message").forEach((error, index) => {
if (!error.id) {
error.id = `error-${index}`;
}
const field = error.previousElementSibling;
if (
field &&
(field.tagName === "INPUT" ||
field.tagName === "SELECT" ||
field.tagName === "TEXTAREA")
) {
field.setAttribute("aria-describedby", error.id);
field.setAttribute("aria-invalid", "true");
}
});
// Add autocomplete attributes
document.querySelectorAll('input[type="email"]').forEach((field) => {
if (!field.hasAttribute("autocomplete")) {
field.setAttribute("autocomplete", "email");
}
});
document.querySelectorAll('input[type="tel"]').forEach((field) => {
if (!field.hasAttribute("autocomplete")) {
field.setAttribute("autocomplete", "tel");
}
});
},
};
// Initialize on DOM ready
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => A11y.init());
} else {
A11y.init();
}
// Export for external use
window.A11y = A11y;
})();