@hortonstudio/main 1.2.34 → 1.2.35
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/autoInit/navbar.js +127 -28
- package/index.js +1 -1
- package/package.json +1 -1
package/autoInit/navbar.js
CHANGED
@@ -1,4 +1,9 @@
|
|
1
|
+
// Global accessibility state
|
2
|
+
let supportsInert = null;
|
3
|
+
let screenReaderLiveRegion = null;
|
4
|
+
|
1
5
|
export const init = () => {
|
6
|
+
setupAccessibilityFeatures();
|
2
7
|
setupDynamicDropdowns();
|
3
8
|
setupMobileMenuButton();
|
4
9
|
setupMobileMenuARIA();
|
@@ -6,6 +11,73 @@ export const init = () => {
|
|
6
11
|
return { result: 'navbar initialized' };
|
7
12
|
};
|
8
13
|
|
14
|
+
// Accessibility features setup
|
15
|
+
function setupAccessibilityFeatures() {
|
16
|
+
// Check inert support once
|
17
|
+
supportsInert = 'inert' in HTMLElement.prototype;
|
18
|
+
|
19
|
+
// Create screen reader live region
|
20
|
+
screenReaderLiveRegion = document.createElement('div');
|
21
|
+
screenReaderLiveRegion.setAttribute('aria-live', 'polite');
|
22
|
+
screenReaderLiveRegion.setAttribute('aria-atomic', 'true');
|
23
|
+
screenReaderLiveRegion.className = 'u-sr-only';
|
24
|
+
screenReaderLiveRegion.style.cssText = 'position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0;';
|
25
|
+
document.body.appendChild(screenReaderLiveRegion);
|
26
|
+
}
|
27
|
+
|
28
|
+
// Inert polyfill for browsers that don't support it
|
29
|
+
function setElementInert(element, isInert) {
|
30
|
+
if (supportsInert) {
|
31
|
+
element.inert = isInert;
|
32
|
+
} else {
|
33
|
+
// Polyfill: manage tabindex for all focusable elements
|
34
|
+
const focusableSelectors = 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
|
35
|
+
const focusableElements = element.querySelectorAll(focusableSelectors);
|
36
|
+
|
37
|
+
if (isInert) {
|
38
|
+
// Store original tabindex values and disable
|
39
|
+
focusableElements.forEach(el => {
|
40
|
+
const currentTabindex = el.getAttribute('tabindex');
|
41
|
+
el.setAttribute('data-inert-tabindex', currentTabindex || '0');
|
42
|
+
el.setAttribute('tabindex', '-1');
|
43
|
+
});
|
44
|
+
element.setAttribute('data-inert', 'true');
|
45
|
+
} else {
|
46
|
+
// Restore original tabindex values
|
47
|
+
focusableElements.forEach(el => {
|
48
|
+
const originalTabindex = el.getAttribute('data-inert-tabindex');
|
49
|
+
if (originalTabindex === '0') {
|
50
|
+
el.removeAttribute('tabindex');
|
51
|
+
} else if (originalTabindex) {
|
52
|
+
el.setAttribute('tabindex', originalTabindex);
|
53
|
+
}
|
54
|
+
el.removeAttribute('data-inert-tabindex');
|
55
|
+
});
|
56
|
+
element.removeAttribute('data-inert');
|
57
|
+
}
|
58
|
+
}
|
59
|
+
}
|
60
|
+
|
61
|
+
// Screen reader announcements
|
62
|
+
function announceToScreenReader(message) {
|
63
|
+
if (screenReaderLiveRegion) {
|
64
|
+
screenReaderLiveRegion.textContent = message;
|
65
|
+
|
66
|
+
// Clear after a delay to allow for repeat announcements
|
67
|
+
setTimeout(() => {
|
68
|
+
screenReaderLiveRegion.textContent = '';
|
69
|
+
}, 1000);
|
70
|
+
}
|
71
|
+
}
|
72
|
+
|
73
|
+
// Extract menu name from element text or aria-label
|
74
|
+
function getMenuName(element) {
|
75
|
+
const text = element.textContent?.trim() ||
|
76
|
+
element.getAttribute('aria-label') ||
|
77
|
+
'menu';
|
78
|
+
return text.replace(/^(Open|Close)\s+/i, '').replace(/\s+(menu|navigation)$/i, '');
|
79
|
+
}
|
80
|
+
|
9
81
|
// Desktop dropdown system
|
10
82
|
function setupDynamicDropdowns() {
|
11
83
|
const dropdownWrappers = document.querySelectorAll('[data-hs-nav="dropdown"]');
|
@@ -68,6 +140,11 @@ function setupDynamicDropdowns() {
|
|
68
140
|
menuItems.forEach(item => {
|
69
141
|
item.setAttribute('tabindex', '0');
|
70
142
|
});
|
143
|
+
|
144
|
+
// Announce to screen readers
|
145
|
+
const menuName = getMenuName(toggle);
|
146
|
+
announceToScreenReader(`${menuName} menu opened`);
|
147
|
+
|
71
148
|
const clickEvent = new MouseEvent('click', {
|
72
149
|
bubbles: true,
|
73
150
|
cancelable: true,
|
@@ -89,6 +166,11 @@ function setupDynamicDropdowns() {
|
|
89
166
|
menuItems.forEach(item => {
|
90
167
|
item.setAttribute('tabindex', '-1');
|
91
168
|
});
|
169
|
+
|
170
|
+
// Announce to screen readers
|
171
|
+
const menuName = getMenuName(toggle);
|
172
|
+
announceToScreenReader(`${menuName} menu closed`);
|
173
|
+
|
92
174
|
const clickEvent = new MouseEvent('click', {
|
93
175
|
bubbles: true,
|
94
176
|
cancelable: true,
|
@@ -357,15 +439,15 @@ function setupMobileMenuButton() {
|
|
357
439
|
mobileMenu.id = menuId;
|
358
440
|
mobileMenu.setAttribute('role', 'dialog');
|
359
441
|
mobileMenu.setAttribute('aria-modal', 'true');
|
360
|
-
mobileMenu
|
442
|
+
setElementInert(mobileMenu, true);
|
361
443
|
|
362
444
|
let isMenuOpen = false;
|
363
445
|
|
364
446
|
function shouldPreventMobileMenu() {
|
365
|
-
const
|
366
|
-
if (!
|
447
|
+
const menuHideElement = document.querySelector('.menu-hide.is-mobile');
|
448
|
+
if (!menuHideElement) return false;
|
367
449
|
|
368
|
-
const computedStyle = window.getComputedStyle(
|
450
|
+
const computedStyle = window.getComputedStyle(menuHideElement);
|
369
451
|
return computedStyle.display === 'none';
|
370
452
|
}
|
371
453
|
|
@@ -385,7 +467,11 @@ function setupMobileMenuButton() {
|
|
385
467
|
|
386
468
|
menuButton.setAttribute('aria-expanded', 'true');
|
387
469
|
menuButton.setAttribute('aria-label', 'Close navigation menu');
|
388
|
-
mobileMenu
|
470
|
+
setElementInert(mobileMenu, false);
|
471
|
+
|
472
|
+
// Announce to screen readers
|
473
|
+
const menuName = getMenuName(menuButton);
|
474
|
+
announceToScreenReader(`${menuName} opened`);
|
389
475
|
|
390
476
|
// Prevent tabbing outside navbar using tabindex management
|
391
477
|
const navbarWrapper = document.querySelector('[data-hs-nav="wrapper"]') ||
|
@@ -430,7 +516,11 @@ function setupMobileMenuButton() {
|
|
430
516
|
}
|
431
517
|
menuButton.setAttribute('aria-expanded', 'false');
|
432
518
|
menuButton.setAttribute('aria-label', 'Open navigation menu');
|
433
|
-
mobileMenu
|
519
|
+
setElementInert(mobileMenu, true);
|
520
|
+
|
521
|
+
// Announce to screen readers
|
522
|
+
const menuName = getMenuName(menuButton);
|
523
|
+
announceToScreenReader(`${menuName} closed`);
|
434
524
|
|
435
525
|
// Restore tabbing to entire page using tabindex management
|
436
526
|
const elementsToRestore = document.querySelectorAll('[data-mobile-menu-tabindex]');
|
@@ -521,7 +611,7 @@ function setupMobileMenuButton() {
|
|
521
611
|
menuButton.setAttribute('aria-label',
|
522
612
|
isMenuOpen ? 'Close navigation menu' : 'Open navigation menu'
|
523
613
|
);
|
524
|
-
mobileMenu
|
614
|
+
setElementInert(mobileMenu, !isMenuOpen);
|
525
615
|
|
526
616
|
// Handle tabindex management for external clicks
|
527
617
|
if (isMenuOpen) {
|
@@ -564,10 +654,10 @@ function setupMobileMenuBreakpointHandler() {
|
|
564
654
|
let preventedMenuState = false;
|
565
655
|
|
566
656
|
function handleBreakpointChange() {
|
567
|
-
const
|
568
|
-
if (!
|
657
|
+
const menuHideElement = document.querySelector('.menu-hide.is-mobile');
|
658
|
+
if (!menuHideElement) return;
|
569
659
|
|
570
|
-
const computedStyle = window.getComputedStyle(
|
660
|
+
const computedStyle = window.getComputedStyle(menuHideElement);
|
571
661
|
const shouldPrevent = computedStyle.display === 'none';
|
572
662
|
|
573
663
|
if (!window.mobileMenuState) return;
|
@@ -586,9 +676,9 @@ function setupMobileMenuBreakpointHandler() {
|
|
586
676
|
// Use ResizeObserver for more accurate detection
|
587
677
|
if (window.ResizeObserver) {
|
588
678
|
const resizeObserver = new ResizeObserver(handleBreakpointChange);
|
589
|
-
const
|
590
|
-
if (
|
591
|
-
resizeObserver.observe(
|
679
|
+
const menuHideElement = document.querySelector('.menu-hide.is-mobile');
|
680
|
+
if (menuHideElement) {
|
681
|
+
resizeObserver.observe(menuHideElement);
|
592
682
|
}
|
593
683
|
}
|
594
684
|
|
@@ -628,30 +718,38 @@ function setupMobileMenuARIA() {
|
|
628
718
|
button.setAttribute('aria-expanded', 'false');
|
629
719
|
button.setAttribute('aria-controls', listId);
|
630
720
|
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
dropdownList = button.parentElement?.nextElementSibling;
|
635
|
-
}
|
721
|
+
// Look for dropdown list in the same container as the button
|
722
|
+
let dropdownList = null;
|
723
|
+
const buttonContainer = button.closest('.menu-card_dropdown, .menu_contain, [data-hs-nav="menu"]');
|
636
724
|
|
637
|
-
if (
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
642
|
-
|
643
|
-
|
725
|
+
if (buttonContainer) {
|
726
|
+
// First try to find a list element within the same container
|
727
|
+
dropdownList = buttonContainer.querySelector('.menu-card_list, .dropdown-list, [role="menu"]');
|
728
|
+
|
729
|
+
// If not found, look for any element with multiple links that's not the button itself
|
730
|
+
if (!dropdownList || !dropdownList.querySelector('a')) {
|
731
|
+
const allListElements = buttonContainer.querySelectorAll('div, ul, nav');
|
732
|
+
dropdownList = Array.from(allListElements).find(el =>
|
733
|
+
el.querySelectorAll('a').length > 1 &&
|
734
|
+
!el.contains(button) &&
|
735
|
+
el !== button
|
736
|
+
);
|
737
|
+
}
|
644
738
|
}
|
645
739
|
|
646
740
|
if (dropdownList && dropdownList.querySelector('a')) {
|
647
741
|
dropdownList.id = listId;
|
648
|
-
dropdownList
|
742
|
+
setElementInert(dropdownList, true);
|
649
743
|
|
650
744
|
button.addEventListener('click', function() {
|
651
745
|
const isExpanded = button.getAttribute('aria-expanded') === 'true';
|
652
746
|
const newState = !isExpanded;
|
653
747
|
button.setAttribute('aria-expanded', newState);
|
654
|
-
dropdownList
|
748
|
+
setElementInert(dropdownList, !newState);
|
749
|
+
|
750
|
+
// Announce to screen readers
|
751
|
+
const menuName = getMenuName(button);
|
752
|
+
announceToScreenReader(`${menuName} submenu ${newState ? 'opened' : 'closed'}`);
|
655
753
|
});
|
656
754
|
}
|
657
755
|
});
|
@@ -675,7 +773,8 @@ function setupMobileMenuArrowNavigation(menuContainer) {
|
|
675
773
|
return Array.from(allElements).filter(el => {
|
676
774
|
let current = el;
|
677
775
|
while (current && current !== menuContainer) {
|
678
|
-
|
776
|
+
// Check both native inert and polyfill inert
|
777
|
+
if (current.inert === true || current.getAttribute('data-inert') === 'true') {
|
679
778
|
return false;
|
680
779
|
}
|
681
780
|
current = current.parentElement;
|
package/index.js
CHANGED