@schukai/monster 4.43.4 → 4.43.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.
@@ -1,5 +1,5 @@
1
1
  /**
2
- * Copyright © schukai GmbH and all contributing authors, {{copyRightYear}}. All rights reserved.
2
+ * Copyright © schukai GmbH and all contributing authors, 2025. All rights reserved.
3
3
  * Node module: @schukai/monster
4
4
  *
5
5
  * This source code is licensed under the GNU Affero General Public License version 3 (AGPLv3).
@@ -26,7 +26,9 @@ import {
26
26
  flip,
27
27
  shift,
28
28
  offset,
29
+ size,
29
30
  } from "@floating-ui/dom";
31
+ import { fireCustomEvent } from "../../dom/events.mjs";
30
32
 
31
33
  export { SiteNavigation };
32
34
 
@@ -39,77 +41,76 @@ const hamburgerButtonSymbol = Symbol("hamburgerButton");
39
41
  const hamburgerNavSymbol = Symbol("hamburgerNav");
40
42
  const instanceSymbol = Symbol("instanceSymbol");
41
43
  const activeSubmenuHiderSymbol = Symbol("activeSubmenuHider");
44
+ const hideHamburgerMenuSymbol = Symbol("hideHamburgerMenu");
45
+ const hamburgerCloseButtonSymbol = Symbol("hamburgerCloseButton");
42
46
 
43
47
  /**
44
- * Responsive site navigation component.
48
+ * A responsive site navigation that automatically moves menu items into a hamburger menu
49
+ * when there isn't enough available space.
45
50
  *
51
+ * @summary An adaptive navigation component that supports hover and click interactions for submenus.
46
52
  * @fragments /fragments/components/navigation/site-navigation/
47
53
  *
48
- * @example /examples/components/navigation/site-navigation-simple
54
+ * @example /examples/components/navigation/site-navigation-simple Simple Navigation
55
+ * @example /examples/components/navigation/site-navigation-with-submenus Navigation with Submenus
56
+ * @example /examples/components/navigation/site-navigation-with-mega-menu Navigation with Mega Menu
49
57
  *
50
- * @since 4.41.0
51
- * @copyright schukai GmbH
52
- * @summary Responsive site navigation with hamburger menu and submenus
58
+ * @issue https://localhost.alvine.dev:8440/development/issues/closed/336.html
59
+ *
60
+ * @fires monster-layout-change - Fired when the layout of menu items changes. The event detail contains `{visibleItems, hiddenItems}`.
61
+ * @fires monster-hamburger-show - Fired when the hamburger menu is shown. The event detail contains `{button, menu}`.
62
+ * @fires monster-hamburger-hide - Fired when the hamburger menu is hidden. The event detail contains `{button, menu}`.
63
+ * @fires monster-submenu-show - Fired when a submenu is shown. The event detail contains `{context, trigger, submenu, level}`.
64
+ * @fires monster-submenu-hide - Fired when a submenu is hidden. The event detail contains `{context, trigger, submenu, level}`.
53
65
  */
54
66
  class SiteNavigation extends CustomElement {
55
- /**
56
- * Returns a unique symbol for the instance.
57
- * @returns {symbol}
58
- */
59
67
  static get [instanceSymbol]() {
60
68
  return Symbol.for("@schukai/monster/components/navigation/site@@instance");
61
69
  }
62
70
 
63
71
  /**
64
- * Returns the default options for the component.
65
- * @returns {object}
72
+ * Configuration options for the SiteNavigation component.
73
+ * @see {@link https://monsterjs.org/en/doc/#configurate-a-monster-control}
74
+ *
75
+ * To set these options via an HTML tag, use the `data-monster-options` attribute.
76
+ * The individual configuration values are detailed in the table below.
77
+ *
78
+ * @property {Object} templates - Template definitions.
79
+ * @property {string} templates.main - The main HTML template for the component.
80
+ * @property {string} interactionModel="auto" - Defines the interaction with submenus. Possible values: `auto`, `click`, `hover`. With `auto`, `hover` is used on desktop and `click` is used in the hamburger menu.
81
+ * @property {Object} features - Container for additional feature flags.
82
+ * @property {boolean} features.resetOnClose=true - If `true`, all open submenus within the hamburger menu will be reset when it is closed.
66
83
  */
67
84
  get defaults() {
68
85
  return Object.assign({}, super.defaults, {
69
86
  templates: { main: getTemplate() },
87
+ interactionModel: "auto", // 'auto', 'click', 'hover'
88
+ features: {
89
+ resetOnClose: true,
90
+ },
70
91
  });
71
92
  }
72
93
 
73
- /**
74
- * Assembles the component and initializes controls and event handlers.
75
- * @returns {void}
76
- */
77
94
  [assembleMethodSymbol]() {
78
95
  super[assembleMethodSymbol]();
79
96
  initControlReferences.call(this);
80
97
  initEventHandler.call(this);
81
98
  }
82
99
 
83
- /**
84
- * Returns the CSS stylesheet for the component.
85
- * @returns {Array<CSSStyleSheet>}
86
- */
87
100
  static getCSSStyleSheet() {
88
101
  return [SiteNavigationStyleSheet];
89
102
  }
90
103
 
91
- /**
92
- * Returns the custom element tag name.
93
- * @returns {string}
94
- */
95
104
  static getTag() {
96
105
  return "monster-site-navigation";
97
106
  }
98
107
 
99
- /**
100
- * Called when the component is connected to the DOM.
101
- * @returns {void}
102
- */
103
108
  connectedCallback() {
104
109
  super.connectedCallback();
105
110
  attachResizeObserver.call(this);
106
111
  setTimeout(() => populateTabs.call(this), 0);
107
112
  }
108
113
 
109
- /**
110
- * Called when the component is disconnected from the DOM.
111
- * @returns {void}
112
- */
113
114
  disconnectedCallback() {
114
115
  super.disconnectedCallback();
115
116
  detachResizeObserver.call(this);
@@ -117,10 +118,9 @@ class SiteNavigation extends CustomElement {
117
118
  }
118
119
 
119
120
  /**
120
- * Initializes references to important control elements.
121
+ * Queries the shadow DOM for essential elements and stores their references.
121
122
  * @private
122
123
  * @this {SiteNavigation}
123
- * @returns {void}
124
124
  */
125
125
  function initControlReferences() {
126
126
  if (!this.shadowRoot) throw new Error("Component requires a shadowRoot.");
@@ -136,54 +136,102 @@ function initControlReferences() {
136
136
  this[hamburgerNavSymbol] = this.shadowRoot.querySelector(
137
137
  '[data-monster-role="hamburger-nav"]',
138
138
  );
139
+ this[hamburgerCloseButtonSymbol] = this.shadowRoot.querySelector(
140
+ '[part="hamburger-close-button"]',
141
+ );
139
142
  }
140
143
 
141
144
  /**
142
- * Initializes event handlers for hamburger menu and submenu interactions.
145
+ * Initializes event handlers for the hamburger menu button and its functionality.
143
146
  * @private
144
147
  * @this {SiteNavigation}
145
- * @returns {void}
146
148
  */
147
149
  function initEventHandler() {
148
150
  if (!this.shadowRoot) throw new Error("Component requires a shadowRoot.");
149
151
 
150
152
  const hamburgerButton = this[hamburgerButtonSymbol];
151
153
  const hamburgerNav = this[hamburgerNavSymbol];
154
+ const hamburgerCloseButton = this[hamburgerCloseButtonSymbol];
152
155
  let cleanup;
153
156
 
154
- if (!hamburgerButton || !hamburgerNav) return;
157
+ if (!hamburgerButton || !hamburgerNav || !hamburgerCloseButton) return;
155
158
 
156
- const showMenu = () => {
157
- hamburgerNav.style.display = "block";
158
- cleanup = autoUpdate(hamburgerButton, hamburgerNav, () => {
159
- computePosition(hamburgerButton, hamburgerNav, {
160
- placement: "bottom-end",
161
- middleware: [offset(8), flip(), shift({ padding: 8 })],
162
- }).then(({ x, y }) => {
163
- Object.assign(hamburgerNav.style, {
164
- left: `${x}px`,
165
- top: `${y}px`,
166
- });
167
- });
168
- });
169
- setTimeout(() => document.addEventListener("click", handleOutsideClick), 0);
159
+ const handleOutsideClick = (event) => {
160
+ if (
161
+ !hamburgerButton.contains(event.target) &&
162
+ !hamburgerNav.contains(event.target)
163
+ ) {
164
+ hideMenu();
165
+ }
170
166
  };
171
167
 
172
168
  const hideMenu = () => {
173
169
  hamburgerNav.style.display = "none";
170
+ document.body.classList.remove("monster-navigation-open");
171
+
172
+ fireCustomEvent(this, "monster-hamburger-hide", {
173
+ button: hamburgerButton,
174
+ menu: hamburgerNav,
175
+ });
176
+
177
+ if (this.getOption("features.resetOnClose") === true) {
178
+ this[hiddenElementsSymbol]
179
+ .querySelectorAll(".is-open")
180
+ .forEach((submenu) => submenu.classList.remove("is-open"));
181
+ }
182
+
174
183
  if (cleanup) {
175
184
  cleanup();
185
+ cleanup = undefined;
176
186
  }
177
187
  document.removeEventListener("click", handleOutsideClick);
178
188
  };
179
189
 
180
- const handleOutsideClick = (event) => {
181
- if (
182
- !hamburgerButton.contains(event.target) &&
183
- !hamburgerNav.contains(event.target)
184
- ) {
185
- hideMenu();
186
- }
190
+ this[hideHamburgerMenuSymbol] = hideMenu;
191
+
192
+ const showMenu = () => {
193
+ this[activeSubmenuHiderSymbol]?.();
194
+ hamburgerNav.style.display = "block";
195
+ document.body.classList.add("monster-navigation-open");
196
+
197
+ fireCustomEvent(this, "monster-hamburger-show", {
198
+ button: hamburgerButton,
199
+ menu: hamburgerNav,
200
+ });
201
+
202
+ cleanup = autoUpdate(hamburgerButton, hamburgerNav, () => {
203
+ // Desktop view where the hamburger menu is a dropdown
204
+ if (window.innerWidth > 768) {
205
+ computePosition(hamburgerButton, hamburgerNav, {
206
+ placement: "bottom-end",
207
+ strategy: "fixed",
208
+ middleware: [
209
+ offset(8),
210
+ flip(),
211
+ shift({ padding: 8 }),
212
+ size({
213
+ apply: ({ availableHeight, elements }) => {
214
+ Object.assign(elements.floating.style, {
215
+ maxHeight: `${availableHeight}px`,
216
+ overflowY: "auto",
217
+ });
218
+ },
219
+ padding: 8,
220
+ }),
221
+ ],
222
+ }).then(({ x, y, strategy }) => {
223
+ Object.assign(hamburgerNav.style, {
224
+ position: strategy,
225
+ left: `${x}px`,
226
+ top: `${y}px`,
227
+ });
228
+ });
229
+ } else {
230
+ // Mobile view (fullscreen overlay), position is handled by CSS
231
+ Object.assign(hamburgerNav.style, { position: "", left: "", top: "" });
232
+ }
233
+ });
234
+ setTimeout(() => document.addEventListener("click", handleOutsideClick), 0);
187
235
  };
188
236
 
189
237
  hamburgerButton.addEventListener("click", (event) => {
@@ -195,13 +243,18 @@ function initEventHandler() {
195
243
  showMenu();
196
244
  }
197
245
  });
246
+
247
+ hamburgerCloseButton.addEventListener("click", (event) => {
248
+ event.stopPropagation();
249
+ hideMenu();
250
+ });
198
251
  }
199
252
 
200
253
  /**
201
- * Attaches a ResizeObserver to re-calculate tabs when the component's size changes.
254
+ * Attaches a ResizeObserver to the main navigation element to recalculate
255
+ * tab distribution on size changes. A DeadMansSwitch is used for debouncing.
202
256
  * @private
203
257
  * @this {SiteNavigation}
204
- * @returns {void}
205
258
  */
206
259
  function attachResizeObserver() {
207
260
  this[resizeObserverSymbol] = new ResizeObserver(() => {
@@ -221,10 +274,9 @@ function attachResizeObserver() {
221
274
  }
222
275
 
223
276
  /**
224
- * Detaches the ResizeObserver when the component is removed from the DOM.
277
+ * Disconnects and cleans up the ResizeObserver instance.
225
278
  * @private
226
279
  * @this {SiteNavigation}
227
- * @returns {void}
228
280
  */
229
281
  function detachResizeObserver() {
230
282
  if (this[resizeObserverSymbol] instanceof ResizeObserver) {
@@ -234,55 +286,125 @@ function detachResizeObserver() {
234
286
  }
235
287
 
236
288
  /**
237
- * Sets up a submenu based on its context (visible nav or hidden hamburger menu).
289
+ * Sets up interaction logic (hover or click) for a submenu.
290
+ * This function is called recursively for nested submenus.
238
291
  * @private
239
292
  * @this {SiteNavigation}
240
- * @param {HTMLElement} parentLi The list item that contains the submenu.
241
- * @param {'visible' | 'hidden'} context The context in which the submenu appears.
242
- * @returns {void}
293
+ * @param {HTMLLIElement} parentLi The list item containing the submenu.
294
+ * @param {'visible'|'hidden'} context The context (main nav or hamburger).
295
+ * @param {number} level The nesting level of the submenu (starts at 1).
243
296
  */
244
- function setupSubmenu(parentLi, context = "visible") {
245
- const submenu = parentLi.querySelector("ul");
297
+ function setupSubmenu(parentLi, context = "visible", level = 1) {
298
+ const submenu = parentLi.querySelector(
299
+ ":scope > ul, :scope > div[part='mega-menu']",
300
+ );
246
301
  if (!submenu) return;
247
302
 
248
- submenu.setAttribute("part", "submenu");
303
+ if (submenu.tagName === "UL") {
304
+ submenu.setAttribute("part", "submenu");
305
+ }
249
306
 
250
- if (context === "visible") {
307
+ const interaction = this.getOption("interactionModel", "auto");
308
+ const useHover =
309
+ interaction === "hover" ||
310
+ (interaction === "auto" && context === "visible");
311
+
312
+ if (useHover) {
251
313
  const component = this;
252
314
  let cleanup;
253
315
  let hideTimeout;
254
316
 
255
317
  const immediateHide = () => {
256
318
  submenu.style.display = "none";
319
+ submenu
320
+ .querySelectorAll(
321
+ "ul[style*='display: block'], div[part='mega-menu'][style*='display: block']",
322
+ )
323
+ .forEach((sub) => {
324
+ sub.style.display = "none";
325
+ });
326
+
327
+ fireCustomEvent(this, "monster-submenu-hide", {
328
+ context,
329
+ trigger: parentLi,
330
+ submenu,
331
+ level,
332
+ });
333
+
257
334
  if (cleanup) {
258
335
  cleanup();
259
336
  cleanup = null;
260
337
  }
261
- if (component[activeSubmenuHiderSymbol] === immediateHide) {
338
+
339
+ if (
340
+ level === 1 &&
341
+ component[activeSubmenuHiderSymbol] === immediateHide
342
+ ) {
262
343
  component[activeSubmenuHiderSymbol] = null;
263
344
  }
264
345
  };
265
346
 
266
347
  const show = () => {
348
+ component[hideHamburgerMenuSymbol]?.();
267
349
  clearTimeout(hideTimeout);
268
- if (
269
- component[activeSubmenuHiderSymbol] &&
270
- component[activeSubmenuHiderSymbol] !== immediateHide
271
- ) {
272
- component[activeSubmenuHiderSymbol]();
350
+
351
+ if (level === 1) {
352
+ if (
353
+ component[activeSubmenuHiderSymbol] &&
354
+ component[activeSubmenuHiderSymbol] !== immediateHide
355
+ ) {
356
+ component[activeSubmenuHiderSymbol]();
357
+ }
358
+ component[activeSubmenuHiderSymbol] = immediateHide;
359
+ } else {
360
+ [...parentLi.parentElement.children]
361
+ .filter((li) => li !== parentLi)
362
+ .forEach((sibling) => {
363
+ const siblingSubmenu = sibling.querySelector(
364
+ ":scope > ul, :scope > div[part='mega-menu']",
365
+ );
366
+ if (siblingSubmenu) {
367
+ siblingSubmenu.style.display = "none";
368
+ }
369
+ });
273
370
  }
371
+
274
372
  submenu.style.display = "block";
275
- if (!cleanup) {
373
+ fireCustomEvent(this, "monster-submenu-show", {
374
+ context,
375
+ trigger: parentLi,
376
+ submenu,
377
+ level,
378
+ });
379
+
380
+ if (level === 1 && !cleanup) {
276
381
  cleanup = autoUpdate(parentLi, submenu, () => {
277
382
  computePosition(parentLi, submenu, {
278
383
  placement: "bottom-start",
279
- middleware: [offset(8), flip(), shift({ padding: 8 })],
280
- }).then(({ x, y }) => {
281
- Object.assign(submenu.style, { left: `${x}px`, top: `${y}px` });
384
+ strategy: "fixed",
385
+ middleware: [
386
+ offset(8),
387
+ flip({ fallbackPlacements: ["top-start"] }),
388
+ shift({ padding: 8 }),
389
+ size({
390
+ apply: ({ availableHeight, elements }) => {
391
+ Object.assign(elements.floating.style, {
392
+ maxHeight: `${availableHeight}px`,
393
+ overflowY: "auto",
394
+ });
395
+ },
396
+ padding: 8,
397
+ }),
398
+ ],
399
+ }).then(({ x, y, strategy }) => {
400
+ Object.assign(submenu.style, {
401
+ position: strategy,
402
+ left: `${x}px`,
403
+ top: `${y}px`,
404
+ });
282
405
  });
283
406
  });
284
407
  }
285
- component[activeSubmenuHiderSymbol] = immediateHide;
286
408
  };
287
409
 
288
410
  const hideWithDelay = () => {
@@ -294,32 +416,83 @@ function setupSubmenu(parentLi, context = "visible") {
294
416
  submenu.addEventListener("mouseenter", () => clearTimeout(hideTimeout));
295
417
  submenu.addEventListener("mouseleave", hideWithDelay);
296
418
  } else {
297
- submenu.style.display = "none";
298
- const anchor = parentLi.querySelector("a");
419
+ // Click behavior
420
+ const anchor = parentLi.querySelector(":scope > a");
299
421
  if (anchor) {
300
422
  anchor.addEventListener("click", (event) => {
301
423
  event.preventDefault();
302
424
  event.stopPropagation();
303
- const isVisible = submenu.style.display === "block";
304
- submenu.style.display = isVisible ? "none" : "block";
425
+
426
+ if (!submenu.classList.contains("is-open")) {
427
+ [...parentLi.parentElement.children]
428
+ .filter((li) => li !== parentLi)
429
+ .forEach((sibling) => {
430
+ const siblingSubmenu = sibling.querySelector(
431
+ ":scope > ul, :scope > div[part='mega-menu']",
432
+ );
433
+ if (siblingSubmenu) {
434
+ siblingSubmenu.classList.remove("is-open");
435
+ }
436
+ });
437
+ }
438
+
439
+ const isOpen = submenu.classList.toggle("is-open");
440
+ const eventName = isOpen
441
+ ? "monster-submenu-show"
442
+ : "monster-submenu-hide";
443
+ fireCustomEvent(this, eventName, {
444
+ context,
445
+ trigger: parentLi,
446
+ submenu,
447
+ level,
448
+ });
305
449
  });
306
450
  }
307
451
  }
452
+
453
+ // Recursive call for nested lists
454
+ if (submenu.tagName === "UL") {
455
+ submenu
456
+ .querySelectorAll(":scope > li")
457
+ .forEach((li) => setupSubmenu.call(this, li, context, level + 1));
458
+ }
459
+ }
460
+
461
+ /**
462
+ * Creates a clone of a navigation list item, setting appropriate part attributes
463
+ * for styling and handling active states.
464
+ * @private
465
+ * @param {HTMLLIElement} item The original list item to clone.
466
+ * @returns {HTMLLIElement} The cloned and configured list item.
467
+ */
468
+ function cloneNavItem(item) {
469
+ const liClone = item.cloneNode(true);
470
+ const aClone = liClone.querySelector("a");
471
+ let navItemPart = "nav-item";
472
+ let navLinkPart = "nav-link";
473
+
474
+ if (item.classList.contains("active")) {
475
+ navItemPart += " nav-item-active";
476
+ if (aClone) navLinkPart += " nav-link-active";
477
+ }
478
+
479
+ liClone.setAttribute("part", navItemPart);
480
+ if (aClone) aClone.setAttribute("part", navLinkPart);
481
+
482
+ return liClone;
308
483
  }
309
484
 
310
485
  /**
311
- * Calculates which navigation items fit in the visible area and moves the rest
312
- * to a hidden "hamburger" menu.
486
+ * Measures available space and distributes slotted navigation items between
487
+ * the visible list and the hidden hamburger menu list.
313
488
  * @private
314
489
  * @this {SiteNavigation}
315
- * @returns {void}
316
490
  */
317
491
  function populateTabs() {
318
492
  const visibleList = this[visibleElementsSymbol];
319
493
  const hiddenList = this[hiddenElementsSymbol];
320
494
  const hamburgerButton = this[hamburgerButtonSymbol];
321
495
  const hamburgerNav = this[hamburgerNavSymbol];
322
-
323
496
  const topLevelUl = [...getSlottedElements.call(this, "ul")].find(
324
497
  (ul) => ul.parentElement === this,
325
498
  );
@@ -369,11 +542,10 @@ function populateTabs() {
369
542
  for (let i = 0; i < sourceItems.length; i++) {
370
543
  const item = sourceItems[i];
371
544
  const itemClone = item.cloneNode(true);
372
- const submenu = itemClone.querySelector("ul");
545
+ const submenu = itemClone.querySelector("ul, div[part='mega-menu']");
373
546
  if (submenu) submenu.style.display = "none";
374
547
 
375
548
  measurementList.appendChild(itemClone);
376
-
377
549
  const isLastItem = i === sourceItems.length - 1;
378
550
  const effectiveMaxWidth = isLastItem ? navWidth : availableWidthForTabs;
379
551
 
@@ -389,71 +561,94 @@ function populateTabs() {
389
561
  const hiddenItems = sourceItems.slice(visibleItems.length);
390
562
 
391
563
  if (visibleItems.length > 0) {
392
- const clonedVisibleItems = visibleItems.map((item) => {
393
- const liClone = item.cloneNode(true);
394
- const aClone = liClone.querySelector("a");
395
-
396
- liClone.setAttribute("part", "nav-item");
397
- if (aClone) aClone.setAttribute("part", "nav-link");
398
-
399
- if (item.classList.contains("active")) {
400
- liClone.setAttribute("part", "nav-item nav-item-active");
401
- if (aClone) aClone.setAttribute("part", "nav-link nav-link-active");
402
- }
403
- return liClone;
404
- });
564
+ const clonedVisibleItems = visibleItems.map(cloneNavItem);
405
565
  visibleList.append(...clonedVisibleItems);
406
566
  visibleList
407
- .querySelectorAll("li")
408
- .forEach((li) => setupSubmenu.call(this, li, "visible"));
567
+ .querySelectorAll(":scope > li")
568
+ .forEach((li) => setupSubmenu.call(this, li, "visible", 1));
409
569
  }
410
570
 
411
571
  if (hiddenItems.length > 0) {
412
- const clonedHiddenItems = hiddenItems.map((item) => {
413
- const liClone = item.cloneNode(true);
414
- const aClone = liClone.querySelector("a");
415
-
416
- liClone.setAttribute("part", "nav-item");
417
- if (aClone) aClone.setAttribute("part", "nav-link");
418
- if (item.classList.contains("active")) {
419
- liClone.setAttribute("part", "nav-item nav-item-active");
420
- if (aClone) aClone.setAttribute("part", "nav-link nav-link-active");
421
- }
422
- return liClone;
423
- });
572
+ const clonedHiddenItems = hiddenItems.map(cloneNavItem);
424
573
  hiddenList.append(...clonedHiddenItems);
425
574
  hamburgerButton.style.display = "flex";
426
575
  hiddenList
427
- .querySelectorAll("li")
428
- .forEach((li) => setupSubmenu.call(this, li, "hidden"));
576
+ .querySelectorAll(":scope > li")
577
+ .forEach((li) => setupSubmenu.call(this, li, "hidden", 1));
429
578
  }
430
579
 
431
580
  visibleList.style.visibility = "visible";
581
+ fireCustomEvent(this, "monster-layout-change", { visibleItems, hiddenItems });
582
+ }
583
+
584
+ /**
585
+ * A simple template literal tag function for clarity.
586
+ * @private
587
+ * @param {TemplateStringsArray} strings
588
+ * @returns {string} The combined string.
589
+ */
590
+ function html(strings) {
591
+ return strings.join("");
432
592
  }
433
593
 
434
594
  /**
435
595
  * Returns the HTML template for the component's shadow DOM.
436
596
  * @private
437
- * @returns {string}
597
+ * @returns {string} The HTML template string.
438
598
  */
439
599
  function getTemplate() {
440
- return `
441
- <div data-monster-role="control" part="control">
442
- <nav data-monster-role="navigation" role="navigation" part="nav">
443
- <ul id="visible-elements" part="visible-list"></ul>
444
- </nav>
445
- <div data-monster-role="hamburger-container" part="hamburger-container">
446
- <button id="hamburger-button" part="hamburger-button" aria-label="More navigation items">
447
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" viewBox="0 0 16 16">
448
- <path fill-rule="evenodd" d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5"/>
600
+ return html`<div data-monster-role="control" part="control">
601
+ <nav data-monster-role="navigation" role="navigation" part="nav">
602
+ <ul id="visible-elements" part="visible-list"></ul>
603
+ </nav>
604
+ <div data-monster-role="hamburger-container" part="hamburger-container">
605
+ <button
606
+ id="hamburger-button"
607
+ part="hamburger-button"
608
+ aria-label="More navigation items"
609
+ >
610
+ <svg
611
+ xmlns="http://www.w3.org/2000/svg"
612
+ width="16"
613
+ height="16"
614
+ fill="currentColor"
615
+ viewBox="0 0 16 16"
616
+ >
617
+ <path
618
+ fill-rule="evenodd"
619
+ d="M2.5 12a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5m0-4a.5.5 0 0 1 .5-.5h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5"
620
+ />
621
+ </svg>
622
+ </button>
623
+ <nav
624
+ data-monster-role="hamburger-nav"
625
+ role="navigation"
626
+ part="hamburger-nav"
627
+ style="display: none;"
628
+ >
629
+ <div part="hamburger-header">
630
+ <button part="hamburger-close-button" aria-label="Close navigation">
631
+ <svg
632
+ xmlns="http://www.w3.org/2000/svg"
633
+ width="24"
634
+ height="24"
635
+ viewBox="0 0 24 24"
636
+ fill="none"
637
+ stroke="currentColor"
638
+ stroke-width="2"
639
+ stroke-linecap="round"
640
+ stroke-linejoin="round"
641
+ >
642
+ <line x1="18" y1="6" x2="6" y2="18"></line>
643
+ <line x1="6" y1="6" x2="18" y2="18"></line>
449
644
  </svg>
450
645
  </button>
451
- <nav data-monster-role="hamburger-nav" role="navigation" part="hamburger-nav" style="display: none;">
452
- <ul id="hidden-elements" part="hidden-list"></ul>
453
- </nav>
454
- </div>
455
- <slot class="hidden-slot" style="display: none;"></slot>
456
- </div>`;
646
+ </div>
647
+ <ul id="hidden-elements" part="hidden-list"></ul>
648
+ </nav>
649
+ </div>
650
+ <slot class="hidden-slot" style="display: none;"></slot>
651
+ </div>`;
457
652
  }
458
653
 
459
654
  registerCustomElement(SiteNavigation);