@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.
@@ -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.inert = true;
442
+ setElementInert(mobileMenu, true);
361
443
 
362
444
  let isMenuOpen = false;
363
445
 
364
446
  function shouldPreventMobileMenu() {
365
- const menuSizeElement = document.querySelector('.menu-size.is-mobile');
366
- if (!menuSizeElement) return false;
447
+ const menuHideElement = document.querySelector('.menu-hide.is-mobile');
448
+ if (!menuHideElement) return false;
367
449
 
368
- const computedStyle = window.getComputedStyle(menuSizeElement);
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.inert = false;
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.inert = true;
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.inert = !isMenuOpen;
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 menuSizeElement = document.querySelector('.menu-size.is-mobile');
568
- if (!menuSizeElement) return;
657
+ const menuHideElement = document.querySelector('.menu-hide.is-mobile');
658
+ if (!menuHideElement) return;
569
659
 
570
- const computedStyle = window.getComputedStyle(menuSizeElement);
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 menuSizeElement = document.querySelector('.menu-size.is-mobile');
590
- if (menuSizeElement) {
591
- resizeObserver.observe(menuSizeElement);
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
- let dropdownList = button.nextElementSibling;
632
-
633
- if (!dropdownList || !dropdownList.querySelector('a')) {
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 (!dropdownList || !dropdownList.querySelector('a')) {
638
- const parent = button.closest('[data-hs-nav="menu"]');
639
- const allListElements = parent?.querySelectorAll('div, ul, nav');
640
- dropdownList = Array.from(allListElements || []).find(el =>
641
- el.querySelectorAll('a').length > 1 &&
642
- !el.contains(button)
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.inert = true;
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.inert = !newState;
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
- if (current.inert === true) {
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
@@ -1,4 +1,4 @@
1
- // Version:1.2.32
1
+ // Version:1.2.35
2
2
 
3
3
  const API_NAME = 'hsmain';
4
4
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hortonstudio/main",
3
- "version": "1.2.34",
3
+ "version": "1.2.35",
4
4
  "description": "Animation and utility library for client websites",
5
5
  "main": "index.js",
6
6
  "type": "module",