@lion/ui 0.17.0-menu-system.0 → 0.18.0-menu-system.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 (89) hide show
  1. package/components/menu/README.md +1 -1
  2. package/components/menu/src/InteractiveListMixin.js +14 -5
  3. package/components/menu/src/MoreButtonMenuMixin.js +296 -0
  4. package/components/menu/src/MultiLevelListMixin.js +0 -1
  5. package/components/navigation-bar/src/LionNavigationBar.js +97 -107
  6. package/components/overlays/src/OverlayController.js +87 -38
  7. package/components/overlays/src/OverlayMixin.js +1 -0
  8. package/components/overlays/src/utils/adopt-styles.js +1 -1
  9. package/components/overlays/src/utils/is-equal-config.js +9 -8
  10. package/components/overlays/test/OverlayController.test.js +3 -1
  11. package/components/overlays/test/utils-tests/is-equal-config.test.js +1 -1
  12. package/components/overlays/test-suites/OverlayMixin.suite.js +8 -8
  13. package/components/overlays/types/OverlayConfig.ts +6 -0
  14. package/custom-elements.json +751 -83
  15. package/dist-types/components/dialog/src/LionDialog.d.ts +1 -0
  16. package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
  17. package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -1
  18. package/dist-types/components/menu/src/LionMenuOverlay.d.ts +1 -0
  19. package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +1 -1
  20. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +31 -0
  21. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +1 -0
  22. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -1
  23. package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts +47 -35
  24. package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts.map +1 -1
  25. package/dist-types/components/overlays/src/OverlayController.d.ts +3 -7
  26. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  27. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  28. package/dist-types/components/overlays/src/utils/is-equal-config.d.ts.map +1 -1
  29. package/dist-types/components/overlays/types/OverlayConfig.d.ts +5 -0
  30. package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
  31. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +1 -0
  32. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  33. package/dist-types/exports/overlays.d.ts +1 -0
  34. package/dist-types/exports/overlays.d.ts.map +1 -1
  35. package/dist-types/tsconfig.tsbuildinfo +1 -1
  36. package/docs/components/checkbox-group/overview.md +2 -2
  37. package/docs/components/collapsible/overview.md +1 -1
  38. package/docs/components/combobox/overview.md +1 -1
  39. package/docs/components/combobox/use-cases.md +1 -1
  40. package/docs/components/dialog/overview.md +1 -1
  41. package/docs/components/fieldset/overview.md +5 -5
  42. package/docs/components/form/overview.md +3 -3
  43. package/docs/components/form/use-cases.md +2 -2
  44. package/docs/components/icon/use-cases.md +1 -1
  45. package/docs/components/input/overview.md +1 -1
  46. package/docs/components/input-amount/overview.md +3 -3
  47. package/docs/components/input-date/overview.md +3 -3
  48. package/docs/components/input-datepicker/overview.md +5 -5
  49. package/docs/components/input-email/overview.md +2 -2
  50. package/docs/components/input-file/overview.md +1 -1
  51. package/docs/components/input-iban/overview.md +2 -2
  52. package/docs/components/input-range/overview.md +2 -2
  53. package/docs/components/input-stepper/overview.md +1 -1
  54. package/docs/components/input-tel/overview.md +1 -1
  55. package/docs/components/input-tel/use-cases.md +6 -6
  56. package/docs/components/input-tel-dropdown/overview.md +1 -1
  57. package/docs/components/listbox/use-cases.md +1 -1
  58. package/docs/components/menu/extensions.md +19 -0
  59. package/docs/components/radio-group/overview.md +3 -3
  60. package/docs/components/select/use-cases.md +1 -1
  61. package/docs/components/select-rich/use-cases.md +2 -2
  62. package/docs/components/tabs/examples.md +1 -1
  63. package/docs/fundamentals/index.md +4 -4
  64. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  65. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  66. package/docs/fundamentals/rationales/accessibility.md +1 -1
  67. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  68. package/docs/fundamentals/systems/core/overview.md +1 -1
  69. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  70. package/docs/fundamentals/systems/form/model-value.md +1 -1
  71. package/docs/fundamentals/systems/form/overview.md +28 -28
  72. package/docs/fundamentals/systems/form/validate.md +2 -2
  73. package/docs/fundamentals/systems/localize/overview.md +4 -4
  74. package/docs/fundamentals/systems/overlays/api-table.md +34 -34
  75. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  76. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  77. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  78. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  79. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  80. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  81. package/docs/guides/how-to/create-a-form.md +4 -4
  82. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  83. package/docs/guides/how-to/index.md +1 -1
  84. package/docs/guides/index.md +8 -8
  85. package/docs/guides/principles/index.md +5 -5
  86. package/docs/guides/principles/styling.md +1 -1
  87. package/exports/overlays.js +4 -0
  88. package/overlays.js +4 -0
  89. package/package.json +1 -1
@@ -147,7 +147,7 @@ export const tree = () => html`
147
147
 
148
148
  ## Disclosure menu
149
149
 
150
- See https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-navigation.html
150
+ See [disclosure navigation](https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-navigation.html)
151
151
 
152
152
  ```js preview-story
153
153
  export const disclosureMenu = () => html`
@@ -3,6 +3,7 @@
3
3
  import { html, css, LitElement } from 'lit';
4
4
  import { SlotMixin, DisabledMixin } from '@lion/ui/core.js';
5
5
  import { dedupeMixin } from '@open-wc/dedupe-mixin';
6
+ import { MoreButtonMenuMixin } from './MoreButtonMenuMixin.js';
6
7
  import { uuid } from './utils/uuid.js';
7
8
  import { isInView } from './utils/isInView.js';
8
9
  import {
@@ -125,7 +126,7 @@ customElements.define('lion-menuitem', LionMenuitem);
125
126
  */
126
127
  const InteractiveListMixinImplementation = superclass =>
127
128
  // @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
128
- class InteractiveListMixin extends DisabledMixin(SlotMixin(superclass)) {
129
+ class InteractiveListMixin extends DisabledMixin(SlotMixin(MoreButtonMenuMixin(superclass))) {
129
130
  /** @type {any} */
130
131
  static styles = [
131
132
  css`
@@ -156,13 +157,13 @@ const InteractiveListMixinImplementation = superclass =>
156
157
 
157
158
  static get properties() {
158
159
  return {
159
- // TODO: align with name of open-ui
160
+ // TODO: align with name of open-ui => "inline|block" in focusgroup
160
161
  orientation: { type: String, reflect: true },
161
- // TODO: align with name of platform? (multiple)
162
+ // TODO: align with name of platform => ("multiple" in select, selection not part of focusgroup)
162
163
  multipleChoice: { type: Boolean, attribute: 'multiple-choice' },
163
164
  // TODO: align with name of open-ui
164
165
  selectionFollowsFocus: { type: Boolean, attribute: 'selection-follows-focus' },
165
- // TODO: align with name of open-ui
166
+ // TODO: align with name of open-ui => "wrap" in focsugroup
166
167
  rotateKeyboardNavigation: { type: Boolean, attribute: 'rotate-keyboard-navigation' },
167
168
  // TODO: align with name of open-ui
168
169
  noPreselect: { type: Boolean, attribute: 'no-preselect' },
@@ -463,6 +464,7 @@ const InteractiveListMixinImplementation = superclass =>
463
464
  // console.debug('checking children of', node, node.children);
464
465
  // @ts-ignore - HTMLCollection is iterable
465
466
  for (const child of node.children) {
467
+ // @ts-ignore
466
468
  handleInteractiveListAdditionLevel(child, { newItems, level: level + 1 });
467
469
  }
468
470
  return;
@@ -486,10 +488,16 @@ const InteractiveListMixinImplementation = superclass =>
486
488
 
487
489
  // Move items to _listNode and add interactive node references (with [role]) to listItems
488
490
  nodes.forEach(node => {
489
- // @ts-ignore - node type compatibility
491
+ // @ts-expect-error - node type compatibility
490
492
  handleInteractiveListAdditionLevel(node, { newItems });
491
493
  });
492
494
 
495
+ // @ts-expect-error
496
+ if (this.itemWrap && nodes.length) {
497
+ // @ts-expect-error
498
+ this._initMoreButtonMenu();
499
+ }
500
+
493
501
  newItems.forEach(item => {
494
502
  if (!this.__listItems.includes(item)) {
495
503
  this.__listItems.push(item);
@@ -793,4 +801,5 @@ const InteractiveListMixinImplementation = superclass =>
793
801
  return this.__getNextOption(currentIndex, offset);
794
802
  }
795
803
  };
804
+ // @ts-ignore
796
805
  export const InteractiveListMixin = dedupeMixin(InteractiveListMixinImplementation);
@@ -0,0 +1,296 @@
1
+ // @ts-nocheck
2
+ /**
3
+ * @param {any} superclass
4
+ */
5
+ /** @type {any} */
6
+
7
+ const styles = `
8
+ <style>
9
+ /** More button is hidden by default */
10
+ [data-more-button-wrapper] {
11
+ display: none;
12
+ }
13
+
14
+ [data-more-button-menu]:has([role='listitem'] [active]:focus):not(
15
+ :has([role='listitem'] [active][aria-expanded='true'])
16
+ ) {
17
+ overflow: visible;
18
+ width: auto;
19
+ height: auto;
20
+ position: absolute;
21
+ top: 100%;
22
+ background-color: var(--oj2_bg_default);
23
+ }
24
+ [data-more-button-menu],
25
+ [data-more-button-menu]:has([role='listitem'] [active][aria-expanded='true']) {
26
+ overflow: hidden;
27
+ width: 1px;
28
+ height: 1px;
29
+ }
30
+
31
+ :host([responsive-mode='desktop']) [level='1'] > [slot='list'] > [role='listitem'] > button,
32
+ :host([responsive-mode='desktop']) [level='1'] > [slot='list'] > [role='listitem'] > a,
33
+ :host([responsive-mode='desktop'])
34
+ [level='1']
35
+ > [slot='list']
36
+ > [data-more-button-wrapper]
37
+ > [role='listitem']
38
+ > button,
39
+ :host([responsive-mode='desktop'])
40
+ [level='1']
41
+ > [slot='list']
42
+ > [data-more-button-wrapper]
43
+ > [role='listitem']
44
+ > a {
45
+ white-space: nowrap;
46
+ }
47
+ /**
48
+ * TODO listitem rigth margin/gap is not taken at the account ATM
49
+ */
50
+ /**
51
+ * More button relies on the fact that the menu items do not break the line
52
+ * TODO is it optimal?
53
+ */
54
+ :host([responsive-mode='desktop']) > [role='list'] > [role='listitem'] > button,
55
+ :host([responsive-mode='desktop']) > [role='list'] > [role='listitem'] > a {
56
+ white-space: nowrap;
57
+ }
58
+ </style>
59
+ `;
60
+
61
+ // @ts-ignore - JS mixin typing
62
+ export const MoreButtonMenuMixin = superclass =>
63
+ class MoreButtonMenuMixinClass extends superclass {
64
+ constructor() {
65
+ super();
66
+ this.__resizeTimeout = null;
67
+ this.isMoreButtonShown = null;
68
+ this.handleResize = this.handleResize.bind(this);
69
+ }
70
+
71
+ disconnectedCallback() {
72
+ window.removeEventListener('resize', this.handleResize);
73
+ if (this.__resizeTimeout) {
74
+ clearTimeout(this.__resizeTimeout);
75
+ }
76
+ super.disconnectedCallback?.();
77
+ }
78
+
79
+ _initMoreButtonMenu() {
80
+ this._createMoreButtonWrapper();
81
+ window.addEventListener('resize', this.handleResize);
82
+ this.handleResize();
83
+ }
84
+
85
+ // eslint-disable-next-line class-methods-use-this
86
+ _getDeepActiveElement() {
87
+ let host = document.activeElement || document.body;
88
+ while (host && host.shadowRoot && host.shadowRoot.activeElement) {
89
+ host = host.shadowRoot.activeElement;
90
+ }
91
+ return host;
92
+ }
93
+
94
+ _createMoreButtonWrapper() {
95
+ const moreButtonWrapper = document.createElement('div');
96
+ const style = document.createElement('style');
97
+ style.textContent = styles;
98
+ moreButtonWrapper.appendChild(style);
99
+ moreButtonWrapper.setAttribute('data-more-button-wrapper', '');
100
+ [...this.getMoreButtonSlotProjection().childNodes].forEach(node =>
101
+ moreButtonWrapper.appendChild(node),
102
+ );
103
+ const moreButton = moreButtonWrapper.querySelector('button');
104
+ moreButton?.setAttribute('data-more-button', '');
105
+ moreButton?.setAttribute('tabindex', '-1');
106
+ moreButton?.setAttribute('aria-expanded', 'false');
107
+ const moreButtonMenu = document.createElement('div');
108
+ moreButtonMenu.setAttribute('data-more-button-menu', '');
109
+ moreButtonWrapper.appendChild(moreButtonMenu);
110
+ moreButtonMenu.setAttribute('role', 'none');
111
+ this._listNode.appendChild(moreButtonWrapper);
112
+
113
+ const isElementDirectFocusableItemUnderMoreButtonMenu = element => {
114
+ const targetTagName = element?.tagName;
115
+ return (
116
+ element?.parentElement?.parentElement === moreButtonMenu &&
117
+ (targetTagName === 'BUTTON' || targetTagName === 'A')
118
+ );
119
+ };
120
+
121
+ moreButtonMenu.addEventListener('focusin', event => {
122
+ const target = event?.target;
123
+ if (isElementDirectFocusableItemUnderMoreButtonMenu(target)) {
124
+ if (target?.getAttribute('aria-expanded') === 'true') {
125
+ target?.click();
126
+ }
127
+ moreButton?.setAttribute('aria-expanded', 'true');
128
+ }
129
+ });
130
+
131
+ moreButtonMenu.addEventListener('focusout', event => {
132
+ const target = event?.target;
133
+ if (isElementDirectFocusableItemUnderMoreButtonMenu(target)) {
134
+ moreButton?.setAttribute('aria-expanded', 'false');
135
+ }
136
+ });
137
+
138
+ this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
139
+
140
+ moreButton?.addEventListener('mousedown', () => {
141
+ this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
142
+ const focusableFirstLevelItems = moreButtonMenu.querySelectorAll(
143
+ ':scope > [role="listitem"] > a, :scope > [role="listitem"] > button',
144
+ );
145
+
146
+ [...focusableFirstLevelItems]?.forEach(focusableFirstLevelItem => {
147
+ if (this._getDeepActiveElement() === focusableFirstLevelItem) {
148
+ this.hasMoreButtonMenuAnyFocusedFirstLevelItems = true;
149
+ }
150
+ });
151
+ });
152
+
153
+ moreButton?.addEventListener('click', () => {
154
+ if (!this.hasMoreButtonMenuAnyFocusedFirstLevelItems) {
155
+ moreButtonMenu
156
+ .querySelector(':scope > [role="listitem"] > a, :scope > [role="listitem"] > button')
157
+ ?.focus();
158
+ }
159
+ });
160
+ }
161
+
162
+ getMoreButtonSlotProjection() {
163
+ return this.querySelector('[slot="more-button"]');
164
+ }
165
+
166
+ getMoreButtonMenu() {
167
+ return this.querySelector('[data-more-button-menu]');
168
+ }
169
+
170
+ getMoreButtonMenuWrapper() {
171
+ return this.querySelector('[data-more-button-wrapper]');
172
+ }
173
+
174
+ displayMoreButton() {
175
+ this.getMoreButtonMenuWrapper().style.display = 'block';
176
+ this.isMoreButtonShown = true;
177
+ }
178
+
179
+ hideMoreButton() {
180
+ this.getMoreButtonMenuWrapper().style.display = 'none';
181
+ this.isMoreButtonShown = false;
182
+ }
183
+
184
+ doItemsFit() {
185
+ return this._listNode.scrollWidth - this._listNode.clientWidth === 0;
186
+ }
187
+
188
+ getListItems() {
189
+ const listNode = this._listNode;
190
+ return Array.from(listNode.querySelectorAll(':scope > [role="listitem"]'));
191
+ }
192
+
193
+ /**
194
+ * @param {Node} listItem
195
+ */
196
+ getListItemWithAdjacentNodes = listItem => {
197
+ const markerNodeType = this.nodeType && Node.COMMENT_NODE;
198
+ /** @param {Node | null | undefined} node */
199
+ const isMarkerComment = node => node?.nodeType === markerNodeType && node?.nodeValue === '';
200
+
201
+ const previousNodes = [];
202
+ let { previousSibling } = listItem;
203
+ while (previousSibling) {
204
+ previousNodes.unshift(previousSibling);
205
+ if (isMarkerComment(previousSibling)) {
206
+ break;
207
+ }
208
+ ({ previousSibling } = previousSibling);
209
+ }
210
+
211
+ const nextNodes = [];
212
+ let { nextSibling } = listItem;
213
+ let includeOneMoreAfterComment = false;
214
+ while (nextSibling) {
215
+ nextNodes.push(nextSibling);
216
+
217
+ if (includeOneMoreAfterComment) {
218
+ break;
219
+ }
220
+
221
+ if (isMarkerComment(nextSibling)) {
222
+ includeOneMoreAfterComment = true;
223
+ }
224
+
225
+ ({ nextSibling } = nextSibling);
226
+ }
227
+
228
+ const fragment = document.createDocumentFragment();
229
+ const nodes = [...previousNodes, listItem, ...nextNodes];
230
+ nodes.forEach(node => fragment.appendChild(node));
231
+ return fragment;
232
+ };
233
+
234
+ moveItemToMoreButtonMenuFromMainMenu() {
235
+ const moreButtonMenuElement = this.getMoreButtonMenu();
236
+ const listItems = this._listNode.querySelectorAll(':scope > [role="listitem"]');
237
+ const listItem = listItems[listItems.length - 1];
238
+
239
+ const fragment = this.getListItemWithAdjacentNodes(listItem);
240
+ moreButtonMenuElement.prepend(fragment);
241
+ listItem.style.display = '';
242
+ this.displayMoreButton();
243
+ }
244
+
245
+ moveAllItemsToMainMenuFromMoreButtonMenu() {
246
+ const moreButtonMenu = this.getMoreButtonMenu();
247
+ const moreButtonMenuWrapper = this.getMoreButtonMenuWrapper();
248
+ if (moreButtonMenu.childNodes.length === 0) {
249
+ return;
250
+ }
251
+ [...moreButtonMenu.childNodes].forEach(node => {
252
+ moreButtonMenuWrapper.before(node);
253
+ });
254
+ }
255
+
256
+ renderAllItemsInMainMenu() {
257
+ this.moveAllItemsToMainMenuFromMoreButtonMenu();
258
+ }
259
+
260
+ hideItemsOneByOneInMainMenuUntilTheyFit() {
261
+ let hiddenItemsCount = 0;
262
+ const mainMenuItems = this.getListItems();
263
+ for (let i = mainMenuItems.length - 1; i >= 0 && !this.doItemsFit(); i -= 1) {
264
+ mainMenuItems[i].style.display = 'none';
265
+ hiddenItemsCount += 1;
266
+ }
267
+ return hiddenItemsCount;
268
+ }
269
+
270
+ moveHiddenItemsFromMainMenuToMoreButtonMenu(hiddenItemsCount) {
271
+ for (let i = 0; i < hiddenItemsCount; i += 1) {
272
+ this.moveItemToMoreButtonMenuFromMainMenu();
273
+ }
274
+ }
275
+
276
+ handleResize() {
277
+ if (this.__resizeTimeout) {
278
+ return;
279
+ }
280
+
281
+ this.__resizeTimeout = setTimeout(() => {
282
+ this.__resizeTimeout = null;
283
+
284
+ this.renderAllItemsInMainMenu();
285
+ this.hideMoreButton();
286
+
287
+ if (this.doItemsFit()) {
288
+ return;
289
+ }
290
+
291
+ this.displayMoreButton();
292
+ const hiddenItemsCount = this.hideItemsOneByOneInMainMenuUntilTheyFit();
293
+ this.moveHiddenItemsFromMainMenuToMoreButtonMenu(hiddenItemsCount);
294
+ }, 50);
295
+ }
296
+ };
@@ -207,7 +207,6 @@ const MultiLevelListMixinImplementation = superclass =>
207
207
  switch (key) {
208
208
  case 'Enter':
209
209
  case ' ':
210
- // console.debug('activeIndex', this.activeIndex);
211
210
  // make it work like a button
212
211
  this.activeItem?.click();
213
212
  break;
@@ -1,4 +1,6 @@
1
- import { LitElement, html, css } from 'lit';
1
+ // @ts-nocheck
2
+ /* eslint-disable class-methods-use-this */
3
+ import { LitElement, html, css, nothing } from 'lit';
2
4
  import { LionMenuHybrid } from '@lion/ui/menu.js';
3
5
  import { ScopedElementsMixin } from '../../core/src/ScopedElementsMixin.js';
4
6
 
@@ -102,6 +104,17 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
102
104
  padding-bottom: 200%;
103
105
  }
104
106
 
107
+ /**
108
+ * TODO remove it. Use it now for testing More button feature
109
+ */
110
+ :host([responsive-mode='desktop']) [level='1'] > [slot='list'] > [role='listitem'],
111
+ :host([responsive-mode='desktop'])
112
+ [level='1']
113
+ > [slot='list']
114
+ > [data-more-button-wrapper] {
115
+ margin-right: 180px;
116
+ }
117
+
105
118
  :host([responsive-mode='mobile']) nav {
106
119
  display: flex;
107
120
  flex-direction: column;
@@ -157,101 +170,25 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
157
170
 
158
171
  this.prefilledSuggestion = '';
159
172
 
160
- // Assign styles as a property, so that subclasses can augment it
161
- // this.styles = styles;
162
- // this.menuOpened = false;
163
-
164
173
  const mql = window.matchMedia(`(width <= ${this.breakpointMin}px)`);
165
174
  /** @type {NavBarResponsiveMode} */
166
175
  this.responsiveMode = mql.matches ? 'mobile' : 'desktop';
167
- this._levelCfg = LionNavigationBar.#getLevelCfg(this.responsiveMode);
168
- this._levelSecondaryCfg = this.#getSecondaryLevelCfg();
176
+ this._levelCfg = this._getLevelCfg(this.responsiveMode);
177
+ this._levelSecondaryCfg = this._getSecondaryLevelCfg(this.responsiveMode);
169
178
 
170
179
  mql.addEventListener('change', () => {
171
180
  this.responsiveMode = mql.matches ? 'mobile' : 'desktop';
172
- this._levelCfg = LionNavigationBar.#getLevelCfg(this.responsiveMode);
173
- this._levelSecondaryCfg = this.#getSecondaryLevelCfg();
181
+ this._levelCfg = this._getLevelCfg(this.responsiveMode);
182
+ this._levelSecondaryCfg = this._getSecondaryLevelCfg(this.responsiveMode);
174
183
  });
175
184
 
176
185
  this.#storeLatestFocusedElementId();
177
- this.#delegateAnalyticsEvents();
178
- }
179
-
180
- #getSecondaryLevelCfg() {
181
- if (this.responsiveMode === 'mobile') {
182
- return {
183
- l1: {
184
- isBar: true,
185
- },
186
- l2: {
187
- isBar: false,
188
- },
189
- };
190
- }
191
-
192
- return {
193
- l1: {
194
- isBar: true,
195
- },
196
- l2: {
197
- isBar: false,
198
- },
199
- };
200
- }
201
-
202
- /** @type {string | null} */
203
- #latestFocusedElementId = null;
204
-
205
- #storeLatestFocusedElementId() {
206
- this.addEventListener('focusin', ({ target }) => {
207
- // @ts-ignore
208
- this.#latestFocusedElementId = target.id;
209
- });
210
- }
211
-
212
- #syncLatestFocusedElementForNewResponsiveMode() {
213
- // We might show/hide or rerender some elements (with same ids) in different positions in the DOM.
214
- // As these are physically different elements, we must move focus (as otherwise it would be reset to body)
215
- if (!this.#latestFocusedElementId) return;
216
- const newFocusedElement = this.shadowRoot?.querySelector(`#${this.#latestFocusedElementId}`);
217
- if (newFocusedElement instanceof HTMLElement) {
218
- newFocusedElement.focus();
219
- }
220
- }
221
-
222
- #delegateAnalyticsEvents() {
223
- this.addEventListener('click', ({ target }) => {
224
- if (!target || !(target instanceof HTMLElement)) return;
225
-
226
- // @ts-expect-error
227
- const isButton = target.tagName === 'BUTTON' || target.constructor._$isLionButton$;
228
- const isAnchor = target.tagName === 'A';
229
- if (!isButton && !isAnchor) return;
230
-
231
- if (!target.id) return;
232
-
233
- // TODO: do in extension layer
234
- this.dispatchEvent(
235
- new CustomEvent('nav-item-click', {
236
- // N.B. we can also retrive the item
237
- detail: {
238
- item: {
239
- id: target.id,
240
- label: target.textContent?.trim() || '',
241
- href: isAnchor ? target.getAttribute('href') : undefined,
242
- },
243
- },
244
- // bubbles: true,
245
- // composed: true,
246
- }),
247
- );
248
- });
249
186
  }
250
187
 
251
188
  /**
252
189
  * @param {NavBarResponsiveMode} responsiveMode
253
190
  */
254
- static #getLevelCfg(responsiveMode) {
191
+ _getLevelCfg(responsiveMode) {
255
192
  // N.B. we're fighting some of the overlay configs provided by LionMenuOverlay here,
256
193
  // the idea is to take over all of that when depending on LionMenu, after removing LonMenuOverlay and LionMenuHybrid components altogether.
257
194
 
@@ -322,12 +259,66 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
322
259
  };
323
260
  }
324
261
 
262
+ _getSecondaryLevelCfg(responsiveMode) {
263
+ if (responsiveMode === 'mobile') {
264
+ return {
265
+ l1: {
266
+ isBar: true,
267
+ },
268
+ l2: {
269
+ isBar: false,
270
+ },
271
+ };
272
+ }
273
+
274
+ return {
275
+ l1: {
276
+ isBar: true,
277
+ },
278
+ l2: {
279
+ isBar: false,
280
+ },
281
+ };
282
+ }
283
+
284
+ /** @type {string | null} */
285
+ #latestFocusedElementId = null;
286
+
287
+ #storeLatestFocusedElementId() {
288
+ this.addEventListener('focusin', ({ target }) => {
289
+ // @ts-ignore
290
+ this.#latestFocusedElementId = target.id;
291
+ });
292
+ }
293
+
294
+ #syncLatestFocusedElementForNewResponsiveMode() {
295
+ // We might show/hide or rerender some elements (with same ids) in different positions in the DOM.
296
+ // As these are physically different elements, we must move focus (as otherwise it would be reset to body)
297
+ if (!this.#latestFocusedElementId) return;
298
+ const newFocusedElement = this.shadowRoot?.querySelector(`#${this.#latestFocusedElementId}`);
299
+ if (newFocusedElement instanceof HTMLElement) {
300
+ newFocusedElement.focus();
301
+ }
302
+ }
303
+
304
+ /**
305
+ * @param {import("./types.js").MenuItem}
306
+ * @param {number} level
307
+ */
308
+ _getId(menuItem, level) {
309
+ if (menuItem.id) return menuItem.id;
310
+ const title = menuItem.title || menuItem.label || 'item';
311
+ return `${
312
+ Number.isInteger(level) ? `l${level}-${title}` : title
313
+ }-${Math.random().toString(16).slice(2)}`;
314
+ }
315
+
325
316
  /**
326
317
  * @param {import('lit').PropertyValues} changedProperties
327
318
  */
328
319
  updated(changedProperties) {
329
320
  if (changedProperties.has('responsiveMode')) {
330
- this._levelCfg = LionNavigationBar.#getLevelCfg(this.responsiveMode);
321
+ this._levelCfg = this._getLevelCfg(this.responsiveMode);
331
322
  this.#syncLatestFocusedElementForNewResponsiveMode();
332
323
  }
333
324
  }
@@ -344,18 +335,18 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
344
335
  ? html`<button data-close data-level="1">✖️</button>`
345
336
  : ''}
346
337
  </div>
347
- ${LionNavigationBar._searchTemplate(this.suggestions, this.prefilledSuggestion)}
338
+ ${this._searchTemplate()}
348
339
  </div>
349
340
  <div class="nav-body">
350
- ${this._menulevelTemplate(this.menuSupportItems, 1, this._levelSecondaryCfg.l1, '')}
341
+ ${this._menulevelTemplate(this.menuSupportItems, 1, '')}
351
342
  ${this.responsiveMode === 'desktop'
352
- ? LionNavigationBar._ctaTemplate(this.ctaPrimary, this.ctaSecondary)
343
+ ? this._ctasTemplate(this.ctaPrimary, this.ctaSecondary)
353
344
  : ''}
354
- ${this._menulevelTemplate(this.menuItems, 1, this._levelCfg.l1, '')}
345
+ ${this._menulevelTemplate(this.menuItems, 1, '', true)}
355
346
  </div>
356
347
  ${this.responsiveMode === 'mobile'
357
348
  ? html`<div class="nav-footer">
358
- ${LionNavigationBar._ctaTemplate(this.ctaPrimary, this.ctaSecondary)}
349
+ ${this._ctasTemplate(this.ctaPrimary, this.ctaSecondary)}
359
350
  </div>`
360
351
  : ''}
361
352
  </nav>
@@ -366,7 +357,7 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
366
357
  * @param {string[]} suggestions
367
358
  * @returns {import('lit').TemplateResult}
368
359
  */
369
- static _searchTemplate(suggestions, prefilledSuggestion = '') {
360
+ _searchTemplate(suggestions = this.suggestions, prefilledSuggestion = this.prefilledSuggestion) {
370
361
  return html` <form role="search">
371
362
  <input
372
363
  type="search"
@@ -386,16 +377,18 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
386
377
  * @param {CtaLink} ctaSecondary
387
378
  * @returns {import('lit').TemplateResult}
388
379
  */
389
- static _ctaTemplate(ctaPrimary, ctaSecondary) {
380
+ _ctasTemplate(ctaPrimary, ctaSecondary) {
390
381
  return html`<div class="cta-items">
391
382
  ${ctaSecondary?.href
392
383
  ? html`<div class="cta-secondary">
393
- <a id="${ctaSecondary.id}" href="${ctaSecondary.href}">${ctaSecondary.label} </a>
384
+ <a id="${this._getId(ctaSecondary)}" href="${ctaSecondary.href}"
385
+ >${ctaSecondary.label}
386
+ </a>
394
387
  </div>`
395
388
  : ''}
396
389
  ${ctaPrimary?.href
397
390
  ? html`<div class="cta-primary">
398
- <a id="${ctaPrimary.id}" href="${ctaPrimary.href}">${ctaPrimary.label} </a>
391
+ <a id="${this._getId(ctaPrimary)}" href="${ctaPrimary.href}"> ${ctaPrimary.label}</a>
399
392
  </div>`
400
393
  : ''}
401
394
  </div>`;
@@ -404,17 +397,14 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
404
397
  /**
405
398
  * @param {MenuItem[]} menuItemsForLevel
406
399
  * @param {number} level
407
- * @param {LevelConfig} cfgForLevel
408
400
  * @returns {import('lit').TemplateResult}
409
401
  */
410
- _menulevelTemplate(menuItemsForLevel, level, cfgForLevel, prevText = '') {
402
+ _menulevelTemplate(menuItemsForLevel, level, prevText = '', itemWrap = false) {
411
403
  // @ts-ignore
412
- const cfgForNextLevel = this._levelCfg[`l${level + 1}`];
413
-
414
- const getId = (/** @type {import("./types.js").MenuItem} */ menuItem) =>
415
- menuItem.id || `l${level}-${menuItem.title}`;
404
+ const cfgForLevel = this._levelCfg[`l${level}`];
416
405
 
417
406
  return html`<lion-menu-hybrid
407
+ ?item-wrap=${itemWrap}
418
408
  .config="${cfgForLevel.openableConfig || {}}"
419
409
  .bar="${cfgForLevel.isBar}"
420
410
  ?data-has-full-width-flyout="${cfgForLevel.hasFullWidthFlyout}"
@@ -429,21 +419,21 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
429
419
  menuItem => html`
430
420
  <div role="listitem">
431
421
  ${menuItem.sub
432
- ? html` <button id="${getId(menuItem)}" data-invoker>${menuItem.title}</button>
422
+ ? html` <button id="${this._getId(menuItem)}" data-invoker>${menuItem.title}</button>
433
423
  ${menuItem.sub
434
- ? this._menulevelTemplate(
435
- menuItem.sub,
436
- level + 1,
437
- cfgForNextLevel,
438
- menuItem.title,
439
- )
424
+ ? this._menulevelTemplate(menuItem.sub, level + 1, menuItem.title)
440
425
  : ''}`
441
- : html` <a id="${getId(menuItem)}" href="${menuItem.link || ''}"
426
+ : html` <a id="${this._getId(menuItem)}" href="${menuItem.link || ''}"
442
427
  >${menuItem.title}</a
443
428
  >`}
444
429
  </div>
445
430
  `,
446
431
  )}
447
- </lion-menu-hybrid>`;
432
+ ${itemWrap
433
+ ? html`<div slot="more-button">
434
+ <button>More</button>
435
+ </div>`
436
+ : nothing}
437
+ </lion-menu-hybrid> `;
448
438
  }
449
439
  }