@qhealth-design-system/core 1.20.2 → 1.21.0

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.
Files changed (127) hide show
  1. package/.storybook/globals.js +79 -69
  2. package/.storybook/main.js +2 -0
  3. package/.storybook/preview-head.html +1 -0
  4. package/.storybook/preview.js +2 -1
  5. package/.storybook/vite-plugin-hbs.js +52 -0
  6. package/.stylelintignore +5 -0
  7. package/.stylelintrc.json +24 -0
  8. package/CHANGELOG.md +2 -0
  9. package/eslint.config.mjs +60 -0
  10. package/package.json +25 -18
  11. package/prettier.config.js +17 -0
  12. package/src/component-loader.js +10 -0
  13. package/src/components/_global/css/cta_links/component.scss +1 -1
  14. package/src/components/_global/css/link_columns/component.scss +8 -0
  15. package/src/components/_global/css/table/component.scss +6 -118
  16. package/src/components/accordion/js/global.js +12 -13
  17. package/src/components/banner/html/component.hbs +31 -3
  18. package/src/components/banner_advanced/css/component.scss +298 -2
  19. package/src/components/basic_search/html/component.hbs +4 -4
  20. package/src/components/global_alert/js/global.js +31 -47
  21. package/src/components/in_page_navigation/html/component.hbs +9 -16
  22. package/src/components/in_page_navigation/js/global.js +25 -3
  23. package/src/components/main_navigation/js/global.js +1 -39
  24. package/src/components/mega_main_navigation/js/global.js +160 -158
  25. package/src/data/current.json +0 -187
  26. package/src/helpers/Handlebars/renderEncoded.js +9 -0
  27. package/src/html/home.html +1 -6
  28. package/src/html/inner-with-nav.html +1 -3
  29. package/src/stories/AZListing/a-z_listing.stories.js +72 -0
  30. package/src/stories/Abstract/abstract.stories.js +68 -0
  31. package/src/stories/BackToTop/BackToTop.stories.js +12 -22
  32. package/src/stories/Banner/banner.stories.js +76 -0
  33. package/src/stories/Banner/bannerAdvanced.stories.js +234 -0
  34. package/src/stories/Banner/bannerBasic.stories.js +105 -0
  35. package/src/stories/Body/body.stories.js +94 -0
  36. package/src/stories/Breadcrumbs/Breadcrumbs.stories.js +16 -21
  37. package/src/stories/Button/Button.stories.js +70 -0
  38. package/src/stories/CTALink/CTALink.stories.js +83 -31
  39. package/src/stories/Callout/Callout.stories.js +25 -35
  40. package/src/stories/Cards/CardFeature.stories.js +164 -0
  41. package/src/stories/Cards/CardMultiAction.stories.js +212 -0
  42. package/src/stories/Cards/CardNoAction.stories.js +201 -0
  43. package/src/stories/Cards/CardSingleAction.stories.js +197 -0
  44. package/src/stories/Cards/Toowoomba-web.jpeg +0 -0
  45. package/src/stories/DirectionLinks/DirectionLinks.stories.js +2 -8
  46. package/src/stories/Footer/Footer.stories.js +7 -13
  47. package/src/stories/{Checkboxes → Forms/Checkboxes}/Checkboxes.stories.js +4 -9
  48. package/src/stories/{RadioButtons → Forms/RadioButtons}/RadioButtons.stories.js +5 -10
  49. package/src/stories/{SelectBox → Forms/SelectBox}/SelectBox.stories.js +5 -10
  50. package/src/stories/Forms/forms.stories.js +188 -0
  51. package/src/stories/Forms/numberInput.stories.js +68 -0
  52. package/src/stories/Forms/textInput.stories.js +147 -0
  53. package/src/stories/Forms/textarea.stories.js +145 -0
  54. package/src/stories/GlobalAlert/GlobalAlert.stories.js +135 -0
  55. package/src/stories/Header/Header.js +2 -4
  56. package/src/stories/Header/Header.stories.js +5 -8
  57. package/src/stories/Iconography/Iconography.stories.js +2 -7
  58. package/src/stories/InPageAlert/InPageAlert.stories.js +27 -31
  59. package/src/stories/InPageNavigation/InPageNavigation.stories.js +89 -0
  60. package/src/stories/Introduction.mdx +3 -36
  61. package/src/stories/LinkColumns/LinkColumns.stories.js +35 -23
  62. package/src/stories/LoadingSpinner/LoadingSpinner.stories.js +11 -25
  63. package/src/stories/MegaMainNavigation/mega_main_navigation.a11y.stories.js +138 -0
  64. package/src/stories/MegaMainNavigation/mega_main_navigation.stories.js +245 -0
  65. package/src/stories/Navbar/Navbar.js +2 -5
  66. package/src/stories/Navbar/Navbar.stories.js +2 -12
  67. package/src/stories/Pagination/Pagination.stories.js +4 -10
  68. package/src/stories/PromoPanel/PromoPanel.stories.js +8 -44
  69. package/src/stories/Table/table.stories.js +112 -0
  70. package/src/stories/Tags/Tags.stories.js +3 -8
  71. package/src/stories/VideoPlayer/VideoPlayer.stories.js +8 -20
  72. package/src/styles/imports/utilities.scss +80 -0
  73. package/src/components/banner_intermediate/css/component.scss +0 -289
  74. package/src/components/banner_intermediate/html/component.hbs +0 -196
  75. package/src/components/banner_intermediate/js/global.js +0 -6
  76. package/src/components/banner_intermediate/js/manifest.json +0 -249
  77. package/src/components/card_no_action/js/global.js +0 -6
  78. package/src/html/component-a-z_listing.html +0 -57
  79. package/src/html/component-abstract.html +0 -582
  80. package/src/html/component-banner.html +0 -581
  81. package/src/html/component-banner_advanced.html +0 -70
  82. package/src/html/component-banner_basic.html +0 -55
  83. package/src/html/component-banner_intermediate.html +0 -64
  84. package/src/html/component-body.html +0 -605
  85. package/src/html/component-breadcrumbs.html +0 -583
  86. package/src/html/component-btn.html +0 -640
  87. package/src/html/component-callout.html +0 -582
  88. package/src/html/component-card_feature.html +0 -601
  89. package/src/html/component-card_multi_action.html +0 -1227
  90. package/src/html/component-card_no_action.html +0 -395
  91. package/src/html/component-card_single_action.html +0 -602
  92. package/src/html/component-footer.html +0 -583
  93. package/src/html/component-forms.html +0 -1612
  94. package/src/html/component-global_alert.html +0 -582
  95. package/src/html/component-header.html +0 -583
  96. package/src/html/component-in_page_navigation.html +0 -605
  97. package/src/html/component-loading_spinner.html +0 -45
  98. package/src/html/component-overflow_menu.html +0 -62
  99. package/src/html/component-page_alert.html +0 -582
  100. package/src/html/component-page_footer_info.html +0 -583
  101. package/src/html/component-pagination.html +0 -583
  102. package/src/html/component-promo_panel.html +0 -46
  103. package/src/html/component-tag_list.html +0 -1027
  104. package/src/html/component-video_player.html +0 -57
  105. package/src/stories/BackToTop/BackToTop.js +0 -8
  106. package/src/stories/BackToTop/BackToTop.mdx +0 -24
  107. package/src/stories/Breadcrumbs/Breadcrumbs.mdx +0 -30
  108. package/src/stories/CTALink/CTALink.mdx +0 -36
  109. package/src/stories/Callout/Callout.mdx +0 -28
  110. package/src/stories/Checkboxes/Checkboxes.mdx +0 -42
  111. package/src/stories/DirectionLinks/DirectionLinks.mdx +0 -34
  112. package/src/stories/Footer/Footer.mdx +0 -36
  113. package/src/stories/Header/Header.mdx +0 -61
  114. package/src/stories/Iconography/Iconography.mdx +0 -23
  115. package/src/stories/InPageAlert/InPageAlert.mdx +0 -64
  116. package/src/stories/LinkColumns/LinkColumns.mdx +0 -28
  117. package/src/stories/LoadingSpinner/LoadingSpinner.mdx +0 -30
  118. package/src/stories/Navbar/Navbar.mdx +0 -37
  119. package/src/stories/Pagination/Pagination.mdx +0 -26
  120. package/src/stories/PromoPanel/PromoPanel.mdx +0 -26
  121. package/src/stories/RadioButtons/RadioButtons.mdx +0 -42
  122. package/src/stories/SelectBox/SelectBox.mdx +0 -118
  123. package/src/stories/Tags/Tags.mdx +0 -63
  124. package/src/stories/VideoPlayer/VideoPlayer.mdx +0 -26
  125. /package/src/stories/{Checkboxes → Forms/Checkboxes}/Checkboxes.js +0 -0
  126. /package/src/stories/{RadioButtons → Forms/RadioButtons}/RadioButtons.js +0 -0
  127. /package/src/stories/{SelectBox → Forms/SelectBox}/SelectBox.js +0 -0
@@ -1,17 +1,10 @@
1
- {{!-- {{#ifCond current.data.metadata.pageType.value '==' 'landing'}} --}}
2
1
  <section class="qld__body">
3
- <div class="container-fluid">
4
- {{!-- {{/ifCond}} --}}
5
-
6
- <nav class="qld__inpage-nav-links" aria-label="In page navigation" data-headingType="{{component.data.metadata.headingType.value}}">
7
- <h2 class="qld__inpage-nav-links__heading">
8
- {{{component.data.metadata.heading.value}}}
9
- </h2>
10
- <ul class="qld__link-list"></ul>
11
- </nav>
12
-
13
- {{!-- {{#ifCond current.data.metadata.pageType.value '==' 'landing'}} --}}
14
- </div>
15
- </section>
16
- {{!-- {{/ifCond}} --}}
17
-
2
+ <div class="container-fluid">
3
+ <nav class="qld__inpage-nav-links" aria-label="In page navigation" data-headingType="{{component.data.metadata.headingType.value}}">
4
+ <h2 class="qld__inpage-nav-links__heading">
5
+ {{{component.data.metadata.heading.value}}}
6
+ </h2>
7
+ <ul class="qld__link-list"></ul>
8
+ </nav>
9
+ </div>
10
+ </section>
@@ -13,6 +13,10 @@ export default function initInPageNavigation(document = document) {
13
13
  navs.forEach((nav) => {
14
14
  const headingSelector = nav.getAttribute("data-headingType") ? nav.getAttribute("data-headingType") : "h2";
15
15
  const pageContent = isLandingPage ? mainEl : document.getElementById("content");
16
+ // Gracefully handle missing page content element
17
+ if (!pageContent) {
18
+ return;
19
+ }
16
20
  // Exclude Code CT, and accordion h3, h4, h5, h6
17
21
  const headings = pageContent.querySelectorAll(
18
22
  headingSelector + ":not(.qld__inpage-nav-links__heading):not(.banner__heading):not(.qld__code *):not(.qld__accordion h3):not(.qld__accordion h4):not(.qld__accordion h5):not(.qld__accordion h6)",
@@ -28,21 +32,39 @@ export default function initInPageNavigation(document = document) {
28
32
 
29
33
  // For all headings (with matching data-headingType) in page content
30
34
  headings.forEach((heading) => {
35
+ // If heading already has an ID, use it
36
+ const existingId = heading.getAttribute("id");
31
37
  const title = heading.innerText;
32
38
  // Create sanitized ID from heading text
33
- const id = "section__" + normaliseIdentifier(title.toLowerCase());
39
+ const id = existingId || `section__${normaliseIdentifier(title.toLowerCase())}`;
34
40
  heading.setAttribute("id", id);
35
41
  heading.setAttribute("tabindex", -1);
42
+ });
43
+
44
+ // Handle leftover duplicate ID's found to ensure new unique ID's are generated
45
+ // Get array of all target heading IDs on the page
46
+ const headingsIds = [...headings].map((heading) => heading.getAttribute("id"));
47
+ // Filter anything that's not unique
48
+ const uniqueIds = headingsIds.filter((heading) => headingsIds.indexOf(heading) === headingsIds.lastIndexOf(heading));
49
+ // Loop through headings again but this time handle only duplicated ids
50
+ headings.forEach((elem, index) => {
51
+ let heading = elem.getAttribute("id");
52
+ const title = elem.innerText;
53
+ if (![...uniqueIds].includes(heading)) {
54
+ heading = `${heading}-${index}`;
55
+ elem.setAttribute("id", heading);
56
+ }
36
57
 
37
58
  // Create link and anchor items to be added to the list
38
59
  const link = document.createElement("li");
39
60
  const anchor = document.createElement("a");
40
- anchor.setAttribute("href", `#${id}`);
61
+ anchor.setAttribute("href", `#${heading}`);
62
+ anchor.setAttribute("target", `_self`);
41
63
  anchor.textContent = title;
42
64
  link.appendChild(anchor);
43
65
 
44
66
  // Append link item if it doesn't already exist in the list
45
- if (list.querySelector(`a[href="#${id}"]`) === null) {
67
+ if (list.querySelector(`a[href="#${heading}"]`) === null) {
46
68
  list.appendChild(link);
47
69
  }
48
70
  });
@@ -294,7 +294,7 @@
294
294
  // Add key listener
295
295
  mobileNavEvents.escKey = addEvent(
296
296
  document,
297
- "keyup",
297
+ "keydown",
298
298
  function () {
299
299
  var event = event || window.event;
300
300
  var overlayOpen = getStyle(overlay, "display");
@@ -385,44 +385,6 @@
385
385
  });
386
386
  });
387
387
 
388
- // Add toggle event listeners to accordion buttons
389
- var itemToggles = document.querySelectorAll(
390
- ".qld__main-nav__item-toggle"
391
- );
392
- itemToggles.forEach(function (button) {
393
- button.addEventListener("click", function () {
394
- if (
395
- button.className
396
- .split(" ")
397
- .indexOf("qld__accordion--closed") >= 0
398
- ) {
399
- button.parentNode
400
- .querySelector(".qld__main-nav__item-link")
401
- .classList.add("qld__main-nav__item-link--open");
402
- itemToggles.forEach(function (item) {
403
- if (
404
- item.className
405
- .split(" ")
406
- .indexOf("qld__accordion--open") >= 0
407
- ) {
408
- item.parentNode
409
- .querySelector(".qld__main-nav__item-link")
410
- .classList.remove(
411
- "qld__main-nav__item-link--open"
412
- );
413
- QLD.accordion.Close(item);
414
- }
415
- });
416
- } else {
417
- button.parentNode
418
- .querySelector(".qld__main-nav__item-link")
419
- .classList.remove("qld__main-nav__item-link--open");
420
- }
421
-
422
- QLD.accordion.Toggle(button);
423
- });
424
- });
425
-
426
388
  // Finds all the menu related icons in main nav, mega nav, and header.
427
389
  QLD.utils.updateSvgIconPath(
428
390
  ".qld__main-nav__cta-wrapper .qld__main-nav__item-link svg.qld__icon > use, .qld__header__cta-wrapper .qld__header__cta-link svg.qld__icon > use"
@@ -1,165 +1,167 @@
1
- (function () {
2
- /**
3
- * The mega menu module
4
- *
5
- * @module megaMenu
6
- */
7
- var megaMenu = {
8
- /**
9
- * Initialise the mega menu listeners for keyboard navigation
10
- *
11
- * @memberof module:megaMenu
12
- */
13
- init: function () {
14
- // Top level items
15
- var topNavItems = document.querySelectorAll(".qld__main-nav__item-title > a");
16
- topNavItems.forEach(function (item) {
17
- item.addEventListener("keydown", handleTopNavKeydown);
18
- item.addEventListener("focusin", toggleMenu);
19
- item.addEventListener("focusout", handleTopNavFocusout);
20
- });
21
-
22
- // Mega menu items
23
- var menuItems = document.querySelectorAll(".qld__main-nav__menu-sub a");
24
- menuItems.forEach(function (item) {
25
- item.addEventListener("keydown", handleMenuKeypress);
26
- });
27
- },
28
- };
1
+ /**
2
+ * Mega main navigation keyboard and interaction behaviour.
3
+ *
4
+ * Each nav item with a submenu supports:
5
+ * - Click toggle button: open/close submenu
6
+ * - ESC (anywhere in document): close submenu, prevent sidebar from also closing
7
+ * - Click outside: close submenu
8
+ * - Arrow Up/Down: move focus between submenu items
9
+ * - Focusout (toggle button or submenu): close submenu unless focus stays within
10
+ */
29
11
 
30
- /**
31
- * Handle keydown on top level nav item to close the menu
32
- * if ESCAPE or UP key are pressed
33
- *
34
- * @memberof module:megaMenu
35
- * @instance
36
- * @private
37
- *
38
- * @param {Document.event} e
39
- */
40
- function handleTopNavKeydown(e) {
41
- var key = e.keyCode;
42
-
43
- // ESC or UP ARROW
44
- if (key === 27 || key == 38) {
45
- toggleMenu(e);
46
- }
47
- }
48
-
49
- /**
50
- * Handle focusout of top level nav items.
51
- * Close the menu, unless we have tabbed within
52
- *
53
- * @memberof module:megaMenu
54
- * @instance
55
- * @private
56
- *
57
- * @param {Document.event} e
58
- */
59
- function handleTopNavFocusout(e) {
60
- var link = e.target;
61
- var navItem = link.closest(".qld__main-nav__item");
62
- var expanded = navItem.classList.contains("expanded") ? true : false;
63
- var menu = navItem.querySelector(".qld__main-nav__menu-sub");
64
-
65
- // Short delay to ensure we are on the new active element
66
- // Close the menu, unless we have tabbed within
67
- setTimeout(function () {
68
- let menuHasFocus;
69
- if (menu) {
70
- menuHasFocus = menu.contains(document.activeElement) ? true : false;
71
- }
72
- if (!menuHasFocus && expanded) {
73
- toggleMenu(e);
74
- }
75
- }, 20);
76
- }
77
-
78
- /**
79
- * Toggle the mega menu open/closed
80
- *
81
- * @memberof module:megaMenu
82
- * @instance
83
- * @private
84
- *
85
- * @param {Document.event} e
86
- */
87
- function toggleMenu(e) {
88
- var link = e.target;
89
- var navItem = link.closest(".qld__main-nav__item");
90
- var expanded = navItem.classList.contains("expanded") ? true : false;
91
-
92
- if (!expanded) {
93
- navItem.classList.add("expanded");
94
- setTimeout(function () {
95
- document.addEventListener("click", handleBackgroundClick);
96
- }, 30);
12
+ import {accordion} from "../../accordion/js/global.js";
13
+
14
+ /** Selector matching all interactive elements that can receive focus. */
15
+ const FOCUSABLE_SELECTOR = 'a[href], button:not([disabled]), input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])';
16
+
17
+ export function initMegaMenu() {
18
+ const navItemEls = document.querySelectorAll(".qld__main-nav__item");
19
+
20
+ const navItems = Array.from(navItemEls).map((item) => {
21
+ const navItemTitle = item.querySelector(".qld__main-nav__item-title");
22
+ if (!navItemTitle) return null;
23
+ const toggleBtnEl = navItemTitle.querySelector("button");
24
+ return {
25
+ linkEl: navItemTitle.querySelector("a"),
26
+ toggleBtnEl,
27
+ subMenuEl: item.querySelector(".qld__main-nav__menu-sub"),
28
+ clickingInsideSubMenu: false,
29
+ handlers: {},
30
+ };
31
+ }).filter(Boolean);
32
+
33
+ // Single shared mouseup listener to reset the clickingInsideSubMenu flag
34
+ // across all items. Registered once rather than once per item.
35
+ document.addEventListener("mouseup", () => {
36
+ navItems.forEach((item) => {
37
+ item.clickingInsideSubMenu = false;
38
+ });
39
+ });
40
+
41
+ navItems.forEach((item) => {
42
+ const {toggleBtnEl, subMenuEl} = item;
43
+ item.handlers.click = handleToggleBtnClick(item, navItems);
44
+ toggleBtnEl?.addEventListener("click", item.handlers.click);
45
+
46
+ subMenuEl?.addEventListener("mousedown", () => {
47
+ item.clickingInsideSubMenu = true;
48
+ });
49
+ });
50
+ }
51
+
52
+ function handleToggleBtnClick(item, navItems) {
53
+ return () => {
54
+ const {toggleBtnEl} = item;
55
+ if (isSubMenuOpen(toggleBtnEl)) {
56
+ closeSubMenu(item);
97
57
  } else {
98
- navItem.classList.remove("expanded");
99
- document.removeEventListener("click", handleBackgroundClick);
100
- }
101
- }
102
-
103
- /**
104
- * Close the mega menu if the user clicks outside of it while opened
105
- *
106
- * @memberof module:megaMenu
107
- * @instance
108
- * @private
109
- *
110
- * @param {Document.event} e
111
- */
112
- function handleBackgroundClick(e) {
113
- var target = e.target;
114
- var nav = document.querySelector(".qld__main-nav__menu-inner");
115
-
116
- // If clicked outside nav
117
- if (!nav.contains(target)) {
118
- // Close any expanded menu(s)
119
- document.querySelectorAll(".qld__main-nav__item.expanded").forEach(function (item) {
120
- item.classList.remove("expanded");
121
- });
122
-
123
- // Remove listener
124
- document.removeEventListener("click", handleBackgroundClick);
125
- }
126
- }
127
-
128
- /**
129
- * Handle keypress for item within mega menu.
130
- * Close the menu on press of ESCAPE or UP
131
- * After TAB press, check if focus is still within menu,
132
- * and close if it's not
133
- *
134
- * @memberof module:megaMenu
135
- * @instance
136
- * @private
137
- *
138
- * @param {Document.event} e
139
- */
140
- function handleMenuKeypress(e) {
141
- var link = e.target;
142
- var key = e.keyCode;
143
- var navItem = link.closest(".qld__main-nav__item");
144
- var menu = link.closest(".qld__main-nav__menu-sub");
145
-
146
- // ESC or UP ARROW
147
- if (key === 27 || key == 38) {
148
- navItem.querySelector(".qld__main-nav__item-title > a").focus();
58
+ closeAllSubMenus(navItems, item);
59
+ openSubMenu(item);
149
60
  }
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Opens the submenu and registers all associated event listeners.
66
+ * Listeners are stored on item.handlers so they can be removed on close.
67
+ */
68
+ function openSubMenu(item) {
69
+ const {linkEl, toggleBtnEl, subMenuEl} = item;
70
+ accordion.Open(toggleBtnEl);
71
+ syncNavItemLinkClass(linkEl, true);
72
+
73
+ item.handlers.documentEscape = handleDocumentEscape(item);
74
+ item.handlers.outsideClick = handleOutsideClick(item);
75
+ item.handlers.focusOut = handleFocusOut(item);
76
+ item.handlers.subMenuKeyDown = handleSubMenuKeyDown(item);
150
77
 
151
- // If TAB key is pressed only (not SHIFT + TAB)
152
- if (key === 9 && !e.shiftKey) {
153
- setTimeout(function () {
154
- var menuHasFocus = menu.contains(document.activeElement) ? true : false;
155
- if (!menuHasFocus) {
156
- toggleMenu(e);
157
- }
158
- }, 20);
78
+ // Capture phase ensures this fires before the sidebar's bubble-phase keydown
79
+ // listener, so ESC closes the submenu first without also closing the sidebar.
80
+ document.addEventListener("keydown", item.handlers.documentEscape, true);
81
+ document.addEventListener("click", item.handlers.outsideClick);
82
+ subMenuEl?.addEventListener("focusout", item.handlers.focusOut);
83
+ subMenuEl?.addEventListener("keydown", item.handlers.subMenuKeyDown);
84
+ toggleBtnEl?.addEventListener("focusout", item.handlers.focusOut);
85
+ }
86
+
87
+ /**
88
+ * Closes the submenu and removes all associated event listeners registered in openSubMenu.
89
+ */
90
+ function closeSubMenu(item) {
91
+ const {linkEl, toggleBtnEl, subMenuEl} = item;
92
+ accordion.Close(toggleBtnEl);
93
+ syncNavItemLinkClass(linkEl, false);
94
+
95
+ document.removeEventListener("keydown", item.handlers.documentEscape, true);
96
+ document.removeEventListener("click", item.handlers.outsideClick);
97
+ subMenuEl?.removeEventListener("focusout", item.handlers.focusOut);
98
+ subMenuEl?.removeEventListener("keydown", item.handlers.subMenuKeyDown);
99
+ toggleBtnEl?.removeEventListener("focusout", item.handlers.focusOut);
100
+ }
101
+
102
+ function handleDocumentEscape(item) {
103
+ return (e) => {
104
+ if (e.key !== "Escape") return;
105
+ // Prevent the sidebar's keydown listener from also firing on this keypress.
106
+ e.stopImmediatePropagation();
107
+ closeSubMenu(item);
108
+ item.toggleBtnEl?.focus();
109
+ };
110
+ }
111
+
112
+ function handleOutsideClick(item) {
113
+ return (e) => {
114
+ const {toggleBtnEl, subMenuEl} = item;
115
+ if (toggleBtnEl?.contains(e.target)) return;
116
+ if (subMenuEl?.contains(e.target)) return;
117
+ closeSubMenu(item);
118
+ };
119
+ }
120
+
121
+ function handleSubMenuKeyDown(item) {
122
+ return (e) => {
123
+ if (e.key !== "ArrowDown" && e.key !== "ArrowUp") return;
124
+ e.preventDefault();
125
+
126
+ const focusableItems = Array.from(item.subMenuEl.querySelectorAll(FOCUSABLE_SELECTOR));
127
+ const currentIndex = focusableItems.indexOf(e.target);
128
+
129
+ if (e.key === "ArrowDown") {
130
+ focusableItems[currentIndex + 1]?.focus();
131
+ } else {
132
+ focusableItems[currentIndex - 1]?.focus();
159
133
  }
160
- }
134
+ };
135
+ }
136
+
137
+ function handleFocusOut(item) {
138
+ return (e) => {
139
+ // Keep menu open if the window lost focus (e.g. user switched app).
140
+ if (!document.hasFocus()) return;
141
+ // Keep menu open if the user is clicking within the submenu — relatedTarget
142
+ // is null when clicking non-focusable whitespace, so we track mousedown instead.
143
+ if (item.clickingInsideSubMenu) return;
144
+
145
+ const {toggleBtnEl, subMenuEl} = item;
146
+ const focusMovingTo = e.relatedTarget;
147
+ if (subMenuEl?.contains(focusMovingTo)) return;
148
+ if (toggleBtnEl?.contains(focusMovingTo)) return;
149
+ closeSubMenu(item);
150
+ };
151
+ }
152
+
153
+ function isSubMenuOpen(toggleBtnEl) {
154
+ return toggleBtnEl.getAttribute("aria-expanded") === "true";
155
+ }
156
+
157
+ function syncNavItemLinkClass(linkEl, isOpen) {
158
+ linkEl?.classList.toggle("qld__main-nav__item-link--open", isOpen);
159
+ }
161
160
 
162
- window.addEventListener("DOMContentLoaded", function () {
163
- megaMenu.init();
161
+ function closeAllSubMenus(navItems, currentItem) {
162
+ navItems.forEach((item) => {
163
+ if (item === currentItem) return;
164
+ if (!item.toggleBtnEl) return;
165
+ if (isSubMenuOpen(item.toggleBtnEl)) closeSubMenu(item);
164
166
  });
165
- })();
167
+ }