@hortonstudio/main 1.9.4 → 1.9.6
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/README.md +0 -0
- package/autoInit/{navbar.js → navbar/navbar.js} +128 -118
- package/index.js +1 -1
- package/package.json +1 -1
|
File without changes
|
|
@@ -17,8 +17,8 @@ export const init = () => {
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
setupDynamicDropdowns(addObserver, addHandler);
|
|
20
|
-
setupMenuButton(addHandler,
|
|
21
|
-
setupMenuARIA(addHandler);
|
|
20
|
+
setupMenuButton(addHandler, addObserver);
|
|
21
|
+
setupMenuARIA(addHandler, addObserver);
|
|
22
22
|
setupMenuDisplayObserver(addObserver);
|
|
23
23
|
|
|
24
24
|
return {
|
|
@@ -74,7 +74,8 @@ function setupDynamicDropdowns(addObserver, addHandler) {
|
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
dropdownWrappers.forEach((wrapper) => {
|
|
77
|
-
const
|
|
77
|
+
const clickableElement = wrapper.querySelector('[data-site-clickable="element"]');
|
|
78
|
+
const toggle = clickableElement ? clickableElement.children[0] : null;
|
|
78
79
|
const dropdownList = wrapper.querySelector('[data-hs-nav="dropdown-list"]');
|
|
79
80
|
|
|
80
81
|
if (!toggle || !dropdownList) {
|
|
@@ -96,7 +97,8 @@ function setupDynamicDropdowns(addObserver, addHandler) {
|
|
|
96
97
|
dropdownList.setAttribute("role", "menu");
|
|
97
98
|
dropdownList.setAttribute("aria-hidden", "true");
|
|
98
99
|
|
|
99
|
-
const
|
|
100
|
+
const clickableItems = dropdownList.querySelectorAll('[data-site-clickable="element"]');
|
|
101
|
+
const menuItems = Array.from(clickableItems).map(el => el.children[0]).filter(Boolean);
|
|
100
102
|
menuItems.forEach((item, index) => {
|
|
101
103
|
item.setAttribute("role", "menuitem");
|
|
102
104
|
item.setAttribute("tabindex", "-1");
|
|
@@ -375,7 +377,8 @@ function addDesktopArrowNavigation(addHandler) {
|
|
|
375
377
|
|
|
376
378
|
e.preventDefault();
|
|
377
379
|
|
|
378
|
-
const
|
|
380
|
+
const clickableElements = navbar.querySelectorAll('[data-site-clickable="element"]');
|
|
381
|
+
const allNavbarElements = Array.from(clickableElements).map(el => el.children[0]).filter(Boolean);
|
|
379
382
|
const focusableElements = Array.from(allNavbarElements).filter((el) => {
|
|
380
383
|
if (el.getAttribute("tabindex") === "-1") return false;
|
|
381
384
|
|
|
@@ -432,8 +435,8 @@ function addDesktopArrowNavigation(addHandler) {
|
|
|
432
435
|
}
|
|
433
436
|
|
|
434
437
|
// Menu button system with modal-like functionality
|
|
435
|
-
function setupMenuButton(addHandler,
|
|
436
|
-
const menuButtons = document.querySelectorAll('[data-hs-nav="
|
|
438
|
+
function setupMenuButton(addHandler, addObserver) {
|
|
439
|
+
const menuButtons = document.querySelectorAll('[data-hs-nav="menu-button"]');
|
|
437
440
|
const menu = document.querySelector('[data-hs-nav="menu"]');
|
|
438
441
|
|
|
439
442
|
if (!menuButtons.length || !menu) return;
|
|
@@ -444,27 +447,25 @@ function setupMenuButton(addHandler, cleanupState) {
|
|
|
444
447
|
menu.setAttribute("role", "dialog");
|
|
445
448
|
menu.setAttribute("aria-modal", "true");
|
|
446
449
|
|
|
447
|
-
let isMenuOpen = false;
|
|
448
|
-
|
|
449
450
|
function shouldPreventMenu() {
|
|
450
|
-
const menuHideElement = document.querySelector("
|
|
451
|
+
const menuHideElement = document.querySelector('[data-hs-nav="menu-hide"]');
|
|
451
452
|
if (!menuHideElement) return false;
|
|
452
453
|
|
|
453
454
|
const computedStyle = window.getComputedStyle(menuHideElement);
|
|
454
455
|
return computedStyle.display === "none";
|
|
455
456
|
}
|
|
456
457
|
|
|
457
|
-
function createFocusTrap() {
|
|
458
|
+
function createFocusTrap(menuButton) {
|
|
458
459
|
const navbarWrapper = document.querySelector('[data-hs-nav="wrapper"]');
|
|
459
460
|
|
|
460
461
|
if (!navbarWrapper) return;
|
|
461
462
|
|
|
462
463
|
const focusTrapHandler = (e) => {
|
|
463
464
|
if (e.key === 'Tab') {
|
|
464
|
-
const
|
|
465
|
-
|
|
466
|
-
);
|
|
467
|
-
const focusableArray = Array.from(
|
|
465
|
+
const clickableElements = navbarWrapper.querySelectorAll('[data-site-clickable="element"]');
|
|
466
|
+
const clickableItems = Array.from(clickableElements).map(el => el.children[0]).filter(Boolean);
|
|
467
|
+
const formElements = navbarWrapper.querySelectorAll('input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
|
468
|
+
const focusableArray = [...clickableItems, ...Array.from(formElements)];
|
|
468
469
|
const firstElement = focusableArray[0];
|
|
469
470
|
const lastElement = focusableArray[focusableArray.length - 1];
|
|
470
471
|
|
|
@@ -485,118 +486,92 @@ function setupMenuButton(addHandler, cleanupState) {
|
|
|
485
486
|
};
|
|
486
487
|
|
|
487
488
|
document.addEventListener('keydown', focusTrapHandler);
|
|
488
|
-
|
|
489
|
+
return focusTrapHandler;
|
|
489
490
|
}
|
|
490
491
|
|
|
491
|
-
function removeFocusTrap() {
|
|
492
|
-
if (
|
|
493
|
-
document.removeEventListener('keydown',
|
|
494
|
-
cleanupState.focusTrapHandler = null;
|
|
492
|
+
function removeFocusTrap(focusTrapHandler) {
|
|
493
|
+
if (focusTrapHandler) {
|
|
494
|
+
document.removeEventListener('keydown', focusTrapHandler);
|
|
495
495
|
}
|
|
496
496
|
}
|
|
497
497
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
498
|
+
menuButtons.forEach(menuButton => {
|
|
499
|
+
menuButton.setAttribute("aria-expanded", "false");
|
|
500
|
+
menuButton.setAttribute("aria-controls", menuId);
|
|
501
|
+
menuButton.setAttribute("aria-label", "Open navigation menu");
|
|
501
502
|
|
|
502
|
-
|
|
503
|
+
let focusTrapHandler = null;
|
|
503
504
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
}
|
|
505
|
+
// Check if menu is open by looking for is-active class on button
|
|
506
|
+
function isMenuOpen() {
|
|
507
|
+
return menuButton.classList.contains('is-active');
|
|
508
|
+
}
|
|
508
509
|
|
|
509
|
-
//
|
|
510
|
-
|
|
510
|
+
// Update ARIA states and menu behavior based on current visual state
|
|
511
|
+
function updateMenuState() {
|
|
512
|
+
const isOpen = isMenuOpen();
|
|
513
|
+
const wasOpen = menuButton.getAttribute("aria-expanded") === "true";
|
|
511
514
|
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
firstElement.focus();
|
|
517
|
-
}
|
|
518
|
-
}, 100);
|
|
515
|
+
if (isOpen && !wasOpen) {
|
|
516
|
+
// Opening
|
|
517
|
+
document.body.classList.add("u-overflow-hidden");
|
|
518
|
+
window.lenis?.stop();
|
|
519
519
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
});
|
|
525
|
-
menuButtons[0].dispatchEvent(clickEvent);
|
|
526
|
-
}
|
|
520
|
+
menuButtons.forEach(btn => {
|
|
521
|
+
btn.setAttribute("aria-expanded", "true");
|
|
522
|
+
btn.setAttribute("aria-label", "Close navigation menu");
|
|
523
|
+
});
|
|
527
524
|
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
isMenuOpen = false;
|
|
525
|
+
// Create focus trap for navbar
|
|
526
|
+
focusTrapHandler = createFocusTrap(menuButton);
|
|
531
527
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
528
|
+
// Focus first menu item after menu opens
|
|
529
|
+
setTimeout(() => {
|
|
530
|
+
const firstClickable = menu.querySelector('[data-site-clickable="element"]');
|
|
531
|
+
const firstElement = firstClickable?.children[0];
|
|
532
|
+
if (firstElement) {
|
|
533
|
+
firstElement.focus();
|
|
534
|
+
}
|
|
535
|
+
}, 100);
|
|
536
|
+
} else if (!isOpen && wasOpen) {
|
|
537
|
+
// Closing
|
|
538
|
+
document.body.classList.remove("u-overflow-hidden");
|
|
539
|
+
window.lenis?.start();
|
|
540
|
+
|
|
541
|
+
// Return focus to button if focus was inside menu
|
|
542
|
+
if (menu.contains(document.activeElement)) {
|
|
543
|
+
menuButton.focus();
|
|
544
|
+
}
|
|
535
545
|
|
|
536
|
-
|
|
546
|
+
menuButtons.forEach(btn => {
|
|
547
|
+
btn.setAttribute("aria-expanded", "false");
|
|
548
|
+
btn.setAttribute("aria-label", "Open navigation menu");
|
|
549
|
+
});
|
|
537
550
|
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
551
|
+
// Remove focus trap
|
|
552
|
+
removeFocusTrap(focusTrapHandler);
|
|
553
|
+
focusTrapHandler = null;
|
|
554
|
+
}
|
|
541
555
|
}
|
|
542
556
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
btn.setAttribute("aria-label", "Open navigation menu");
|
|
546
|
-
});
|
|
547
|
-
|
|
548
|
-
// Remove focus trap
|
|
549
|
-
removeFocusTrap();
|
|
557
|
+
// Set initial ARIA states
|
|
558
|
+
updateMenuState();
|
|
550
559
|
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
view: window,
|
|
560
|
+
// Monitor for class changes and update menu state
|
|
561
|
+
const observer = new MutationObserver(() => {
|
|
562
|
+
updateMenuState();
|
|
555
563
|
});
|
|
556
|
-
menuButtons[0].dispatchEvent(clickEvent);
|
|
557
|
-
}
|
|
558
564
|
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
closeMenu();
|
|
564
|
-
} else {
|
|
565
|
-
openMenu();
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
menuButtons.forEach(menuButton => {
|
|
570
|
-
menuButton.setAttribute("aria-expanded", "false");
|
|
571
|
-
menuButton.setAttribute("aria-controls", menuId);
|
|
572
|
-
menuButton.setAttribute("aria-label", "Open navigation menu");
|
|
565
|
+
observer.observe(menuButton, {
|
|
566
|
+
attributes: true,
|
|
567
|
+
attributeFilter: ['class']
|
|
568
|
+
});
|
|
573
569
|
|
|
574
|
-
|
|
575
|
-
if (e.key === "Enter" || e.key === " ") {
|
|
576
|
-
e.preventDefault();
|
|
577
|
-
const config = menuButton.getAttribute("data-hs-config");
|
|
578
|
-
if (config === "close" && isMenuOpen) {
|
|
579
|
-
closeMenu();
|
|
580
|
-
} else if (config === "open" && !isMenuOpen) {
|
|
581
|
-
openMenu();
|
|
582
|
-
} else if (!config) {
|
|
583
|
-
toggleMenu();
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
};
|
|
570
|
+
addObserver(observer);
|
|
587
571
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
if (!e.isTrusted) return;
|
|
592
|
-
const config = menuButton.getAttribute("data-hs-config");
|
|
593
|
-
if (config === "close" && isMenuOpen) {
|
|
594
|
-
closeMenu();
|
|
595
|
-
} else if (config === "open" && !isMenuOpen) {
|
|
596
|
-
openMenu();
|
|
597
|
-
} else if (!config) {
|
|
598
|
-
toggleMenu();
|
|
599
|
-
}
|
|
572
|
+
const clickHandler = function () {
|
|
573
|
+
// Webflow interaction handles the visual state (is-active class)
|
|
574
|
+
// MutationObserver will sync ARIA and behavior
|
|
600
575
|
};
|
|
601
576
|
|
|
602
577
|
addHandler(menuButton, "click", clickHandler);
|
|
@@ -606,14 +581,14 @@ function setupMenuButton(addHandler, cleanupState) {
|
|
|
606
581
|
|
|
607
582
|
function setupMenuDisplayObserver(addObserver) {
|
|
608
583
|
function handleDisplayChange() {
|
|
609
|
-
const menuHideElement = document.querySelector("
|
|
584
|
+
const menuHideElement = document.querySelector('[data-hs-nav="menu-hide"]');
|
|
610
585
|
if (!menuHideElement) return;
|
|
611
586
|
|
|
612
587
|
const computedStyle = window.getComputedStyle(menuHideElement);
|
|
613
588
|
const isMenuVisible = computedStyle.display !== "none";
|
|
614
589
|
|
|
615
590
|
// Get menu button to check if menu is open
|
|
616
|
-
const menuButton = document.querySelector('[data-hs-nav="
|
|
591
|
+
const menuButton = document.querySelector('[data-hs-nav="menu-button"]');
|
|
617
592
|
const isMenuOpen = menuButton && menuButton.getAttribute("aria-expanded") === "true";
|
|
618
593
|
|
|
619
594
|
const shouldShowModal = isMenuVisible && isMenuOpen;
|
|
@@ -623,7 +598,7 @@ function setupMenuDisplayObserver(addObserver) {
|
|
|
623
598
|
}
|
|
624
599
|
|
|
625
600
|
const displayObserver = new ResizeObserver(handleDisplayChange);
|
|
626
|
-
const menuHideElement = document.querySelector("
|
|
601
|
+
const menuHideElement = document.querySelector('[data-hs-nav="menu-hide"]');
|
|
627
602
|
if (menuHideElement) {
|
|
628
603
|
displayObserver.observe(menuHideElement);
|
|
629
604
|
addObserver(displayObserver);
|
|
@@ -642,15 +617,17 @@ function sanitizeForID(text) {
|
|
|
642
617
|
}
|
|
643
618
|
|
|
644
619
|
// Menu ARIA setup
|
|
645
|
-
function setupMenuARIA(addHandler) {
|
|
620
|
+
function setupMenuARIA(addHandler, addObserver) {
|
|
646
621
|
const menuContainer = document.querySelector('[data-hs-nav="menu"]');
|
|
647
622
|
if (!menuContainer) return;
|
|
648
623
|
|
|
649
624
|
const dropdownWrappers = menuContainer.querySelectorAll('[data-hs-nav="menu-dropdown"]');
|
|
650
|
-
const
|
|
625
|
+
const clickableLinks = menuContainer.querySelectorAll('[data-site-clickable="element"]');
|
|
626
|
+
const links = Array.from(clickableLinks).map(el => el.children[0]).filter(Boolean);
|
|
651
627
|
|
|
652
628
|
dropdownWrappers.forEach((wrapper) => {
|
|
653
|
-
const
|
|
629
|
+
const clickableElement = wrapper.querySelector('[data-site-clickable="element"]');
|
|
630
|
+
const button = clickableElement ? clickableElement.children[0] : null;
|
|
654
631
|
const dropdownList = wrapper.querySelector('[data-hs-nav="menu-dropdown-list"]');
|
|
655
632
|
|
|
656
633
|
if (button && dropdownList) {
|
|
@@ -666,11 +643,43 @@ function setupMenuARIA(addHandler) {
|
|
|
666
643
|
dropdownList.id = listId;
|
|
667
644
|
dropdownList.setAttribute("aria-hidden", "true");
|
|
668
645
|
|
|
646
|
+
// Check if dropdown is open by looking for is-active class on wrapper
|
|
647
|
+
function isDropdownOpen() {
|
|
648
|
+
return wrapper.classList.contains('is-active');
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
// Update ARIA states based on current visual state
|
|
652
|
+
function updateARIAStates() {
|
|
653
|
+
const isOpen = isDropdownOpen();
|
|
654
|
+
const wasOpen = button.getAttribute("aria-expanded") === "true";
|
|
655
|
+
|
|
656
|
+
// If dropdown is closing (was open, now closed), focus the button if focus is inside dropdown
|
|
657
|
+
if (wasOpen && !isOpen && dropdownList.contains(document.activeElement)) {
|
|
658
|
+
button.focus();
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
button.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
662
|
+
dropdownList.setAttribute("aria-hidden", isOpen ? "false" : "true");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// Set initial ARIA states
|
|
666
|
+
updateARIAStates();
|
|
667
|
+
|
|
668
|
+
// Monitor for class changes and update ARIA states
|
|
669
|
+
const observer = new MutationObserver(() => {
|
|
670
|
+
updateARIAStates();
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
observer.observe(wrapper, {
|
|
674
|
+
attributes: true,
|
|
675
|
+
attributeFilter: ['class']
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
addObserver(observer);
|
|
679
|
+
|
|
669
680
|
const clickHandler = function () {
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
button.setAttribute("aria-expanded", newState);
|
|
673
|
-
dropdownList.setAttribute("aria-hidden", !newState);
|
|
681
|
+
// Webflow interaction handles the visual state (is-active class)
|
|
682
|
+
// MutationObserver will sync ARIA attributes
|
|
674
683
|
};
|
|
675
684
|
|
|
676
685
|
addHandler(button, "click", clickHandler);
|
|
@@ -690,7 +699,8 @@ function setupMenuARIA(addHandler) {
|
|
|
690
699
|
// Menu arrow navigation
|
|
691
700
|
function setupMenuArrowNavigation(menuContainer, addHandler) {
|
|
692
701
|
function getFocusableElements() {
|
|
693
|
-
const
|
|
702
|
+
const clickableElements = menuContainer.querySelectorAll('[data-site-clickable="element"]');
|
|
703
|
+
const allElements = Array.from(clickableElements).map(el => el.children[0]).filter(Boolean);
|
|
694
704
|
return Array.from(allElements).filter((el) => {
|
|
695
705
|
// Check if element or any ancestor has aria-hidden="true"
|
|
696
706
|
let current = el;
|
|
@@ -763,7 +773,7 @@ function setupMenuArrowNavigation(menuContainer, addHandler) {
|
|
|
763
773
|
e.preventDefault();
|
|
764
774
|
} else if (e.key === "Escape") {
|
|
765
775
|
const menuButton = document.querySelector(
|
|
766
|
-
'[data-hs-nav="
|
|
776
|
+
'[data-hs-nav="menu-button"]',
|
|
767
777
|
);
|
|
768
778
|
if (menuButton) {
|
|
769
779
|
menuButton.click();
|
package/index.js
CHANGED
|
@@ -28,7 +28,7 @@ const initializeHsMain = async () => {
|
|
|
28
28
|
"data-hs-util-ba": () => import("./utils/before-after.js"),
|
|
29
29
|
"data-hs-util-slider": () => import("./utils/slider.js"),
|
|
30
30
|
"smooth-scroll": () => import("./autoInit/smooth-scroll.js"),
|
|
31
|
-
navbar: () => import("./autoInit/navbar.js"),
|
|
31
|
+
navbar: () => import("./autoInit/navbar/navbar.js"),
|
|
32
32
|
accessibility: () => import("./autoInit/accessibility.js"),
|
|
33
33
|
counter: () => import("./autoInit/counter.js"),
|
|
34
34
|
form: () => import("./autoInit/form.js"),
|