@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.
- package/.storybook/globals.js +79 -69
- package/.storybook/main.js +2 -0
- package/.storybook/preview-head.html +1 -0
- package/.storybook/preview.js +2 -1
- package/.storybook/vite-plugin-hbs.js +52 -0
- package/.stylelintignore +5 -0
- package/.stylelintrc.json +24 -0
- package/CHANGELOG.md +2 -0
- package/eslint.config.mjs +60 -0
- package/package.json +25 -18
- package/prettier.config.js +17 -0
- package/src/component-loader.js +10 -0
- package/src/components/_global/css/cta_links/component.scss +1 -1
- package/src/components/_global/css/link_columns/component.scss +8 -0
- package/src/components/_global/css/table/component.scss +6 -118
- package/src/components/accordion/js/global.js +12 -13
- package/src/components/banner/html/component.hbs +31 -3
- package/src/components/banner_advanced/css/component.scss +298 -2
- package/src/components/basic_search/html/component.hbs +4 -4
- package/src/components/global_alert/js/global.js +31 -47
- package/src/components/in_page_navigation/html/component.hbs +9 -16
- package/src/components/in_page_navigation/js/global.js +25 -3
- package/src/components/main_navigation/js/global.js +1 -39
- package/src/components/mega_main_navigation/js/global.js +160 -158
- package/src/data/current.json +0 -187
- package/src/helpers/Handlebars/renderEncoded.js +9 -0
- package/src/html/home.html +1 -6
- package/src/html/inner-with-nav.html +1 -3
- package/src/stories/AZListing/a-z_listing.stories.js +72 -0
- package/src/stories/Abstract/abstract.stories.js +68 -0
- package/src/stories/BackToTop/BackToTop.stories.js +12 -22
- package/src/stories/Banner/banner.stories.js +76 -0
- package/src/stories/Banner/bannerAdvanced.stories.js +234 -0
- package/src/stories/Banner/bannerBasic.stories.js +105 -0
- package/src/stories/Body/body.stories.js +94 -0
- package/src/stories/Breadcrumbs/Breadcrumbs.stories.js +16 -21
- package/src/stories/Button/Button.stories.js +70 -0
- package/src/stories/CTALink/CTALink.stories.js +83 -31
- package/src/stories/Callout/Callout.stories.js +25 -35
- package/src/stories/Cards/CardFeature.stories.js +164 -0
- package/src/stories/Cards/CardMultiAction.stories.js +212 -0
- package/src/stories/Cards/CardNoAction.stories.js +201 -0
- package/src/stories/Cards/CardSingleAction.stories.js +197 -0
- package/src/stories/Cards/Toowoomba-web.jpeg +0 -0
- package/src/stories/DirectionLinks/DirectionLinks.stories.js +2 -8
- package/src/stories/Footer/Footer.stories.js +7 -13
- package/src/stories/{Checkboxes → Forms/Checkboxes}/Checkboxes.stories.js +4 -9
- package/src/stories/{RadioButtons → Forms/RadioButtons}/RadioButtons.stories.js +5 -10
- package/src/stories/{SelectBox → Forms/SelectBox}/SelectBox.stories.js +5 -10
- package/src/stories/Forms/forms.stories.js +188 -0
- package/src/stories/Forms/numberInput.stories.js +68 -0
- package/src/stories/Forms/textInput.stories.js +147 -0
- package/src/stories/Forms/textarea.stories.js +145 -0
- package/src/stories/GlobalAlert/GlobalAlert.stories.js +135 -0
- package/src/stories/Header/Header.js +2 -4
- package/src/stories/Header/Header.stories.js +5 -8
- package/src/stories/Iconography/Iconography.stories.js +2 -7
- package/src/stories/InPageAlert/InPageAlert.stories.js +27 -31
- package/src/stories/InPageNavigation/InPageNavigation.stories.js +89 -0
- package/src/stories/Introduction.mdx +3 -36
- package/src/stories/LinkColumns/LinkColumns.stories.js +35 -23
- package/src/stories/LoadingSpinner/LoadingSpinner.stories.js +11 -25
- package/src/stories/MegaMainNavigation/mega_main_navigation.a11y.stories.js +138 -0
- package/src/stories/MegaMainNavigation/mega_main_navigation.stories.js +245 -0
- package/src/stories/Navbar/Navbar.js +2 -5
- package/src/stories/Navbar/Navbar.stories.js +2 -12
- package/src/stories/Pagination/Pagination.stories.js +4 -10
- package/src/stories/PromoPanel/PromoPanel.stories.js +8 -44
- package/src/stories/Table/table.stories.js +112 -0
- package/src/stories/Tags/Tags.stories.js +3 -8
- package/src/stories/VideoPlayer/VideoPlayer.stories.js +8 -20
- package/src/styles/imports/utilities.scss +80 -0
- package/src/components/banner_intermediate/css/component.scss +0 -289
- package/src/components/banner_intermediate/html/component.hbs +0 -196
- package/src/components/banner_intermediate/js/global.js +0 -6
- package/src/components/banner_intermediate/js/manifest.json +0 -249
- package/src/components/card_no_action/js/global.js +0 -6
- package/src/html/component-a-z_listing.html +0 -57
- package/src/html/component-abstract.html +0 -582
- package/src/html/component-banner.html +0 -581
- package/src/html/component-banner_advanced.html +0 -70
- package/src/html/component-banner_basic.html +0 -55
- package/src/html/component-banner_intermediate.html +0 -64
- package/src/html/component-body.html +0 -605
- package/src/html/component-breadcrumbs.html +0 -583
- package/src/html/component-btn.html +0 -640
- package/src/html/component-callout.html +0 -582
- package/src/html/component-card_feature.html +0 -601
- package/src/html/component-card_multi_action.html +0 -1227
- package/src/html/component-card_no_action.html +0 -395
- package/src/html/component-card_single_action.html +0 -602
- package/src/html/component-footer.html +0 -583
- package/src/html/component-forms.html +0 -1612
- package/src/html/component-global_alert.html +0 -582
- package/src/html/component-header.html +0 -583
- package/src/html/component-in_page_navigation.html +0 -605
- package/src/html/component-loading_spinner.html +0 -45
- package/src/html/component-overflow_menu.html +0 -62
- package/src/html/component-page_alert.html +0 -582
- package/src/html/component-page_footer_info.html +0 -583
- package/src/html/component-pagination.html +0 -583
- package/src/html/component-promo_panel.html +0 -46
- package/src/html/component-tag_list.html +0 -1027
- package/src/html/component-video_player.html +0 -57
- package/src/stories/BackToTop/BackToTop.js +0 -8
- package/src/stories/BackToTop/BackToTop.mdx +0 -24
- package/src/stories/Breadcrumbs/Breadcrumbs.mdx +0 -30
- package/src/stories/CTALink/CTALink.mdx +0 -36
- package/src/stories/Callout/Callout.mdx +0 -28
- package/src/stories/Checkboxes/Checkboxes.mdx +0 -42
- package/src/stories/DirectionLinks/DirectionLinks.mdx +0 -34
- package/src/stories/Footer/Footer.mdx +0 -36
- package/src/stories/Header/Header.mdx +0 -61
- package/src/stories/Iconography/Iconography.mdx +0 -23
- package/src/stories/InPageAlert/InPageAlert.mdx +0 -64
- package/src/stories/LinkColumns/LinkColumns.mdx +0 -28
- package/src/stories/LoadingSpinner/LoadingSpinner.mdx +0 -30
- package/src/stories/Navbar/Navbar.mdx +0 -37
- package/src/stories/Pagination/Pagination.mdx +0 -26
- package/src/stories/PromoPanel/PromoPanel.mdx +0 -26
- package/src/stories/RadioButtons/RadioButtons.mdx +0 -42
- package/src/stories/SelectBox/SelectBox.mdx +0 -118
- package/src/stories/Tags/Tags.mdx +0 -63
- package/src/stories/VideoPlayer/VideoPlayer.mdx +0 -26
- /package/src/stories/{Checkboxes → Forms/Checkboxes}/Checkboxes.js +0 -0
- /package/src/stories/{RadioButtons → Forms/RadioButtons}/RadioButtons.js +0 -0
- /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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 =
|
|
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", `#${
|
|
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="#${
|
|
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
|
-
"
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
99
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
163
|
-
|
|
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
|
+
}
|