@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.
File without changes
@@ -17,8 +17,8 @@ export const init = () => {
17
17
  };
18
18
 
19
19
  setupDynamicDropdowns(addObserver, addHandler);
20
- setupMenuButton(addHandler, cleanup.state);
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 toggle = wrapper.querySelector('[data-hs-nav="dropdown-toggle"]');
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 menuItems = dropdownList.querySelectorAll("a");
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 allNavbarElements = navbar.querySelectorAll("a, button");
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, cleanupState) {
436
- const menuButtons = document.querySelectorAll('[data-hs-nav="menubtn"]');
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(".menu_hide");
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 focusableElements = navbarWrapper.querySelectorAll(
465
- 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
466
- );
467
- const focusableArray = Array.from(focusableElements);
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
- cleanupState.focusTrapHandler = focusTrapHandler;
489
+ return focusTrapHandler;
489
490
  }
490
491
 
491
- function removeFocusTrap() {
492
- if (cleanupState.focusTrapHandler) {
493
- document.removeEventListener('keydown', cleanupState.focusTrapHandler);
494
- cleanupState.focusTrapHandler = null;
492
+ function removeFocusTrap(focusTrapHandler) {
493
+ if (focusTrapHandler) {
494
+ document.removeEventListener('keydown', focusTrapHandler);
495
495
  }
496
496
  }
497
497
 
498
- function openMenu() {
499
- if (isMenuOpen || shouldPreventMenu()) return;
500
- isMenuOpen = true;
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
- document.body.classList.add("u-overflow-hidden");
503
+ let focusTrapHandler = null;
503
504
 
504
- menuButtons.forEach(btn => {
505
- btn.setAttribute("aria-expanded", "true");
506
- btn.setAttribute("aria-label", "Close navigation menu");
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
- // Create focus trap for navbar
510
- createFocusTrap();
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
- // Focus first menu item after menu opens
513
- setTimeout(() => {
514
- const firstElement = menu.querySelector("button, a");
515
- if (firstElement) {
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
- const clickEvent = new MouseEvent("click", {
521
- bubbles: true,
522
- cancelable: true,
523
- view: window,
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
- function closeMenu() {
529
- if (!isMenuOpen) return;
530
- isMenuOpen = false;
525
+ // Create focus trap for navbar
526
+ focusTrapHandler = createFocusTrap(menuButton);
531
527
 
532
- // Close all open menu dropdowns
533
- const openDropdownButtons = menu.querySelectorAll('[data-hs-nav="menu-dropdown-btn"][aria-expanded="true"]');
534
- openDropdownButtons.forEach(button => button.click());
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
- document.body.classList.remove("u-overflow-hidden");
546
+ menuButtons.forEach(btn => {
547
+ btn.setAttribute("aria-expanded", "false");
548
+ btn.setAttribute("aria-label", "Open navigation menu");
549
+ });
537
550
 
538
- const activeMenuButton = Array.from(menuButtons).find(() => menu.contains(document.activeElement));
539
- if (activeMenuButton) {
540
- activeMenuButton.focus();
551
+ // Remove focus trap
552
+ removeFocusTrap(focusTrapHandler);
553
+ focusTrapHandler = null;
554
+ }
541
555
  }
542
556
 
543
- menuButtons.forEach(btn => {
544
- btn.setAttribute("aria-expanded", "false");
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
- const clickEvent = new MouseEvent("click", {
552
- bubbles: true,
553
- cancelable: true,
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
- function toggleMenu() {
560
- if (shouldPreventMenu()) return;
561
-
562
- if (isMenuOpen) {
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
- const keydownHandler = function (e) {
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
- addHandler(menuButton, "keydown", keydownHandler);
589
-
590
- const clickHandler = function (e) {
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(".menu_hide");
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="menubtn"]');
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(".menu_hide");
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 links = menuContainer.querySelectorAll("a");
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 button = wrapper.querySelector('[data-hs-nav="menu-dropdown-btn"]');
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
- const isExpanded = button.getAttribute("aria-expanded") === "true";
671
- const newState = !isExpanded;
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 allElements = menuContainer.querySelectorAll("button, a");
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="menubtn"]',
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"),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hortonstudio/main",
3
- "version": "1.9.4",
3
+ "version": "1.9.6",
4
4
  "description": "Animation and utility library for client websites",
5
5
  "main": "index.js",
6
6
  "type": "module",