webupdate
This commit is contained in:
616
website/public/assets/js/accessibility.js
Normal file
616
website/public/assets/js/accessibility.js
Normal file
@@ -0,0 +1,616 @@
|
||||
/**
|
||||
* Accessibility Enhancements
|
||||
* Adds ARIA labels, focus states, keyboard navigation, and screen reader support
|
||||
*/
|
||||
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
console.log("[Accessibility] Loading...");
|
||||
|
||||
const A11y = {
|
||||
init() {
|
||||
this.addARIALabels();
|
||||
this.enhanceKeyboardNavigation();
|
||||
this.addSkipLinks();
|
||||
this.enhanceFocusStates();
|
||||
this.announceLiveRegions();
|
||||
this.fixMobileAccessibility();
|
||||
console.log("[Accessibility] Initialized");
|
||||
},
|
||||
|
||||
// Add ARIA labels to interactive elements
|
||||
addARIALabels() {
|
||||
// Navbar
|
||||
const navbar = document.querySelector(".navbar");
|
||||
if (navbar) {
|
||||
navbar.setAttribute("role", "navigation");
|
||||
navbar.setAttribute("aria-label", "Main navigation");
|
||||
}
|
||||
|
||||
// Nav menu
|
||||
const navMenu = document.querySelector(".nav-menu");
|
||||
if (navMenu) {
|
||||
navMenu.setAttribute("role", "menubar");
|
||||
navMenu.setAttribute("aria-label", "Primary menu");
|
||||
|
||||
navMenu.querySelectorAll(".nav-link").forEach((link, index) => {
|
||||
link.setAttribute("role", "menuitem");
|
||||
link.setAttribute("tabindex", "0");
|
||||
});
|
||||
}
|
||||
|
||||
// Nav actions buttons
|
||||
const navActions = document.querySelector(".nav-actions");
|
||||
if (navActions) {
|
||||
navActions.setAttribute("role", "group");
|
||||
navActions.setAttribute("aria-label", "Account and cart actions");
|
||||
}
|
||||
|
||||
// Cart button
|
||||
const cartBtn = document.querySelector(".cart-btn");
|
||||
if (cartBtn) {
|
||||
cartBtn.setAttribute("aria-label", "Shopping cart");
|
||||
cartBtn.setAttribute("aria-haspopup", "dialog");
|
||||
const cartCount = cartBtn.querySelector(".cart-count");
|
||||
if (cartCount) {
|
||||
cartBtn.setAttribute("aria-describedby", "cart-count-desc");
|
||||
// Create hidden description for screen readers
|
||||
if (!document.getElementById("cart-count-desc")) {
|
||||
const desc = document.createElement("span");
|
||||
desc.id = "cart-count-desc";
|
||||
desc.className = "sr-only";
|
||||
desc.textContent = "items in cart";
|
||||
cartBtn.appendChild(desc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wishlist button
|
||||
const wishlistBtn = document.querySelector(".wishlist-btn-nav");
|
||||
if (wishlistBtn) {
|
||||
wishlistBtn.setAttribute("aria-label", "Wishlist");
|
||||
wishlistBtn.setAttribute("aria-haspopup", "dialog");
|
||||
}
|
||||
|
||||
// Sign in link
|
||||
const signinLink = document.querySelector('a[href="/signin"]');
|
||||
if (signinLink) {
|
||||
signinLink.setAttribute("aria-label", "Sign in to your account");
|
||||
}
|
||||
|
||||
// Mobile menu toggle
|
||||
const mobileToggle = document.querySelector(".nav-mobile-toggle");
|
||||
if (mobileToggle) {
|
||||
mobileToggle.setAttribute("aria-expanded", "false");
|
||||
mobileToggle.setAttribute("aria-controls", "nav-menu");
|
||||
mobileToggle.setAttribute("aria-label", "Toggle navigation menu");
|
||||
}
|
||||
|
||||
// Product cards
|
||||
document.querySelectorAll(".product-card").forEach((card, index) => {
|
||||
card.setAttribute("role", "article");
|
||||
const title = card.querySelector(".product-title, .product-name, h3");
|
||||
if (title) {
|
||||
card.setAttribute("aria-label", title.textContent.trim());
|
||||
}
|
||||
|
||||
// Quick view button
|
||||
const quickView = card.querySelector(
|
||||
'.quick-view-btn, [data-action="quick-view"]'
|
||||
);
|
||||
if (quickView) {
|
||||
quickView.setAttribute(
|
||||
"aria-label",
|
||||
`Quick view ${title ? title.textContent.trim() : "product"}`
|
||||
);
|
||||
}
|
||||
|
||||
// Add to cart button
|
||||
const addCart = card.querySelector(
|
||||
'.add-to-cart-btn, [data-action="add-to-cart"]'
|
||||
);
|
||||
if (addCart) {
|
||||
addCart.setAttribute(
|
||||
"aria-label",
|
||||
`Add ${title ? title.textContent.trim() : "product"} to cart`
|
||||
);
|
||||
}
|
||||
|
||||
// Wishlist button
|
||||
const wishlist = card.querySelector(
|
||||
'.wishlist-btn, [data-action="wishlist"]'
|
||||
);
|
||||
if (wishlist) {
|
||||
wishlist.setAttribute(
|
||||
"aria-label",
|
||||
`Add ${title ? title.textContent.trim() : "product"} to wishlist`
|
||||
);
|
||||
wishlist.setAttribute("aria-pressed", "false");
|
||||
}
|
||||
});
|
||||
|
||||
// Slider controls
|
||||
const sliderPrev = document.querySelector(".slider-arrow.prev");
|
||||
const sliderNext = document.querySelector(".slider-arrow.next");
|
||||
if (sliderPrev) sliderPrev.setAttribute("aria-label", "Previous slide");
|
||||
if (sliderNext) sliderNext.setAttribute("aria-label", "Next slide");
|
||||
|
||||
// Slider nav dots
|
||||
document
|
||||
.querySelectorAll(".slider-nav .dot, .slider-dot")
|
||||
.forEach((dot, index) => {
|
||||
dot.setAttribute("role", "tab");
|
||||
dot.setAttribute("aria-label", `Go to slide ${index + 1}`);
|
||||
dot.setAttribute(
|
||||
"aria-selected",
|
||||
dot.classList.contains("active") ? "true" : "false"
|
||||
);
|
||||
});
|
||||
|
||||
// Cart drawer
|
||||
const cartDrawer = document.querySelector(".cart-drawer");
|
||||
if (cartDrawer) {
|
||||
cartDrawer.setAttribute("role", "dialog");
|
||||
cartDrawer.setAttribute("aria-modal", "true");
|
||||
cartDrawer.setAttribute("aria-label", "Shopping cart");
|
||||
|
||||
const closeBtn = cartDrawer.querySelector(".cart-close, .close-cart");
|
||||
if (closeBtn) {
|
||||
closeBtn.setAttribute("aria-label", "Close cart");
|
||||
}
|
||||
}
|
||||
|
||||
// Wishlist drawer
|
||||
const wishlistDrawer = document.querySelector(".wishlist-drawer");
|
||||
if (wishlistDrawer) {
|
||||
wishlistDrawer.setAttribute("role", "dialog");
|
||||
wishlistDrawer.setAttribute("aria-modal", "true");
|
||||
wishlistDrawer.setAttribute("aria-label", "Wishlist");
|
||||
|
||||
const closeBtn = wishlistDrawer.querySelector(
|
||||
".wishlist-close, .close-wishlist"
|
||||
);
|
||||
if (closeBtn) {
|
||||
closeBtn.setAttribute("aria-label", "Close wishlist");
|
||||
}
|
||||
}
|
||||
|
||||
// Form inputs
|
||||
document
|
||||
.querySelectorAll("input:not([aria-label]):not([aria-labelledby])")
|
||||
.forEach((input) => {
|
||||
const label =
|
||||
input.closest("label") ||
|
||||
document.querySelector(`label[for="${input.id}"]`);
|
||||
if (label) {
|
||||
if (!input.id) {
|
||||
input.id = `input-${Math.random().toString(36).substr(2, 9)}`;
|
||||
label.setAttribute("for", input.id);
|
||||
}
|
||||
} else {
|
||||
const placeholder = input.getAttribute("placeholder");
|
||||
if (placeholder) {
|
||||
input.setAttribute("aria-label", placeholder);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Images without alt text
|
||||
document.querySelectorAll("img:not([alt])").forEach((img) => {
|
||||
img.setAttribute("alt", "");
|
||||
img.setAttribute("role", "presentation");
|
||||
});
|
||||
|
||||
// Footer
|
||||
const footer = document.querySelector("footer, .footer");
|
||||
if (footer) {
|
||||
footer.setAttribute("role", "contentinfo");
|
||||
}
|
||||
|
||||
// Main content
|
||||
const main = document.querySelector("main, .page-content");
|
||||
if (main) {
|
||||
main.setAttribute("role", "main");
|
||||
main.id = main.id || "main-content";
|
||||
}
|
||||
|
||||
// Sections
|
||||
document.querySelectorAll("section").forEach((section) => {
|
||||
const heading = section.querySelector("h1, h2, h3");
|
||||
if (heading) {
|
||||
section.setAttribute(
|
||||
"aria-labelledby",
|
||||
heading.id ||
|
||||
(heading.id = `heading-${Math.random()
|
||||
.toString(36)
|
||||
.substr(2, 9)}`)
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Enhance keyboard navigation
|
||||
enhanceKeyboardNavigation() {
|
||||
// ESC key to close modals/drawers
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Escape") {
|
||||
// Close cart drawer
|
||||
const cartDrawer = document.querySelector(
|
||||
".cart-drawer.open, .cart-drawer.active"
|
||||
);
|
||||
if (cartDrawer) {
|
||||
const closeBtn = cartDrawer.querySelector(
|
||||
".cart-close, .close-cart"
|
||||
);
|
||||
if (closeBtn) closeBtn.click();
|
||||
document.querySelector(".cart-btn")?.focus();
|
||||
}
|
||||
|
||||
// Close wishlist drawer
|
||||
const wishlistDrawer = document.querySelector(
|
||||
".wishlist-drawer.open, .wishlist-drawer.active"
|
||||
);
|
||||
if (wishlistDrawer) {
|
||||
const closeBtn = wishlistDrawer.querySelector(
|
||||
".wishlist-close, .close-wishlist"
|
||||
);
|
||||
if (closeBtn) closeBtn.click();
|
||||
document.querySelector(".wishlist-btn-nav")?.focus();
|
||||
}
|
||||
|
||||
// Close mobile menu
|
||||
const navMenu = document.querySelector(
|
||||
".nav-menu.open, .nav-menu.active"
|
||||
);
|
||||
if (navMenu) {
|
||||
document.querySelector(".nav-mobile-toggle")?.click();
|
||||
}
|
||||
|
||||
// Close modals
|
||||
const modal = document.querySelector(".modal.show, .modal.open");
|
||||
if (modal) {
|
||||
const closeBtn = modal.querySelector(
|
||||
'.modal-close, .close-modal, [data-dismiss="modal"]'
|
||||
);
|
||||
if (closeBtn) closeBtn.click();
|
||||
}
|
||||
|
||||
// Close user dropdown
|
||||
const userDropdown = document.querySelector(
|
||||
".user-dropdown-menu.show"
|
||||
);
|
||||
if (userDropdown) {
|
||||
userDropdown.classList.remove("show");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Arrow key navigation for nav menu
|
||||
const navLinks = document.querySelectorAll(".nav-menu .nav-link");
|
||||
navLinks.forEach((link, index) => {
|
||||
link.addEventListener("keydown", (e) => {
|
||||
if (e.key === "ArrowRight" || e.key === "ArrowDown") {
|
||||
e.preventDefault();
|
||||
const next = navLinks[index + 1] || navLinks[0];
|
||||
next.focus();
|
||||
} else if (e.key === "ArrowLeft" || e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
const prev = navLinks[index - 1] || navLinks[navLinks.length - 1];
|
||||
prev.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Enter/Space for clickable elements
|
||||
document
|
||||
.querySelectorAll('[role="button"], [role="tab"], .product-card')
|
||||
.forEach((el) => {
|
||||
if (!el.getAttribute("tabindex")) {
|
||||
el.setAttribute("tabindex", "0");
|
||||
}
|
||||
|
||||
el.addEventListener("keydown", (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
el.click();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Focus trap for modals/drawers
|
||||
this.setupFocusTrap(".cart-drawer");
|
||||
this.setupFocusTrap(".wishlist-drawer");
|
||||
this.setupFocusTrap(".modal");
|
||||
},
|
||||
|
||||
// Setup focus trap for modal-like elements
|
||||
setupFocusTrap(selector) {
|
||||
const container = document.querySelector(selector);
|
||||
if (!container) return;
|
||||
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.attributeName === "class") {
|
||||
const isOpen =
|
||||
container.classList.contains("open") ||
|
||||
container.classList.contains("active") ||
|
||||
container.classList.contains("show");
|
||||
|
||||
if (isOpen) {
|
||||
this.trapFocus(container);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
observer.observe(container, { attributes: true });
|
||||
},
|
||||
|
||||
// Trap focus within container
|
||||
trapFocus(container) {
|
||||
const focusableElements = container.querySelectorAll(
|
||||
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
|
||||
);
|
||||
|
||||
if (focusableElements.length === 0) return;
|
||||
|
||||
const firstElement = focusableElements[0];
|
||||
const lastElement = focusableElements[focusableElements.length - 1];
|
||||
|
||||
firstElement.focus();
|
||||
|
||||
container.addEventListener("keydown", function trapHandler(e) {
|
||||
if (e.key !== "Tab") return;
|
||||
|
||||
if (e.shiftKey) {
|
||||
if (document.activeElement === firstElement) {
|
||||
e.preventDefault();
|
||||
lastElement.focus();
|
||||
}
|
||||
} else {
|
||||
if (document.activeElement === lastElement) {
|
||||
e.preventDefault();
|
||||
firstElement.focus();
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
// Add skip to main content link
|
||||
addSkipLinks() {
|
||||
if (document.querySelector(".skip-link")) return;
|
||||
|
||||
const main = document.querySelector("main, .page-content, #main-content");
|
||||
if (!main) return;
|
||||
|
||||
main.id = main.id || "main-content";
|
||||
|
||||
const skipLink = document.createElement("a");
|
||||
skipLink.href = "#" + main.id;
|
||||
skipLink.className = "skip-link";
|
||||
skipLink.textContent = "Skip to main content";
|
||||
skipLink.setAttribute("tabindex", "0");
|
||||
|
||||
document.body.insertBefore(skipLink, document.body.firstChild);
|
||||
|
||||
// Add CSS for skip link
|
||||
if (!document.getElementById("skip-link-styles")) {
|
||||
const style = document.createElement("style");
|
||||
style.id = "skip-link-styles";
|
||||
style.textContent = `
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
top: -100px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: var(--primary-pink, #f8c8dc);
|
||||
color: var(--text-primary, #333);
|
||||
padding: 12px 24px;
|
||||
border-radius: 0 0 8px 8px;
|
||||
font-weight: 600;
|
||||
text-decoration: none;
|
||||
z-index: 10000;
|
||||
transition: top 0.3s ease;
|
||||
}
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
outline: 3px solid var(--accent-pink, #ff69b4);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
.sr-only {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
padding: 0;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
white-space: nowrap;
|
||||
border: 0;
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
},
|
||||
|
||||
// Enhance focus states for better visibility
|
||||
enhanceFocusStates() {
|
||||
if (document.getElementById("focus-state-styles")) return;
|
||||
|
||||
const style = document.createElement("style");
|
||||
style.id = "focus-state-styles";
|
||||
style.textContent = `
|
||||
/* Enhanced focus states for accessibility */
|
||||
:focus {
|
||||
outline: 2px solid var(--accent-pink, #ff69b4);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
:focus:not(:focus-visible) {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 3px solid var(--accent-pink, #ff69b4);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* Button focus */
|
||||
.btn:focus-visible,
|
||||
button:focus-visible {
|
||||
outline: 3px solid var(--accent-pink, #ff69b4);
|
||||
outline-offset: 2px;
|
||||
box-shadow: 0 0 0 4px rgba(255, 105, 180, 0.25);
|
||||
}
|
||||
|
||||
/* Link focus */
|
||||
a:focus-visible {
|
||||
outline: 2px solid var(--accent-pink, #ff69b4);
|
||||
outline-offset: 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Input focus */
|
||||
input:focus-visible,
|
||||
select:focus-visible,
|
||||
textarea:focus-visible {
|
||||
outline: 2px solid var(--primary-pink, #f8c8dc);
|
||||
outline-offset: 0;
|
||||
border-color: var(--accent-pink, #ff69b4);
|
||||
box-shadow: 0 0 0 3px rgba(248, 200, 220, 0.3);
|
||||
}
|
||||
|
||||
/* Card focus */
|
||||
.product-card:focus-visible,
|
||||
.blog-card:focus-visible {
|
||||
outline: 3px solid var(--accent-pink, #ff69b4);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
|
||||
/* Nav link focus */
|
||||
.nav-link:focus-visible {
|
||||
background: rgba(248, 200, 220, 0.3);
|
||||
border-radius: var(--radius-sm, 4px);
|
||||
}
|
||||
|
||||
/* Icon button focus */
|
||||
.nav-icon-btn:focus-visible {
|
||||
outline: 2px solid var(--accent-pink, #ff69b4);
|
||||
outline-offset: 2px;
|
||||
background: rgba(248, 200, 220, 0.3);
|
||||
}
|
||||
`;
|
||||
document.head.appendChild(style);
|
||||
},
|
||||
|
||||
// Setup live regions for dynamic content announcements
|
||||
announceLiveRegions() {
|
||||
// Create live region for announcements
|
||||
if (!document.getElementById("a11y-live-region")) {
|
||||
const liveRegion = document.createElement("div");
|
||||
liveRegion.id = "a11y-live-region";
|
||||
liveRegion.setAttribute("aria-live", "polite");
|
||||
liveRegion.setAttribute("aria-atomic", "true");
|
||||
liveRegion.className = "sr-only";
|
||||
document.body.appendChild(liveRegion);
|
||||
}
|
||||
|
||||
// Announce cart updates
|
||||
const originalAddToCart = window.ShopState?.addToCart;
|
||||
if (originalAddToCart) {
|
||||
window.ShopState.addToCart = function (...args) {
|
||||
const result = originalAddToCart.apply(this, args);
|
||||
A11y.announce("Item added to cart");
|
||||
return result;
|
||||
};
|
||||
}
|
||||
|
||||
// Announce wishlist updates
|
||||
const originalAddToWishlist = window.ShopState?.addToWishlist;
|
||||
if (originalAddToWishlist) {
|
||||
window.ShopState.addToWishlist = function (...args) {
|
||||
const result = originalAddToWishlist.apply(this, args);
|
||||
A11y.announce("Item added to wishlist");
|
||||
return result;
|
||||
};
|
||||
}
|
||||
},
|
||||
|
||||
// Announce message to screen readers
|
||||
announce(message) {
|
||||
const liveRegion = document.getElementById("a11y-live-region");
|
||||
if (liveRegion) {
|
||||
liveRegion.textContent = message;
|
||||
setTimeout(() => {
|
||||
liveRegion.textContent = "";
|
||||
}, 1000);
|
||||
}
|
||||
},
|
||||
|
||||
// Mobile accessibility enhancements
|
||||
fixMobileAccessibility() {
|
||||
// Ensure touch targets are at least 44x44px
|
||||
const style = document.createElement("style");
|
||||
style.id = "mobile-a11y-styles";
|
||||
style.textContent = `
|
||||
@media (max-width: 768px) {
|
||||
/* Minimum touch target size */
|
||||
button,
|
||||
.btn,
|
||||
.nav-icon-btn,
|
||||
.nav-link,
|
||||
input[type="button"],
|
||||
input[type="submit"],
|
||||
a {
|
||||
min-height: 44px;
|
||||
min-width: 44px;
|
||||
}
|
||||
|
||||
/* Ensure adequate spacing for touch */
|
||||
.nav-actions {
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* Larger tap targets for mobile menu */
|
||||
.nav-menu .nav-link {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
if (!document.getElementById("mobile-a11y-styles")) {
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
// Update mobile toggle aria-expanded on click
|
||||
const mobileToggle = document.querySelector(".nav-mobile-toggle");
|
||||
if (mobileToggle) {
|
||||
const observer = new MutationObserver(() => {
|
||||
const navMenu = document.querySelector(".nav-menu");
|
||||
const isOpen =
|
||||
navMenu?.classList.contains("open") ||
|
||||
navMenu?.classList.contains("active") ||
|
||||
document.body.classList.contains("nav-open");
|
||||
mobileToggle.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
attributeFilter: ["class"],
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
// Initialize on DOM ready
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", () => A11y.init());
|
||||
} else {
|
||||
A11y.init();
|
||||
}
|
||||
|
||||
// Expose for external use
|
||||
window.A11y = A11y;
|
||||
})();
|
||||
Reference in New Issue
Block a user