1/** 2 * Accessible Tabs Module 3 * Manages tab navigation with accessibility features for screen readers 4 */ 5 6export default function initTabs(options = {}) { 7 const { 8 excludeFromTabsSelector = [], 9 } = options; 10 // console.log('initTabs() called'); 11 12 // Look for both new and old tab structures 13 const newTabsContainers = document.querySelectorAll('.tabs'); 14 const oldTabsContainers = document.querySelectorAll('.gdm-tabs'); 15 16 // console.log('Found tabs containers:', { 17 // newStructure: newTabsContainers.length, 18 // oldStructure: oldTabsContainers.length 19 // }); 20 21 // Handle new structure (.tabs) 22 if (newTabsContainers.length) { 23 // console.log('Processing new tab structure...'); 24 newTabsContainers.forEach((tabsContainer, index) => { 25 // console.log(`Processing new tabs container ${index}:`, tabsContainer); 26 const tabsNav = tabsContainer.querySelector('.tabs-nav'); 27 const tabNavItems = filterNavItems( 28 Array.from(tabsContainer.querySelectorAll('.tabs-nav li a')), 29 excludeFromTabsSelector 30 ); 31 const tabPanels = tabsContainer.querySelectorAll('.tab'); 32 33 // console.log('Found elements:', { 34 // tabsNav, 35 // tabNavItems: tabNavItems.length, 36 // tabPanels: tabPanels.length 37 // }); 38 39 if (!tabsNav || !tabNavItems.length || !tabPanels.length) { 40 // console.log('Missing required elements, skipping this container'); 41 return; 42 } 43 44 // Set up ARIA attributes for accessibility 45 setupAccessibility(tabsContainer, tabNavItems, tabPanels); 46 47 // Initialize tabs - check for hash or show first tab by default 48 initializeTabs(tabNavItems, tabPanels); 49 50 // Bind click events 51 bindTabEvents(tabNavItems, tabPanels); 52 53 // Bind keyboard events for accessibility 54 bindKeyboardEvents(tabNavItems); 55 56 // Initialize mobile menu toggle for side navigation 57 initMobileMenuToggle(tabsContainer); 58 }); 59 } 60 61 // Handle old structure (.gdm-tabs) for compatibility 62 if (oldTabsContainers.length) { 63 // console.log('Processing old tab structure for compatibility...'); 64 oldTabsContainers.forEach((tabsContainer, index) => { 65 // console.log(`Processing old tabs container ${index}:`, tabsContainer); 66 const tabsNav = tabsContainer.querySelector('.gdm-tabs-nav'); 67 const tabNavItems = filterNavItems( 68 Array.from(tabsContainer.querySelectorAll('.gdm-tabs-nav li a')), 69 excludeFromTabsSelector 70 ); 71 72 // console.log('Found old structure elements:', { 73 // tabsNav, 74 // tabNavItems: tabNavItems.length 75 // }); 76 77 if (!tabsNav || !tabNavItems.length) { 78 // console.log('Missing required elements in old structure, skipping'); 79 return; 80 } 81 82 // For old structure, just bind basic click events for mobile nav 83 bindOldStructureEvents(tabsContainer, tabNavItems); 84 }); 85 } 86 87 if (!newTabsContainers.length && !oldTabsContainers.length) { 88 // console.log('No tabs containers found, exiting'); 89 return; 90 } 91 92 document.documentElement.classList.add('tabs-js-enabled'); 93} 94 95/** 96 * Handle old structure mobile navigation (compatibility mode) 97 */ 98function bindOldStructureEvents(tabsContainer, tabNavItems) { 99 // console.log('Setting up old structure mobile navigation'); 100 101 tabNavItems.forEach((navItem, index) => { 102 navItem.addEventListener('click', (e) => { 103 // console.log(`Old structure tab ${index} clicked!`, navItem); 104 105 // Close side menu when tab is clicked (for mobile) 106 const sideNav = navItem.closest('.side-nav'); 107 if (sideNav && sideNav.classList.contains('active')) { 108 const menuToggle = sideNav.querySelector('.menu-toggle'); 109 closeSideNav(sideNav, menuToggle); 110 } 111 112 // Check if this is mobile and if we should handle nav expansion 113 const isMobile = window.innerWidth < 768; 114 if (isMobile) { 115 // console.log('Mobile detected, checking for side nav functionality...'); 116 // For the old structure, we'll need to work with what exists 117 const parentNav = navItem.closest('.gdm-tabs-nav'); 118 if (parentNav) { 119 // console.log('Adding active class to parent nav for mobile'); 120 parentNav.classList.add('active'); 121 } 122 } 123 }); 124 }); 125} 126 127/** 128 * Set up ARIA attributes for screen readers 129 */ 130function setupAccessibility(tabsContainer, tabNavItems, tabPanels) { 131 // Set role attributes 132 const tabsList = tabsContainer.querySelector('.tabs-nav ul') || tabsContainer.querySelector('.tabs-nav'); 133 if (tabsList) { 134 tabsList.setAttribute('role', 'tablist'); 135 } 136
137 tabNavItems.forEach((navItem, index) => { 138 const tabId = `tab-${index}`; 139 const panelId = `tabpanel-${index}`; 140 141 // Set up tab navigation attributes 142 navItem.setAttribute('role', 'tab'); 143 navItem.setAttribute('id', tabId); 144 navItem.setAttribute('aria-controls', panelId); 145 navItem.setAttribute('tabindex', index === 0 ? '0' : '-1'); 146 147 // Set up tab panel attributes 148 if (tabPanels[index]) { 149 tabPanels[index].setAttribute('role', 'tabpanel'); 150 tabPanels[index].setAttribute('id', panelId); 151 tabPanels[index].setAttribute('aria-labelledby', tabId); 152 tabPanels[index].setAttribute('tabindex', '0'); 153 } 154 }); 155} 156 157/** 158 * Initialize tabs - check for hash or show first tab by default 159 */ 160function initializeTabs(tabNavItems, tabPanels) { 161 // Hide all tabs first 162 tabPanels.forEach((tab) => { 163 tab.classList.remove('active'); 164 tab.setAttribute('aria-hidden', 'true'); 165 }); 166 167 // Remove active from all nav items 168 tabNavItems.forEach((navItem) => { 169 navItem.setAttribute('aria-selected', 'false'); 170 navItem.parentElement.classList.remove('active'); 171 }); 172 173 // Check for hash in URL 174 const hash = window.location.hash.slice(1); // Remove the # symbol 175 let activeIndex = 0; 176 177 if (hash) { 178 // Find tab that matches the hash 179 const matchingIndex = Array.from(tabNavItems).findIndex((navItem) => { 180 const tabText = navItem.textContent.trim().toLowerCase().replace(/\s+/g, '-'); 181 return tabText === hash.toLowerCase(); 182 }); 183 184 if (matchingIndex !== -1) { 185 activeIndex = matchingIndex; 186 } 187 } 188 189 // Show the determined active tab 190 if (tabPanels[activeIndex]) { 191 tabPanels[activeIndex].classList.add('active'); 192 tabPanels[activeIndex].setAttribute('aria-hidden', 'false'); 193 } 194 195 if (tabNavItems[activeIndex]) { 196 tabNavItems[activeIndex].setAttribute('aria-selected', 'true'); 197 tabNavItems[activeIndex].parentElement.classList.add('active'); 198 } 199} 200 201/** 202 * Bind click events to tab navigation 203 */ 204function bindTabEvents(tabNavItems, tabPanels) { 205 // console.log('bindTabEvents called with', tabNavItems.length, 'nav items'); 206 tabNavItems.forEach((navItem, index) => { 207 // console.log(`Binding click event to tab ${index}:`, navItem); 208 navItem.addEventListener('click', (e) => { 209 // console.log(`Tab ${index} clicked!`, navItem); 210 e.preventDefault(); 211 212 // Check if we're in mobile view and handle side nav expansion 213 if (shouldHandleMobileNavigation(navItem)) { 214 // console.log('Mobile navigation handled, preventing tab activation'); 215 return; // Don't activate tab, just expand nav 216 } 217 218 // Close side menu when tab is clicked (for mobile) 219 const sideNav = navItem.closest('.side-nav'); 220 if (sideNav && sideNav.classList.contains('active')) { 221 const menuToggle = sideNav.querySelector('.menu-toggle'); 222 closeSideNav(sideNav, menuToggle); 223 } 224 225 // console.log('Activating tab normally'); 226 activateTab(index, tabNavItems, tabPanels); 227 }); 228 }); 229} 230 231/** 232 * Bind keyboard events for accessibility (arrow keys, enter, space) 233 */ 234function bindKeyboardEvents(tabNavItems) { 235 tabNavItems.forEach((navItem, index) => { 236 navItem.addEventListener('keydown', (e) => { 237 let targetIndex = index; 238 239 switch (e.key) { 240 case 'ArrowRight': 241 case 'ArrowDown': 242 e.preventDefault(); 243 targetIndex = (index + 1) % tabNavItems.length; 244 break; 245 246 case 'ArrowLeft': 247 case 'ArrowUp': 248 e.preventDefault(); 249 targetIndex = (index - 1 + tabNavItems.length) % tabNavItems.length; 250 break; 251 252 case 'Home': 253 e.preventDefault(); 254 targetIndex = 0; 255 break; 256 257 case 'End': 258 e.preventDefault(); 259 targetIndex = tabNavItems.length - 1; 260 break; 261 262 case 'Enter': 263 case ' ': 264 e.preventDefault(); 265 activateTab(index, tabNavItems, document.querySelectorAll('.tab')); 266 return; 267 268 default: 269 return; 270 } 271 272 // Focus and activate the target tab 273 tabNavItems[targetIndex].focus(); 274 activateTab(targetIndex, tabNavItems, document.querySelectorAll('.tab')); 275 }); 276 }); 277} 278 279/** 280 * Create hash from tab text 281 */ 282function createTabHash(tabText) { 283 return tabText.trim().toLowerCase().replace(/\s+/g, '-'); 284} 285 286/** 287 * Check if mobile navigation should be handled instead of tab activation 288 */ 289function shouldHandleMobileNavigation(navItem) { 290 // Check if screen width is under 768px 291 const isMobile = window.innerWidth < 768; 292 293 // console.log('shouldHandleMobileNavigation called:', { 294 // isMobile, 295 // windowWidth: window.innerWidth 296 // }); 297 298 if (!isMobile) return false;
299 300 // Find the side nav container 301 const sideNav = navItem.closest('.side-nav'); 302 303 // console.log('sideNav found:', sideNav); 304 305 if (!sideNav) return false; 306 307 // If side nav is not active (expanded), expand it instead of changing tabs 308 const isActive = sideNav.classList.contains('active'); 309 // console.log('sideNav active status:', isActive); 310 311 if (!isActive) { 312 const menuToggle = sideNav.querySelector('.menu-toggle'); 313 // console.log('menuToggle found:', menuToggle); 314 315 if (menuToggle) { 316 // console.log('Calling openSideNav with menu toggle...'); 317 openSideNav(sideNav, menuToggle); 318 } else { 319 // console.log('No menu toggle found, adding active class directly...'); 320 sideNav.classList.add('active'); 321 // console.log('sideNav classes after direct addition:', sideNav.classList.toString()); 322 } 323 return true; // Prevent tab activation 324 } 325 326 return false; // Allow tab activation 327} 328 329/** 330 * Activate a specific tab 331 */ 332function activateTab(activeIndex, tabNavItems, tabPanels) { 333 // Remove active state from all tabs and nav items 334 tabPanels.forEach((tab) => { 335 tab.classList.remove('active'); 336 tab.setAttribute('aria-hidden', 'true'); 337 }); 338 339 tabNavItems.forEach((navItem, index) => { 340 navItem.setAttribute('aria-selected', 'false'); 341 navItem.setAttribute('tabindex', '-1'); 342 navItem.parentElement.classList.remove('active'); 343 }); 344 345 // Add active state to selected tab and nav item 346 if (tabPanels[activeIndex]) { 347 tabPanels[activeIndex].classList.add('active'); 348 tabPanels[activeIndex].setAttribute('aria-hidden', 'false'); 349 } 350 351 if (tabNavItems[activeIndex]) { 352 tabNavItems[activeIndex].setAttribute('aria-selected', 'true'); 353 tabNavItems[activeIndex].setAttribute('tabindex', '0'); 354 tabNavItems[activeIndex].parentElement.classList.add('active'); 355 356 // Update URL hash 357 const tabText = tabNavItems[activeIndex].textContent; 358 const hash = createTabHash(tabText); 359 window.history.replaceState(null, null, `#${hash}`); 360 } 361} 362 363/** 364 * Initialize mobile menu toggle functionality 365 */ 366function initMobileMenuToggle(tabsContainer) { 367 const menuToggle = tabsContainer.querySelector('.side-nav .menu-toggle'); 368 const sideNav = tabsContainer.querySelector('.side-nav'); 369 370 if (!sideNav) { 371 // console.log('No side-nav found, skipping mobile menu toggle setup'); 372 return; 373 } 374 375 // console.log('Setting up mobile menu toggle for:', { sideNav, menuToggle }); 376 377 // Set up ARIA attributes for accessibility (if menu toggle exists) 378 if (menuToggle) { 379 menuToggle.setAttribute('role', 'button'); 380 menuToggle.setAttribute('aria-expanded', 'false'); 381 menuToggle.setAttribute('aria-controls', 'side-navigation'); 382 sideNav.setAttribute('id', 'side-navigation'); 383 384 // Toggle menu on menu-toggle click 385 menuToggle.addEventListener('click', (e) => { 386 e.preventDefault(); 387 toggleSideNav(sideNav, menuToggle); 388 }); 389 } 390 391 // Close menu on Escape key 392 document.addEventListener('keydown', (e) => { 393 if (e.key === 'Escape' && sideNav.classList.contains('active')) { 394 // console.log('Escape pressed, closing side nav'); 395 closeSideNav(sideNav, menuToggle); 396 } 397 }); 398 399 // Close menu when clicking outside 400 document.addEventListener('click', (e) => { 401 if (sideNav.classList.contains('active') && 402 !sideNav.contains(e.target) && 403 (!menuToggle || !menuToggle.contains(e.target))) { 404 // console.log('Clicked outside side nav, closing'); 405 closeSideNav(sideNav, menuToggle); 406 } 407 }); 408} 409 410function filterNavItems(items, excludeSelectors = []) { 411 if (!excludeSelectors.length) { 412 return items; 413 } 414 415 return items.filter((item) => !excludeSelectors.some((selector) => item.matches(selector))); 416} 417 418/** 419 * Toggle the side navigation menu 420 */ 421function toggleSideNav(sideNav, menuToggle) { 422 const isActive = sideNav.classList.contains('active'); 423 424 if (isActive) { 425 closeSideNav(sideNav, menuToggle); 426 } else { 427 openSideNav(sideNav, menuToggle); 428 } 429} 430 431/** 432 * Open the side navigation menu 433 */ 434function openSideNav(sideNav, menuToggle) { 435 // console.log('openSideNav called with:', { sideNav, menuToggle }); 436 // console.log('sideNav classes before:', sideNav.classList.toString()); 437 438 sideNav.classList.add('active'); 439 440 // Only set aria-expanded if menuToggle exists 441 if (menuToggle) { 442 menuToggle.setAttribute('aria-expanded', 'true'); 443 } 444 445 // console.log('sideNav classes after:', sideNav.classList.toString()); 446 // if (menuToggle) { 447 // console.log('menuToggle aria-expanded:', menuToggle.getAttribute('aria-expanded')); 448 // } 449} 450 451/** 452 * Close the side navigation menu 453 */ 454function closeSideNav(sideNav, menuToggle) { 455 // console.log('closeSideNav called', { sideNav, menuToggle }); 456 sideNav.classList.remove('active'); 457 458 // Only set aria-expanded if menuToggle exists 459 if (menuToggle) { 460 menuToggle.setAttribute('aria-expanded', 'false'); 461 } 462 463 // console.log('sideNav classes after closing:', sideNav.classList.toString()); 464}
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.