1/** 2 * Product Carousel Module 3 * Creates navigation and handles slide transitions for product carousels 4 */ 5 6export default function initProductCarousel() { 7 const carousels = document.querySelectorAll('.product-carousel'); 8 9 if (!carousels.length) return; 10 11 carousels.forEach((carousel, carouselIndex) => { 12 const slides = Array.from(carousel.children); 13 14 if (slides.length <= 1) return; // No need for navigation if only one slide 15 16 // Create navigation 17 const nav = createNavigation(slides.length, carouselIndex); 18 19 // Insert navigation after carousel 20 carousel.insertAdjacentElement('afterend', nav); 21 22 // Initialize carousel 23 initializeCarousel(carousel, slides, nav, carouselIndex); 24 25 // Bind navigation events 26 bindNavigationEvents(carousel, slides, nav, carouselIndex); 27 }); 28} 29 30/** 31 * Create navigation element with full accessibility support 32 */ 33function createNavigation(slideCount, carouselIndex) { 34 const nav = document.createElement('nav'); 35 nav.className = 'product-carousel-nav'; 36 nav.setAttribute('aria-label', `Product carousel ${carouselIndex + 1} navigation`); 37 nav.setAttribute('role', 'tablist'); 38 39 const ul = document.createElement('ul'); 40 ul.setAttribute('role', 'presentation'); 41 42 for (let i = 0; i < slideCount; i++) { 43 const li = document.createElement('li'); 44 li.setAttribute('role', 'presentation'); 45 46 const button = document.createElement('button'); 47 button.setAttribute('type', 'button'); 48 button.setAttribute('role', 'tab'); 49 button.setAttribute('id', `carousel-${carouselIndex}-tab-${i}`); 50 button.setAttribute('aria-controls', `carousel-${carouselIndex}-slide-${i}`); 51 button.setAttribute('aria-label', `Go to slide ${i + 1} of ${slideCount}`); 52 button.setAttribute('aria-selected', i === 0 ? 'true' : 'false'); 53 button.setAttribute('tabindex', i === 0 ? '0' : '-1'); 54 button.setAttribute('data-slide-index', i); 55 56 li.appendChild(button); 57 ul.appendChild(li); 58 } 59 60 nav.appendChild(ul); 61 return nav; 62} 63 64/** 65 * Initialize carousel state with full accessibility 66 */ 67function initializeCarousel(carousel, slides, nav, carouselIndex) { 68 // Set up carousel container 69 carousel.style.position = 'relative'; 70 carousel.style.overflow = 'hidden'; 71 carousel.style.width = '100%'; 72 carousel.setAttribute('role', 'region'); 73 carousel.setAttribute('aria-label', `Product carousel ${carouselIndex + 1}`); 74 carousel.setAttribute('aria-live', 'polite'); 75 76 // Create slide container 77 const slideContainer = document.createElement('div'); 78 slideContainer.style.display = 'flex'; 79 slideContainer.style.width = `${slides.length * 100}%`; 80 slideContainer.style.transition = 'transform 0.3s ease-in-out'; 81 slideContainer.setAttribute('data-carousel-container', 'true'); 82 83 // Move all slides into the container 84 slides.forEach((slide) => { 85 slideContainer.appendChild(slide); 86 }); 87 88 // Add container to carousel 89 carousel.appendChild(slideContainer); 90 91 // Create live region for slide change announcements 92 const liveRegion = document.createElement('div'); 93 liveRegion.setAttribute('aria-live', 'polite'); 94 liveRegion.setAttribute('aria-atomic', 'true'); 95 liveRegion.setAttribute('class', 'sr-only'); 96 liveRegion.setAttribute('id', `carousel-${carouselIndex}-live-region`); 97 carousel.appendChild(liveRegion); 98 99 // Set up slides 100 slides.forEach((slide, index) => { 101 slide.style.flex = `0 0 ${100 / slides.length}%`; 102 slide.style.minWidth = '0'; 103 slide.setAttribute('role', 'tabpanel'); 104 slide.setAttribute('id', `carousel-${carouselIndex}-slide-${index}`); 105 slide.setAttribute('aria-labelledby', `carousel-${carouselIndex}-tab-${index}`); 106 slide.setAttribute('aria-hidden', index === 0 ? 'false' : 'true'); 107 108 if (index === 0) { 109 slide.setAttribute('tabindex', '0'); 110 } else { 111 slide.setAttribute('tabindex', '-1'); 112 } 113 }); 114 115 // Set initial active nav item 116 const firstNavButton = nav.querySelector('button'); 117 if (firstNavButton) { 118 firstNavButton.classList.add('active'); 119 firstNavButton.setAttribute('aria-selected', 'true'); 120 firstNavButton.setAttribute('tabindex', '0'); 121 } 122 123 // Announce initial slide 124 announceSlideChange(liveRegion, 1, slides.length); 125} 126 127/** 128 * Bind navigation events with full keyboard support 129 */ 130function bindNavigationEvents(carousel, slides, nav, carouselIndex) { 131 const navButtons = nav.querySelectorAll('button'); 132 133 navButtons.forEach((button, index) => { 134 // Click events 135 button.addEventListener('click', (e) => { 136 e.preventDefault(); 137 goToSlide(carousel, slides, nav, index, carouselIndex); 138 }); 139 140 // Keyboard events 141 button.addEventListener('keydown', (e) => { 142 let targetIndex = index; 143 144 switch (e.key) { 145 case 'ArrowRight': 146 case 'ArrowDown': 147 e.preventDefault(); 148 targetIndex = (index + 1) % navButtons.length; 149 break; 150 151 case 'ArrowLeft': 152 case 'ArrowUp': 153 e.preventDefault(); 154 targetIndex = (index - 1 + navButtons.length) % navButtons.length; 155 break; 156 157 case 'Home': 158 e.preventDefault(); 159 targetIndex = 0; 160 break; 161 162 case 'End': 163 e.preventDefault(); 164 targetIndex = navButtons.length - 1; 165 break; 166 167 case 'Enter': 168 case ' ': 169 e.preventDefault(); 170 goToSlide(carousel, slides, nav, index, carouselIndex); 171 return; 172 173 default: 174 return; 175 } 176 177 // Focus the target button 178 navButtons[targetIndex].focus(); 179 goToSlide(carousel, slides, nav, targetIndex, carouselIndex); 180 }); 181 182 // Focus management 183 button.addEventListener('focus', () => { 184 // Optional: scroll into view if needed 185 button.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); 186 }); 187 }); 188} 189 190/** 191 * Navigate to specific slide with full accessibility updates 192 */ 193function goToSlide(carousel, slides, nav, targetIndex, carouselIndex) { 194 const navButtons = nav.querySelectorAll('button'); 195 const liveRegion = carousel.querySelector(`#carousel-${carouselIndex}-live-region`); 196 const slideContainer = carousel.querySelector('[data-carousel-container]'); 197 198 // Move the slide container to show the target slide 199 if (slideContainer) { 200 const translateX = -(targetIndex * (100 / slides.length)); 201 slideContainer.style.transform = `translateX(${translateX}%)`; 202 } 203 204 // Update slide accessibility
205 slides.forEach((slide, index) => { 206 // Update ARIA attributes 207 if (index === targetIndex) { 208 slide.setAttribute('aria-hidden', 'false'); 209 slide.setAttribute('tabindex', '0'); 210 } else { 211 slide.setAttribute('aria-hidden', 'true'); 212 slide.setAttribute('tabindex', '-1'); 213 } 214 }); 215 216 // Update navigation active states 217 navButtons.forEach((button, index) => { 218 if (index === targetIndex) { 219 button.classList.add('active'); 220 button.setAttribute('aria-selected', 'true'); 221 button.setAttribute('tabindex', '0'); 222 } else { 223 button.classList.remove('active'); 224 button.setAttribute('aria-selected', 'false'); 225 button.setAttribute('tabindex', '-1'); 226 } 227 }); 228 229 // Announce slide change to screen readers 230 announceSlideChange(liveRegion, targetIndex + 1, slides.length); 231} 232 233/** 234 * Announce slide changes to screen readers 235 */ 236function announceSlideChange(liveRegion, currentSlide, totalSlides) { 237 if (liveRegion) { 238 liveRegion.textContent = `Slide ${currentSlide} of ${totalSlides}`; 239 } 240}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.