@lion/ui 0.18.0-menu-system.2 → 0.18.0-menu-system.5

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 (88) hide show
  1. package/components/collapsible/test-suites/DisclosureMixin.suite.js +30 -0
  2. package/components/menu/src/InteractiveListMixin.js +57 -24
  3. package/components/menu/src/LionMenu.js +1 -1
  4. package/components/menu/src/LionToolbar.js +1 -1
  5. package/components/menu/src/MoreButtonMenuMixin.js +79 -92
  6. package/components/menu/src/MultiLevelListMixin.js +1 -1
  7. package/components/menu/src/utils/listItemInteractions.js +25 -6
  8. package/components/menu/test/lion-menu.test.js +2 -1
  9. package/components/menu/test-suites/InteractiveListMixin.suite.js +164 -2
  10. package/components/navigation-bar/src/LionNavigationBar.js +2 -2
  11. package/custom-elements.json +354 -54
  12. package/dist-types/components/collapsible/test-suites/DisclosureMixin.suite.d.ts +2 -0
  13. package/dist-types/components/collapsible/test-suites/DisclosureMixin.suite.d.ts.map +1 -0
  14. package/dist-types/components/menu/src/InteractiveListMixin.d.ts +11 -5
  15. package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -1
  16. package/dist-types/components/menu/src/LionMenu.d.ts +4 -2
  17. package/dist-types/components/menu/src/LionMenu.d.ts.map +1 -1
  18. package/dist-types/components/menu/src/LionMenuOverlay.d.ts +8 -4
  19. package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +1 -1
  20. package/dist-types/components/menu/src/LionTree.d.ts +4 -2
  21. package/dist-types/components/menu/src/LionTree.d.ts.map +1 -1
  22. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +11 -2
  23. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +1 -1
  24. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts +4 -2
  25. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -1
  26. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts +8 -2
  27. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts.map +1 -1
  28. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts +6 -5
  29. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +1 -1
  30. package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts +4 -4
  31. package/dist-types/exports/menu-test-suites.d.ts +2 -0
  32. package/dist-types/exports/menu-test-suites.d.ts.map +1 -0
  33. package/dist-types/tsconfig.tsbuildinfo +1 -1
  34. package/docs/components/checkbox-group/overview.md +2 -2
  35. package/docs/components/collapsible/overview.md +1 -1
  36. package/docs/components/combobox/overview.md +1 -1
  37. package/docs/components/combobox/use-cases.md +1 -1
  38. package/docs/components/dialog/overview.md +1 -1
  39. package/docs/components/fieldset/overview.md +5 -5
  40. package/docs/components/form/overview.md +3 -3
  41. package/docs/components/form/use-cases.md +2 -2
  42. package/docs/components/icon/use-cases.md +1 -1
  43. package/docs/components/input/overview.md +1 -1
  44. package/docs/components/input-amount/overview.md +3 -3
  45. package/docs/components/input-date/overview.md +3 -3
  46. package/docs/components/input-datepicker/overview.md +5 -5
  47. package/docs/components/input-email/overview.md +2 -2
  48. package/docs/components/input-file/overview.md +1 -1
  49. package/docs/components/input-iban/overview.md +2 -2
  50. package/docs/components/input-range/overview.md +2 -2
  51. package/docs/components/input-stepper/overview.md +1 -1
  52. package/docs/components/input-tel/overview.md +1 -1
  53. package/docs/components/input-tel/use-cases.md +6 -6
  54. package/docs/components/input-tel-dropdown/overview.md +1 -1
  55. package/docs/components/listbox/use-cases.md +1 -1
  56. package/docs/components/menu/api-table.md +197 -192
  57. package/docs/components/menu/extensions.md +27 -0
  58. package/docs/components/menu/overview.md +39 -39
  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/configuration.md +2 -2
  75. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  76. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  77. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  78. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  79. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  80. package/docs/guides/how-to/create-a-form.md +4 -4
  81. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  82. package/docs/guides/how-to/index.md +1 -1
  83. package/docs/guides/index.md +8 -8
  84. package/docs/guides/principles/index.md +5 -5
  85. package/docs/guides/principles/styling.md +1 -1
  86. package/exports/menu-test-suites.js +2 -0
  87. package/menu-test-suites.js +2 -0
  88. package/package.json +1 -1
@@ -0,0 +1,30 @@
1
+ // import { LitElement } from 'lit';
2
+ // import { expect, html, fixture as _fixture, unsafeStatic, defineCE } from '@open-wc/testing';
3
+ // import { DisclosureMixin } from '../src/DisclosureMixin.js';
4
+
5
+ // import '@lion/ui/define/lion-menu.js';
6
+ // import '@lion/ui/define/lion-item.js';
7
+
8
+ // class DisclosureClass extends DisclosureMixin(LitElement) {}
9
+
10
+ // /**
11
+ // * @typedef {import('lit').TemplateResult} TemplateResult
12
+ // */
13
+
14
+ // const fixture = /** @type {(arg: TemplateResult) => Promise<LionMenu>} */ (_fixture);
15
+
16
+ // /**
17
+ // * @param { {tagString?:string, itemTagString?:string} } [customConfig]
18
+ // */
19
+ // export function runDisclosureMixinSuite(customConfig = {}) {
20
+ // const cfg = {
21
+ // tagString: 'lion-menu',
22
+ // ...customConfig,
23
+ // };
24
+
25
+ // const tagString = cfg.tagString || defineCE(DisclosureClass);
26
+ // const tag = unsafeStatic(tagString);
27
+
28
+ // describe('DisclosureMixin', () => {
29
+ // });
30
+ // }
@@ -22,7 +22,7 @@ import {
22
22
 
23
23
  // TODO: consider renaming to FocusGroupMixin
24
24
 
25
- // TODO: make all available in controller/directve (same logic with an elegent prop-to-host-mapping)
25
+ // TODO: make all available in controller/directive (same logic with an elegant prop-to-host-mapping)
26
26
 
27
27
  /**
28
28
  * @param {Element} potentialFocusable
@@ -55,13 +55,25 @@ function isFocusableElement(potentialFocusable) {
55
55
  export class LionItem extends LitElement {
56
56
  /** @type {any} */
57
57
  static get properties() {
58
- return { type: String };
58
+ return {
59
+ type: { type: String },
60
+ };
59
61
  }
60
62
 
61
63
  get _invokerNode() {
62
- return Array.from(this.children).find(child => child.slot === 'invoker');
64
+ return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
65
+ child => child.slot === 'invoker',
66
+ );
63
67
  }
64
68
 
69
+ static styles = [
70
+ css`
71
+ :host {
72
+ display: block;
73
+ }
74
+ `,
75
+ ];
76
+
65
77
  constructor() {
66
78
  super();
67
79
  /** @type {InteractiveListItemRole |''} */
@@ -81,14 +93,6 @@ export class LionItem extends LitElement {
81
93
  }
82
94
  }
83
95
 
84
- static styles = [
85
- css`
86
- :host {
87
- display: block;
88
- }
89
- `,
90
- ];
91
-
92
96
  render() {
93
97
  return html`
94
98
  <slot name="invoker"></slot>
@@ -332,11 +336,10 @@ const InteractiveListMixinImplementation = superclass =>
332
336
  setCheckedIndex(index) {
333
337
  const item = this.listItems[index];
334
338
  if (!item) return;
335
-
336
339
  if (!this.multipleChoice) {
337
340
  // Uncheck all
338
341
  this.listItems.forEach(listItem => {
339
- setChecked(listItem, false);
342
+ setChecked(listItem, true);
340
343
  });
341
344
  setChecked(this.listItems[index]);
342
345
  } else {
@@ -377,7 +380,7 @@ const InteractiveListMixinImplementation = superclass =>
377
380
  * - [role="menubar"](https://www.w3.org/TR/wai-aria-practices-1.1/examples/menubar/menubar-1/menubar-1.html)
378
381
  * - [role="tree"](https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-1/treeview-1b.html)
379
382
  * For disclosure menus, we need keyboard navigation, but treat every list item
380
- * (more precisely its anchor child) as a tab stop. Also we apply aria-current
383
+ * (more precisely its anchor child) as a tab stop.
381
384
  * @type {'activedescendant'|'roving-tabindex'|'tabbable-disclosure'|'none'}
382
385
  */
383
386
  this._activeMode = 'activedescendant';
@@ -426,6 +429,16 @@ const InteractiveListMixinImplementation = superclass =>
426
429
  this._onListFocusIn = this._onListFocusIn.bind(this);
427
430
  }
428
431
 
432
+ connectedCallback() {
433
+ super.connectedCallback();
434
+ window.addEventListener('popstate', this._onPopState);
435
+ }
436
+
437
+ disconnectedCallback() {
438
+ super.disconnectedCallback();
439
+ window.removeEventListener('popstate', this._onPopState);
440
+ }
441
+
429
442
  /**
430
443
  * @param {import('lit').PropertyValues } changedProperties
431
444
  */
@@ -457,11 +470,10 @@ const InteractiveListMixinImplementation = superclass =>
457
470
  * menuitemradios or -checkboxes.
458
471
  * When no role at all, we might deal with a wrapped node (for instance triggering a
459
472
  * submenu)
460
- * Alternatively, when _activeMode is ['disclosure'](https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-navigation.html),
473
+ * Alternatively, when _activeMode is ['tabbable-disclosure'](https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-navigation.html),
461
474
  * we search for a[href]
462
475
  */
463
476
  if (node.getAttribute('role') === 'group') {
464
- // console.debug('checking children of', node, node.children);
465
477
  // @ts-ignore - HTMLCollection is iterable
466
478
  for (const child of node.children) {
467
479
  // @ts-ignore
@@ -568,6 +580,15 @@ const InteractiveListMixinImplementation = superclass =>
568
580
  if (!this.noPreselect && this.checkedIndex === -1) {
569
581
  this.checkedIndex = 0;
570
582
  }
583
+ newItems.forEach(item => {
584
+ if (!item.hasAttribute('checked')) {
585
+ setChecked(item, true);
586
+ } else {
587
+ setChecked(item);
588
+ }
589
+ });
590
+
591
+ this.__syncCurrentPageWithLocationHref(window.location);
571
592
  }
572
593
 
573
594
  /**
@@ -706,16 +727,8 @@ const InteractiveListMixinImplementation = superclass =>
706
727
  // const item = /** @type {HTMLElement} */ (target.closest('[role]'));
707
728
  // @ts-ignore - target is HTMLElement
708
729
  const foundIndex = this.listItems.indexOf(target);
709
- // console.debug('focusin', target, foundIndex, this.listItems);
710
- const previousActiveIndex = this.activeIndex;
711
730
  if (foundIndex > -1 && foundIndex !== this.activeIndex) {
712
731
  this.activeIndex = foundIndex;
713
- if (this._activeMode === 'tabbable-disclosure') {
714
- const previousActiveItem = this.listItems[previousActiveIndex];
715
- previousActiveItem?.removeAttribute('aria-current');
716
- const activeItem = this.listItems[this.activeIndex];
717
- activeItem.setAttribute('aria-current', 'page');
718
- }
719
732
  }
720
733
  }
721
734
 
@@ -800,6 +813,26 @@ const InteractiveListMixinImplementation = superclass =>
800
813
  _getPreviousEnabledOption(currentIndex, offset = -1) {
801
814
  return this.__getNextOption(currentIndex, offset);
802
815
  }
816
+
817
+ /**
818
+ * @param {Location} location
819
+ */
820
+ __syncCurrentPageWithLocationHref(location) {
821
+ this.listItems.forEach(item => {
822
+ if (/** @type {HTMLAnchorElement} */ (item).href) {
823
+ if (location.href.includes(/** @type {HTMLAnchorElement} */ (item).href)) {
824
+ setChecked(item);
825
+ } else {
826
+ setChecked(item, true);
827
+ }
828
+ }
829
+ });
830
+ }
831
+
832
+ /** @protected */
833
+ _onPopState = () => {
834
+ this.__syncCurrentPageWithLocationHref(document.location);
835
+ };
803
836
  };
804
837
  // @ts-ignore
805
838
  export const InteractiveListMixin = dedupeMixin(InteractiveListMixinImplementation);
@@ -189,7 +189,7 @@ export class LionMenu extends MultiLevelListMixin(LitElement) {
189
189
  if (!multiple) {
190
190
  // Uncheck all
191
191
  listItemsWithinGroup.forEach(listItem => {
192
- setChecked(listItem, false);
192
+ setChecked(listItem, true);
193
193
  });
194
194
  setChecked(this.listItems[index]);
195
195
  } else {
@@ -60,7 +60,7 @@ export class LionToolbar extends LionMenu {
60
60
  if (!multiple) {
61
61
  // Uncheck all
62
62
  listItemsWithinGroup.forEach(listItem => {
63
- setChecked(listItem, false);
63
+ setChecked(listItem, true);
64
64
  });
65
65
  setChecked(this.listItems[index]);
66
66
  } else {
@@ -1,62 +1,31 @@
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
- `;
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+ import { css } from 'lit';
3
+
4
+ const styles = css`
5
+ /** More button is hidden by default */
6
+ [data-more-button-wrapper] {
7
+ display: none;
8
+ }
9
+
10
+ [data-more-button-menu][data-open]:not(:has([role='listitem'] [aria-expanded='true'])) {
11
+ position: absolute;
12
+ overflow: visible;
13
+ height: auto;
14
+ width: auto;
15
+ top: 100%;
16
+ }
17
+
18
+ /** N.B. this is a simplified version of sr-only. when we move to overlayctrl, we should build this into overlayctrl */
19
+ [data-more-button-menu] {
20
+ overflow: hidden;
21
+ height: 1px;
22
+ width: 1px;
23
+ }
24
+
25
+ [role='list']:has(> [data-more-button-wrapper]) > [role='listitem'] > :is(button, a) {
26
+ white-space: nowrap;
27
+ }
28
+ `;
60
29
 
61
30
  // @ts-ignore - JS mixin typing
62
31
  export const MoreButtonMenuMixin = superclass =>
@@ -85,7 +54,7 @@ export const MoreButtonMenuMixin = superclass =>
85
54
  // eslint-disable-next-line class-methods-use-this
86
55
  _getDeepActiveElement() {
87
56
  let host = document.activeElement || document.body;
88
- while (host && host.shadowRoot && host.shadowRoot.activeElement) {
57
+ while (host?.shadowRoot?.activeElement) {
89
58
  host = host.shadowRoot.activeElement;
90
59
  }
91
60
  return host;
@@ -94,7 +63,7 @@ export const MoreButtonMenuMixin = superclass =>
94
63
  _createMoreButtonWrapper() {
95
64
  const moreButtonWrapper = document.createElement('div');
96
65
  const style = document.createElement('style');
97
- style.textContent = styles;
66
+ style.textContent = styles.cssText;
98
67
  moreButtonWrapper.appendChild(style);
99
68
  moreButtonWrapper.setAttribute('data-more-button-wrapper', '');
100
69
  [...this.getMoreButtonSlotProjection().childNodes].forEach(node =>
@@ -107,32 +76,38 @@ export const MoreButtonMenuMixin = superclass =>
107
76
  const moreButtonMenu = document.createElement('div');
108
77
  moreButtonMenu.setAttribute('data-more-button-menu', '');
109
78
  moreButtonWrapper.appendChild(moreButtonMenu);
110
- moreButtonMenu.setAttribute('role', 'none');
111
79
  this._listNode.appendChild(moreButtonWrapper);
112
80
 
113
- const isElementDirectFocusableItemUnderMoreButtonMenu = element => {
114
- const targetTagName = element?.tagName;
115
- return (
116
- element?.parentElement?.parentElement === moreButtonMenu &&
117
- (targetTagName === 'BUTTON' || targetTagName === 'A')
118
- );
119
- };
81
+ const isElementDirectFocusableItemUnderMoreButtonMenu = (/** @type {Element} */ element) =>
82
+ this.listItems.includes(element) && moreButtonMenu.contains(element);
120
83
 
121
- moreButtonMenu.addEventListener('focusin', event => {
122
- const target = event?.target;
123
- if (isElementDirectFocusableItemUnderMoreButtonMenu(target)) {
84
+ moreButtonMenu.addEventListener(
85
+ 'focusin',
86
+ /** @type {FocusEvent} */ event => {
87
+ const target = /** @type {HTMLElement} */ (event?.target);
88
+ if (!isElementDirectFocusableItemUnderMoreButtonMenu(target)) return;
89
+
90
+ // A user did `Shift + Tab` from L2 menu to More menu
124
91
  if (target?.getAttribute('aria-expanded') === 'true') {
92
+ // close L2 menu
125
93
  target?.click();
126
94
  }
127
95
  moreButton?.setAttribute('aria-expanded', 'true');
128
- }
129
- });
96
+ moreButtonMenu.setAttribute('data-open', '');
97
+ },
98
+ );
130
99
 
131
- moreButtonMenu.addEventListener('focusout', event => {
132
- const target = event?.target;
133
- if (isElementDirectFocusableItemUnderMoreButtonMenu(target)) {
100
+ moreButtonMenu.addEventListener('focusout', () => {
101
+ // We need this timeout for Safari/WebKit.
102
+ // Unfortunately, an in progress click gets canceled when overflow:hidden is applied.
103
+ // Although mousedown would work instead, the consuming developer should have the freedom to apply all possible interaction pattterns.
104
+ // Especially, we should be compatible with 'withClickInteraction'
105
+ setTimeout(() => {
106
+ if (isElementDirectFocusableItemUnderMoreButtonMenu(this._getDeepActiveElement())) return;
134
107
  moreButton?.setAttribute('aria-expanded', 'false');
135
- }
108
+
109
+ moreButtonMenu.removeAttribute('data-open');
110
+ }, 100);
136
111
  });
137
112
 
138
113
  this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
@@ -140,22 +115,26 @@ export const MoreButtonMenuMixin = superclass =>
140
115
  moreButton?.addEventListener('mousedown', () => {
141
116
  this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
142
117
  const focusableFirstLevelItems = moreButtonMenu.querySelectorAll(
143
- ':scope > [role="listitem"] > a, :scope > [role="listitem"] > button',
118
+ ':scope > [role="listitem"] > :is(a, button)',
144
119
  );
145
120
 
146
- [...focusableFirstLevelItems]?.forEach(focusableFirstLevelItem => {
121
+ // @ts-ignore Type 'NodeListOf<Element>' must have a '[Symbol.iterator]()' method that returns an iterator.
122
+ for (const focusableFirstLevelItem of focusableFirstLevelItems) {
147
123
  if (this._getDeepActiveElement() === focusableFirstLevelItem) {
148
124
  this.hasMoreButtonMenuAnyFocusedFirstLevelItems = true;
149
125
  }
150
- });
126
+ }
151
127
  });
152
128
 
153
129
  moreButton?.addEventListener('click', () => {
154
- if (!this.hasMoreButtonMenuAnyFocusedFirstLevelItems) {
155
- moreButtonMenu
156
- .querySelector(':scope > [role="listitem"] > a, :scope > [role="listitem"] > button')
157
- ?.focus();
158
- }
130
+ if (this.hasMoreButtonMenuAnyFocusedFirstLevelItems) return;
131
+ /** @type {HTMLElement | null} */
132
+ const firstInteractiveItem = moreButtonMenu.querySelector(
133
+ ':scope > [role="listitem"] > :is(a, button)',
134
+ );
135
+
136
+ firstInteractiveItem?.focus();
137
+ moreButtonMenu.setAttribute('data-open', '');
159
138
  });
160
139
  }
161
140
 
@@ -232,13 +211,13 @@ export const MoreButtonMenuMixin = superclass =>
232
211
  return fragment;
233
212
  };
234
213
 
235
- moveItemToMoreButtonMenuFromMainMenu() {
236
- const moreButtonMenuElement = this.getMoreButtonMenu();
237
- const listItems = this._listNode.querySelectorAll(':scope > [role="listitem"]');
238
- const listItem = listItems[listItems.length - 1];
239
-
214
+ /**
215
+ * @param {{ moreButtonMenuElement: HTMLButtonElement, listItem: HTMLElement }} opts
216
+ */
217
+ moveItemToMoreButtonMenuFromMainMenu({ moreButtonMenuElement, listItem }) {
240
218
  const fragment = this.getListItemWithAdjacentNodes(listItem);
241
219
  moreButtonMenuElement.prepend(fragment);
220
+ // eslint-disable-next-line no-param-reassign
242
221
  listItem.style.display = '';
243
222
  this.displayMoreButton();
244
223
  }
@@ -268,9 +247,15 @@ export const MoreButtonMenuMixin = superclass =>
268
247
  return hiddenItemsCount;
269
248
  }
270
249
 
250
+ /**
251
+ * @param {number} hiddenItemsCount
252
+ */
271
253
  moveHiddenItemsFromMainMenuToMoreButtonMenu(hiddenItemsCount) {
254
+ const moreButtonMenuElement = this.getMoreButtonMenu();
255
+ const listItems = this._listNode.querySelectorAll(':scope > [role="listitem"]');
272
256
  for (let i = 0; i < hiddenItemsCount; i += 1) {
273
- this.moveItemToMoreButtonMenuFromMainMenu();
257
+ const listItem = listItems[listItems.length - 1 - i];
258
+ this.moveItemToMoreButtonMenuFromMainMenu({ moreButtonMenuElement, listItem });
274
259
  }
275
260
  }
276
261
 
@@ -279,6 +264,8 @@ export const MoreButtonMenuMixin = superclass =>
279
264
  return;
280
265
  }
281
266
 
267
+ this.style.visibility = 'hidden';
268
+
282
269
  this.__resizeTimeout = setTimeout(() => {
283
270
  this.__resizeTimeout = null;
284
271
 
@@ -214,7 +214,7 @@ const MultiLevelListMixinImplementation = superclass =>
214
214
  if (
215
215
  this.orientation === 'horizontal' &&
216
216
  subListOfActiveItem &&
217
- this._activeMode !== 'disclosure'
217
+ this._activeMode !== 'tabbable-disclosure'
218
218
  ) {
219
219
  this.activeItem.click();
220
220
  }
@@ -3,6 +3,10 @@
3
3
  * 'smart' (LionOption) and 'regular' items
4
4
  */
5
5
 
6
+ /**
7
+ * @typedef {import('@lion/ui/checkbox-group.js').LionCheckboxIndeterminate} LionCheckboxIndeterminate
8
+ */
9
+
6
10
  /**
7
11
  * @param {HTMLElement} item
8
12
  */
@@ -34,18 +38,33 @@ export function toggleDisabled(item) {
34
38
  * @param {HTMLElement} item
35
39
  */
36
40
  export function isChecked(item) {
37
- return item.hasAttribute('checked') || item.getAttribute('aria-selected') === 'true';
41
+ return item.hasAttribute('checked');
38
42
  }
39
43
 
40
44
  /**
41
45
  * @param {HTMLElement} item
42
- * @param {boolean} [shouldSet=false]
46
+ * @param {Object} [options]
47
+ * @param {boolean} [options.unset=false]
48
+ * @param {'page'|'mixed'} [options.text] - optional text to set for aria-checked/aria-selected/aria-current (e.g. "mixed")
43
49
  */
44
- export function setChecked(item, shouldSet = true) {
45
- const ariaAttr = item.getAttribute('role') === 'option' ? 'selected' : 'checked';
46
- if (shouldSet) {
50
+ export function setChecked(item, unset = false) {
51
+ const role = item.getAttribute('role');
52
+ let ariaAttr = 'current';
53
+ if (role?.endsWith('checkbox') || role?.endsWith('radio')) {
54
+ ariaAttr = 'checked';
55
+ } else if (role === 'option' || role === 'treeitem') {
56
+ ariaAttr = 'selected';
57
+ }
58
+ let text;
59
+ if (/** @type {HTMLAnchorElement} */ (item).href) {
60
+ text = 'page';
61
+ }
62
+ if (/** @type {LionCheckboxIndeterminate} */ (item).mixedState) {
63
+ text = 'mixed';
64
+ }
65
+ if (!unset) {
47
66
  item.setAttribute('checked', '');
48
- item.setAttribute(`aria-${ariaAttr}`, 'true');
67
+ item.setAttribute(`aria-${ariaAttr}`, text || 'true');
49
68
  } else {
50
69
  item.removeAttribute('checked');
51
70
  item.setAttribute(`aria-${ariaAttr}`, 'false');
@@ -1,3 +1,4 @@
1
1
  import { runInteractiveListMixinSuite } from '../test-suites/InteractiveListMixin.suite.js';
2
+ import '@lion/ui/define/lion-menu.js';
2
3
 
3
- runInteractiveListMixinSuite();
4
+ runInteractiveListMixinSuite({ tagString: 'lion-menu' });