@lion/ui 0.17.0-menu-system.1 → 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 (63) hide show
  1. package/components/menu/README.md +1 -1
  2. package/components/menu/src/InteractiveListMixin.js +11 -2
  3. package/components/menu/src/MoreButtonMenuMixin.js +296 -0
  4. package/components/navigation-bar/src/LionNavigationBar.js +21 -4
  5. package/components/overlays/src/utils/adopt-styles.js +1 -1
  6. package/custom-elements.json +682 -30
  7. package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -1
  8. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +31 -0
  9. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +1 -0
  10. package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts +1 -1
  11. package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts.map +1 -1
  12. package/dist-types/tsconfig.tsbuildinfo +1 -1
  13. package/docs/components/checkbox-group/overview.md +2 -2
  14. package/docs/components/collapsible/overview.md +1 -1
  15. package/docs/components/combobox/overview.md +1 -1
  16. package/docs/components/combobox/use-cases.md +1 -1
  17. package/docs/components/dialog/overview.md +1 -1
  18. package/docs/components/fieldset/overview.md +5 -5
  19. package/docs/components/form/overview.md +3 -3
  20. package/docs/components/form/use-cases.md +2 -2
  21. package/docs/components/icon/use-cases.md +1 -1
  22. package/docs/components/input/overview.md +1 -1
  23. package/docs/components/input-amount/overview.md +3 -3
  24. package/docs/components/input-date/overview.md +3 -3
  25. package/docs/components/input-datepicker/overview.md +5 -5
  26. package/docs/components/input-email/overview.md +2 -2
  27. package/docs/components/input-file/overview.md +1 -1
  28. package/docs/components/input-iban/overview.md +2 -2
  29. package/docs/components/input-range/overview.md +2 -2
  30. package/docs/components/input-stepper/overview.md +1 -1
  31. package/docs/components/input-tel/overview.md +1 -1
  32. package/docs/components/input-tel/use-cases.md +6 -6
  33. package/docs/components/input-tel-dropdown/overview.md +1 -1
  34. package/docs/components/listbox/use-cases.md +1 -1
  35. package/docs/components/menu/extensions.md +19 -0
  36. package/docs/components/radio-group/overview.md +3 -3
  37. package/docs/components/select/use-cases.md +1 -1
  38. package/docs/components/select-rich/use-cases.md +2 -2
  39. package/docs/components/tabs/examples.md +1 -1
  40. package/docs/fundamentals/index.md +4 -4
  41. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  42. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  43. package/docs/fundamentals/rationales/accessibility.md +1 -1
  44. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  45. package/docs/fundamentals/systems/core/overview.md +1 -1
  46. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  47. package/docs/fundamentals/systems/form/model-value.md +1 -1
  48. package/docs/fundamentals/systems/form/overview.md +28 -28
  49. package/docs/fundamentals/systems/form/validate.md +2 -2
  50. package/docs/fundamentals/systems/localize/overview.md +4 -4
  51. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  52. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  53. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  54. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  55. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  56. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  57. package/docs/guides/how-to/create-a-form.md +4 -4
  58. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  59. package/docs/guides/how-to/index.md +1 -1
  60. package/docs/guides/index.md +8 -8
  61. package/docs/guides/principles/index.md +5 -5
  62. package/docs/guides/principles/styling.md +1 -1
  63. 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`
@@ -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
+ };
@@ -1,6 +1,6 @@
1
1
  // @ts-nocheck
2
2
  /* eslint-disable class-methods-use-this */
3
- import { LitElement, html, css } from 'lit';
3
+ import { LitElement, html, css, nothing } from 'lit';
4
4
  import { LionMenuHybrid } from '@lion/ui/menu.js';
5
5
  import { ScopedElementsMixin } from '../../core/src/ScopedElementsMixin.js';
6
6
 
@@ -104,6 +104,17 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
104
104
  padding-bottom: 200%;
105
105
  }
106
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
+
107
118
  :host([responsive-mode='mobile']) nav {
108
119
  display: flex;
109
120
  flex-direction: column;
@@ -331,7 +342,7 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
331
342
  ${this.responsiveMode === 'desktop'
332
343
  ? this._ctasTemplate(this.ctaPrimary, this.ctaSecondary)
333
344
  : ''}
334
- ${this._menulevelTemplate(this.menuItems, 1, '')}
345
+ ${this._menulevelTemplate(this.menuItems, 1, '', true)}
335
346
  </div>
336
347
  ${this.responsiveMode === 'mobile'
337
348
  ? html`<div class="nav-footer">
@@ -388,11 +399,12 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
388
399
  * @param {number} level
389
400
  * @returns {import('lit').TemplateResult}
390
401
  */
391
- _menulevelTemplate(menuItemsForLevel, level, prevText = '') {
402
+ _menulevelTemplate(menuItemsForLevel, level, prevText = '', itemWrap = false) {
392
403
  // @ts-ignore
393
404
  const cfgForLevel = this._levelCfg[`l${level}`];
394
405
 
395
406
  return html`<lion-menu-hybrid
407
+ ?item-wrap=${itemWrap}
396
408
  .config="${cfgForLevel.openableConfig || {}}"
397
409
  .bar="${cfgForLevel.isBar}"
398
410
  ?data-has-full-width-flyout="${cfgForLevel.hasFullWidthFlyout}"
@@ -417,6 +429,11 @@ export class LionNavigationBar extends ScopedElementsMixin(LitElement) {
417
429
  </div>
418
430
  `,
419
431
  )}
420
- </lion-menu-hybrid>`;
432
+ ${itemWrap
433
+ ? html`<div slot="more-button">
434
+ <button>More</button>
435
+ </div>`
436
+ : nothing}
437
+ </lion-menu-hybrid> `;
421
438
  }
422
439
  }
@@ -125,7 +125,7 @@ export function adoptStyle(renderRoot, style, { teardown = false } = {}) {
125
125
  if (!teardown) {
126
126
  // @ts-ignore
127
127
  // eslint-disable-next-line no-param-reassign
128
- renderRoot.adoptedStyleSheets = [...renderRoot.adoptedStyleSheets, sheet];
128
+ renderRoot.adoptedStyleSheets = [...(renderRoot.adoptedStyleSheets || []), sheet];
129
129
  } else if (renderRoot.adoptedStyleSheets.includes(sheet)) {
130
130
  renderRoot.adoptedStyleSheets.splice(renderRoot.adoptedStyleSheets.indexOf(sheet), 1);
131
131
  }