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

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 (123) hide show
  1. package/components/menu/src/InteractiveListMixin.js +11 -9
  2. package/components/menu/src/LionMenu.js +1 -1
  3. package/components/menu/src/LionTree.js +1 -1
  4. package/components/menu/src/MoreButtonMenuMixin.js +18 -27
  5. package/components/menu/src/MultiLevelListMixin.js +46 -9
  6. package/components/menu/src/utils/listItemInteractions.js +2 -1
  7. package/components/menu/test/InteractiveListMixin.test.js +3 -0
  8. package/components/menu/test/MoreButtonMenuMixin.test.js +4 -0
  9. package/components/menu/test/MultiLevelListMixin.test.js +3 -0
  10. package/components/menu/test/lion-menu-hybrid.test.js +3 -0
  11. package/components/menu/test/lion-menu.test.js +7 -0
  12. package/components/menu/test/lion-toolbar.test.js +8 -0
  13. package/components/menu/test/lion-tree.test.js +7 -0
  14. package/components/menu/test-helpers/mimicKeyPress.js +9 -0
  15. package/components/menu/test-suites/InteractiveListMixin.suite.js +155 -5
  16. package/components/menu/test-suites/LionMenu.suite.js +113 -0
  17. package/components/menu/test-suites/LionMenuHybrid.suite.js +583 -0
  18. package/components/menu/test-suites/MoreButtonMenuMixin.suite.js +372 -0
  19. package/components/menu/test-suites/MultiLevelListMixin.suite.js +303 -0
  20. package/components/menu/types/InteractiveListMixinTypes.d.ts +2 -0
  21. package/components/overlays/src/OverlayController.js +1 -1
  22. package/components/overlays/types/OverlayMixinTypes.ts +2 -0
  23. package/custom-elements.json +245 -153
  24. package/dist-types/components/combobox/src/LionCombobox.d.ts +1 -1
  25. package/dist-types/components/dialog/src/LionDialog.d.ts +1 -1
  26. package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts +1 -1
  27. package/dist-types/components/menu/src/InteractiveListMixin.d.ts +1 -1
  28. package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -1
  29. package/dist-types/components/menu/src/LionMenu.d.ts +75 -5
  30. package/dist-types/components/menu/src/LionMenu.d.ts.map +1 -1
  31. package/dist-types/components/menu/src/LionMenuOverlay.d.ts +4 -2
  32. package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +1 -1
  33. package/dist-types/components/menu/src/LionTree.d.ts +2 -2
  34. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +0 -1
  35. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +1 -1
  36. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts +10 -2
  37. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -1
  38. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts.map +1 -1
  39. package/dist-types/components/menu/test/InteractiveListMixin.test.d.ts +2 -0
  40. package/dist-types/components/menu/test/InteractiveListMixin.test.d.ts.map +1 -0
  41. package/dist-types/components/menu/test/MoreButtonMenuMixin.test.d.ts +2 -0
  42. package/dist-types/components/menu/test/MoreButtonMenuMixin.test.d.ts.map +1 -0
  43. package/dist-types/components/menu/test/MultiLevelListMixin.test.d.ts +2 -0
  44. package/dist-types/components/menu/test/MultiLevelListMixin.test.d.ts.map +1 -0
  45. package/dist-types/components/menu/test/lion-menu-hybrid.test.d.ts +2 -0
  46. package/dist-types/components/menu/test/lion-menu-hybrid.test.d.ts.map +1 -0
  47. package/dist-types/components/menu/test/lion-toolbar.test.d.ts +2 -0
  48. package/dist-types/components/menu/test/lion-toolbar.test.d.ts.map +1 -0
  49. package/dist-types/components/menu/test/lion-tree.test.d.ts +2 -0
  50. package/dist-types/components/menu/test/lion-tree.test.d.ts.map +1 -0
  51. package/dist-types/components/menu/test-helpers/mimicKeyPress.d.ts +7 -0
  52. package/dist-types/components/menu/test-helpers/mimicKeyPress.d.ts.map +1 -0
  53. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +1 -1
  54. package/dist-types/components/menu/test-suites/LionMenu.suite.d.ts +9 -0
  55. package/dist-types/components/menu/test-suites/LionMenu.suite.d.ts.map +1 -0
  56. package/dist-types/components/menu/test-suites/LionMenuHybrid.suite.d.ts +5 -0
  57. package/dist-types/components/menu/test-suites/LionMenuHybrid.suite.d.ts.map +1 -0
  58. package/dist-types/components/menu/test-suites/MoreButtonMenuMixin.suite.d.ts +14 -0
  59. package/dist-types/components/menu/test-suites/MoreButtonMenuMixin.suite.d.ts.map +1 -0
  60. package/dist-types/components/menu/test-suites/MultiLevelListMixin.suite.d.ts +11 -0
  61. package/dist-types/components/menu/test-suites/MultiLevelListMixin.suite.d.ts.map +1 -0
  62. package/dist-types/components/overlays/types/OverlayMixinTypes.d.ts +1 -0
  63. package/dist-types/components/overlays/types/OverlayMixinTypes.d.ts.map +1 -1
  64. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +1 -1
  65. package/dist-types/components/tooltip/src/LionTooltip.d.ts +1 -1
  66. package/dist-types/exports/menu-test-suites.d.ts +2 -0
  67. package/dist-types/tsconfig.tsbuildinfo +1 -1
  68. package/docs/components/checkbox-group/overview.md +2 -2
  69. package/docs/components/collapsible/overview.md +1 -1
  70. package/docs/components/combobox/overview.md +1 -1
  71. package/docs/components/combobox/use-cases.md +1 -1
  72. package/docs/components/dialog/overview.md +1 -1
  73. package/docs/components/fieldset/overview.md +5 -5
  74. package/docs/components/form/overview.md +3 -3
  75. package/docs/components/form/use-cases.md +2 -2
  76. package/docs/components/icon/use-cases.md +1 -1
  77. package/docs/components/input/overview.md +1 -1
  78. package/docs/components/input-amount/overview.md +3 -3
  79. package/docs/components/input-date/overview.md +3 -3
  80. package/docs/components/input-datepicker/overview.md +5 -5
  81. package/docs/components/input-email/overview.md +2 -2
  82. package/docs/components/input-file/overview.md +1 -1
  83. package/docs/components/input-iban/overview.md +2 -2
  84. package/docs/components/input-range/overview.md +2 -2
  85. package/docs/components/input-stepper/overview.md +1 -1
  86. package/docs/components/input-tel/overview.md +1 -1
  87. package/docs/components/input-tel/use-cases.md +6 -6
  88. package/docs/components/input-tel-dropdown/overview.md +1 -1
  89. package/docs/components/listbox/use-cases.md +1 -1
  90. package/docs/components/menu/api-table.md +44 -39
  91. package/docs/components/radio-group/overview.md +3 -3
  92. package/docs/components/select/use-cases.md +1 -1
  93. package/docs/components/select-rich/use-cases.md +2 -2
  94. package/docs/components/tabs/examples.md +1 -1
  95. package/docs/fundamentals/index.md +4 -4
  96. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  97. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  98. package/docs/fundamentals/rationales/accessibility.md +1 -1
  99. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  100. package/docs/fundamentals/systems/core/overview.md +1 -1
  101. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  102. package/docs/fundamentals/systems/form/model-value.md +1 -1
  103. package/docs/fundamentals/systems/form/overview.md +28 -28
  104. package/docs/fundamentals/systems/form/validate.md +2 -2
  105. package/docs/fundamentals/systems/localize/overview.md +4 -4
  106. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  107. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  108. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  109. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  110. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  111. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  112. package/docs/guides/how-to/create-a-form.md +4 -4
  113. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  114. package/docs/guides/how-to/index.md +1 -1
  115. package/docs/guides/index.md +8 -8
  116. package/docs/guides/principles/index.md +5 -5
  117. package/docs/guides/principles/styling.md +1 -1
  118. package/exports/menu-test-suites.js +2 -0
  119. package/menu-test-suites.js +2 -0
  120. package/package.json +1 -1
  121. package/components/menu/src/utils/uuid.js +0 -3
  122. package/dist-types/components/menu/src/utils/uuid.d.ts +0 -2
  123. package/dist-types/components/menu/src/utils/uuid.d.ts.map +0 -1
@@ -1,10 +1,9 @@
1
1
  /* eslint-disable max-classes-per-file */
2
2
  /* eslint-disable import/no-extraneous-dependencies */
3
3
  import { html, css, LitElement } from 'lit';
4
- import { SlotMixin, DisabledMixin } from '@lion/ui/core.js';
4
+ import { SlotMixin, DisabledMixin, uuid } from '@lion/ui/core.js';
5
5
  import { dedupeMixin } from '@open-wc/dedupe-mixin';
6
6
  import { MoreButtonMenuMixin } from './MoreButtonMenuMixin.js';
7
- import { uuid } from './utils/uuid.js';
8
7
  import { isInView } from './utils/isInView.js';
9
8
  import {
10
9
  isDisabled,
@@ -167,7 +166,7 @@ const InteractiveListMixinImplementation = superclass =>
167
166
  multipleChoice: { type: Boolean, attribute: 'multiple-choice' },
168
167
  // TODO: align with name of open-ui
169
168
  selectionFollowsFocus: { type: Boolean, attribute: 'selection-follows-focus' },
170
- // TODO: align with name of open-ui => "wrap" in focsugroup
169
+ // TODO: align with name of open-ui => "wrap" in focusgroup
171
170
  rotateKeyboardNavigation: { type: Boolean, attribute: 'rotate-keyboard-navigation' },
172
171
  // TODO: align with name of open-ui
173
172
  noPreselect: { type: Boolean, attribute: 'no-preselect' },
@@ -293,7 +292,7 @@ const InteractiveListMixinImplementation = superclass =>
293
292
  }
294
293
 
295
294
  // @ts-ignore - focusableEl is safely used as HTMLElement here
296
- if (!isInView(this._scrollTargetNode, focusableEl)) {
295
+ if (this.orientation === 'vertical' && !isInView(this._scrollTargetNode, focusableEl)) {
297
296
  focusableEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
298
297
  }
299
298
 
@@ -556,7 +555,7 @@ const InteractiveListMixinImplementation = superclass =>
556
555
  // having hideVisually as hide mechanism (opening on focus, closing on blur).
557
556
  // N.B. this._listNode has `display:flex`. This means we need to add `text-wrap: nowrap;`
558
557
  // to avoid elems going over two lines, or give them a max-height based on measuring the height of one item.
559
- // 2. N.B. hideVisually needs to be implementd in OverlayController.
558
+ // 2. N.B. hideVisually needs to be implemented in OverlayController.
560
559
  // 3. when the more menu opens a next level, MultiLevelListMixin will be applied to the host as well.
561
560
  // That means it knows when a child menu is open. If _subListMap of one of the items is open, we should hide more menu like this:
562
561
  // https://jsfiddle.net/a4ocktfp/19/
@@ -588,7 +587,7 @@ const InteractiveListMixinImplementation = superclass =>
588
587
  }
589
588
  });
590
589
 
591
- this.__syncCurrentPageWithLocationHref(window.location);
590
+ this._syncCurrentPageWithLocationHref(window.location);
592
591
  }
593
592
 
594
593
  /**
@@ -817,10 +816,13 @@ const InteractiveListMixinImplementation = superclass =>
817
816
  /**
818
817
  * @param {Location} location
819
818
  */
820
- __syncCurrentPageWithLocationHref(location) {
819
+ _syncCurrentPageWithLocationHref(location) {
821
820
  this.listItems.forEach(item => {
822
821
  if (/** @type {HTMLAnchorElement} */ (item).href) {
823
- if (location.href.includes(/** @type {HTMLAnchorElement} */ (item).href)) {
822
+ const pathname = location.href.endsWith('/')
823
+ ? location.href.substring(0, location.href.length - 1)
824
+ : location.href;
825
+ if (pathname === /** @type {HTMLAnchorElement} */ (item).href) {
824
826
  setChecked(item);
825
827
  } else {
826
828
  setChecked(item, true);
@@ -831,7 +833,7 @@ const InteractiveListMixinImplementation = superclass =>
831
833
 
832
834
  /** @protected */
833
835
  _onPopState = () => {
834
- this.__syncCurrentPageWithLocationHref(document.location);
836
+ this._syncCurrentPageWithLocationHref(document.location);
835
837
  };
836
838
  };
837
839
  // @ts-ignore
@@ -187,7 +187,7 @@ export class LionMenu extends MultiLevelListMixin(LitElement) {
187
187
  }
188
188
 
189
189
  if (!multiple) {
190
- // Uncheck all
190
+ // Uncheck all within group
191
191
  listItemsWithinGroup.forEach(listItem => {
192
192
  setChecked(listItem, true);
193
193
  });
@@ -22,7 +22,7 @@ export class LionTree extends MultiLevelListMixin(LitElement) {
22
22
  super.updated(changedProperties);
23
23
 
24
24
  if (changedProperties.has('level')) {
25
- if (this.level > 0) {
25
+ if (this.level > 1) {
26
26
  this._listNode.setAttribute('role', 'group');
27
27
  }
28
28
  this.__setAriaLevelForListItems();
@@ -18,8 +18,8 @@ const styles = css`
18
18
  /** N.B. this is a simplified version of sr-only. when we move to overlayctrl, we should build this into overlayctrl */
19
19
  [data-more-button-menu] {
20
20
  overflow: hidden;
21
- height: 1px;
22
- width: 1px;
21
+ height: 0;
22
+ width: 0;
23
23
  }
24
24
 
25
25
  [role='list']:has(> [data-more-button-wrapper]) > [role='listitem'] > :is(button, a) {
@@ -75,11 +75,13 @@ export const MoreButtonMenuMixin = superclass =>
75
75
  moreButton?.setAttribute('aria-expanded', 'false');
76
76
  const moreButtonMenu = document.createElement('div');
77
77
  moreButtonMenu.setAttribute('data-more-button-menu', '');
78
+ moreButtonMenu.setAttribute('tabindex', '-1');
78
79
  moreButtonWrapper.appendChild(moreButtonMenu);
79
80
  this._listNode.appendChild(moreButtonWrapper);
80
81
 
81
82
  const isElementDirectFocusableItemUnderMoreButtonMenu = (/** @type {Element} */ element) =>
82
- this.listItems.includes(element) && moreButtonMenu.contains(element);
83
+ (this.listItems.includes(element) && moreButtonMenu.contains(element)) ||
84
+ element === moreButtonMenu;
83
85
 
84
86
  moreButtonMenu.addEventListener(
85
87
  'focusin',
@@ -87,6 +89,9 @@ export const MoreButtonMenuMixin = superclass =>
87
89
  const target = /** @type {HTMLElement} */ (event?.target);
88
90
  if (!isElementDirectFocusableItemUnderMoreButtonMenu(target)) return;
89
91
 
92
+ // If applicable hide any other L2 menus
93
+ this.querySelector('[level="2"]:has([data-open])')?._overlayCtrl?.hide();
94
+
90
95
  // A user did `Shift + Tab` from L2 menu to More menu
91
96
  if (target?.getAttribute('aria-expanded') === 'true') {
92
97
  // close L2 menu
@@ -110,31 +115,11 @@ export const MoreButtonMenuMixin = superclass =>
110
115
  }, 100);
111
116
  });
112
117
 
113
- this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
114
-
115
- moreButton?.addEventListener('mousedown', () => {
116
- this.hasMoreButtonMenuAnyFocusedFirstLevelItems = false;
117
- const focusableFirstLevelItems = moreButtonMenu.querySelectorAll(
118
- ':scope > [role="listitem"] > :is(a, button)',
119
- );
120
-
121
- // @ts-ignore Type 'NodeListOf<Element>' must have a '[Symbol.iterator]()' method that returns an iterator.
122
- for (const focusableFirstLevelItem of focusableFirstLevelItems) {
123
- if (this._getDeepActiveElement() === focusableFirstLevelItem) {
124
- this.hasMoreButtonMenuAnyFocusedFirstLevelItems = true;
125
- }
126
- }
127
- });
128
-
129
118
  moreButton?.addEventListener('click', () => {
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', '');
119
+ if (moreButtonMenu.getAttribute('data-open') === null) {
120
+ moreButtonMenu?.focus({ preventScroll: true });
121
+ moreButtonMenu.setAttribute('data-open', '');
122
+ }
138
123
  });
139
124
  }
140
125
 
@@ -186,6 +171,9 @@ export const MoreButtonMenuMixin = superclass =>
186
171
  const previousNodes = [];
187
172
  let { previousSibling } = listItem;
188
173
  while (previousSibling) {
174
+ if (previousSibling.nodeType === Node.ELEMENT_NODE) {
175
+ break;
176
+ }
189
177
  previousNodes.unshift(previousSibling);
190
178
  if (isMarkerComment(previousSibling)) {
191
179
  break;
@@ -196,6 +184,9 @@ export const MoreButtonMenuMixin = superclass =>
196
184
  const nextNodes = [];
197
185
  let { nextSibling } = listItem;
198
186
  while (nextSibling) {
187
+ if (nextSibling.nodeType === Node.ELEMENT_NODE) {
188
+ break;
189
+ }
199
190
  nextNodes.push(nextSibling);
200
191
 
201
192
  if (isMarkerComment(nextSibling)) {
@@ -3,10 +3,12 @@ import { html } from 'lit';
3
3
  import { dedupeMixin } from '@open-wc/dedupe-mixin';
4
4
  // import { DisclosureMixin } from '@lion/ui/collapsible.js';
5
5
  import { InteractiveListMixin } from './InteractiveListMixin.js';
6
-
6
+ import { isChecked, setChecked } from './utils/listItemInteractions.js';
7
7
  /**
8
8
  * @typedef {import('./InteractiveListMixin.js').LionItem} LionItem
9
9
  * @typedef {import('../types/InteractiveListMixinTypes.js').InteractiveListHost} InteractiveList
10
+ * @typedef {typeof import('../../overlays/types/OverlayMixinTypes.js').OverlayHost} OverlayHost
11
+ * @typedef {import('./LionMenuOverlay.js').LionMenuOverlay} LionMenuOverlay
10
12
  */
11
13
 
12
14
  /**
@@ -37,8 +39,13 @@ const MultiLevelListMixinImplementation = superclass =>
37
39
  * @configure DisclosureMixin
38
40
  */
39
41
  get _invokerNode() {
40
- // @ts-ignore - accessing static method and super property
41
- return this.constructor._getFocusableInvokerEl(this.invokerNode) || super._invokerNode;
42
+ const ctor = /** @type {OverlayHost} */ (this.constructor);
43
+ if (ctor._getFocusableInvokerEl && this.invokerNode) {
44
+ // @ts-ignore - accessing static method and super property
45
+ return ctor._getFocusableInvokerEl(this.invokerNode);
46
+ }
47
+ // @ts-ignore
48
+ return super._invokerNode;
42
49
  }
43
50
 
44
51
  /**
@@ -84,6 +91,7 @@ const MultiLevelListMixinImplementation = superclass =>
84
91
  this.noPreselect = true;
85
92
  /**
86
93
  * @configure InteractiveListMixin
94
+ * @type {'activedescendant'|'roving-tabindex'|'tabbable-disclosure'}
87
95
  */
88
96
  this._activeMode = 'roving-tabindex';
89
97
  /**
@@ -184,12 +192,12 @@ const MultiLevelListMixinImplementation = superclass =>
184
192
  * @param {*} ev
185
193
  */
186
194
  _onListKeyDown(ev) {
187
- const targetFromsubList = Array.from(this._subListMap).find(
195
+ const targetFromSubList = Array.from(this._subListMap).find(
188
196
  // @ts-ignore - contains method on InteractiveListHost
189
197
  ([, s]) => s === ev.target || s.contains(ev.target),
190
198
  );
191
199
 
192
- if (targetFromsubList) {
200
+ if (targetFromSubList) {
193
201
  return;
194
202
  }
195
203
 
@@ -228,22 +236,51 @@ const MultiLevelListMixinImplementation = superclass =>
228
236
  if (this.orientation === 'horizontal' && parentListOfActiveItem) {
229
237
  // @ts-ignore - close method
230
238
  this.close();
231
- // @ts-ignore - _onOverlayShow method
232
- parentListOfActiveItem._onOverlayShow();
239
+ if (
240
+ /** @type {InteractiveList & LionMenuOverlay} */ (parentListOfActiveItem)
241
+ ._onOverlayShow
242
+ ) {
243
+ // @ts-ignore - _onOverlayShow method
244
+ /** @type {OverlayHost} */ (parentListOfActiveItem)._onOverlayShow();
245
+ }
233
246
  }
234
247
  break;
235
248
  case 'ArrowLeft':
236
249
  if (this.orientation === 'vertical' && parentListOfActiveItem) {
237
250
  // @ts-ignore - close method
238
251
  this.close();
239
- // @ts-ignore - _onOverlayShow method
240
- parentListOfActiveItem._onOverlayShow();
252
+ if (
253
+ /** @type {InteractiveList & LionMenuOverlay} */ (parentListOfActiveItem)
254
+ ._onOverlayShow
255
+ ) {
256
+ // @ts-ignore - _onOverlayShow method
257
+ /** @type {OverlayHost} */ (parentListOfActiveItem)._onOverlayShow();
258
+ }
241
259
  }
242
260
  break;
243
261
  /* no default */
244
262
  }
245
263
  }
246
264
 
265
+ /**
266
+ * @param {Location} location
267
+ */
268
+ _syncCurrentPageWithLocationHref(location) {
269
+ super._syncCurrentPageWithLocationHref(location);
270
+
271
+ this._subListMap.forEach(subList => {
272
+ subList.listItems.forEach(
273
+ /** @param {LionItem|HTMLElement} item */ item => {
274
+ // @ts-ignore
275
+ if (isChecked(item) && subList._invokerNode) {
276
+ // @ts-ignore
277
+ setChecked(subList._invokerNode);
278
+ }
279
+ },
280
+ );
281
+ });
282
+ }
283
+
247
284
  // /**
248
285
  // * @enhance DisclosureMixin
249
286
  // */
@@ -75,7 +75,8 @@ export function setChecked(item, unset = false) {
75
75
  * @param {HTMLElement} item
76
76
  */
77
77
  export function toggleChecked(item) {
78
- setChecked(item, !isChecked(item));
78
+ const shouldUnset = isChecked(item);
79
+ setChecked(item, shouldUnset);
79
80
  }
80
81
 
81
82
  /**
@@ -0,0 +1,3 @@
1
+ import { runInteractiveListMixinSuite } from '../test-suites/InteractiveListMixin.suite.js';
2
+
3
+ runInteractiveListMixinSuite();
@@ -0,0 +1,4 @@
1
+ import { runMoreButtonMenuMixinSuite } from '../test-suites/MoreButtonMenuMixin.suite.js';
2
+ import '@lion/ui/define/lion-menu.js';
3
+
4
+ runMoreButtonMenuMixinSuite({ tagString: 'lion-menu' });
@@ -0,0 +1,3 @@
1
+ import { runMultiLevelListMixinSuite } from '../test-suites/MultiLevelListMixin.suite.js';
2
+
3
+ runMultiLevelListMixinSuite();
@@ -0,0 +1,3 @@
1
+ import { runLionMenuHybridSuite } from '../test-suites/LionMenuHybrid.suite.js';
2
+
3
+ runLionMenuHybridSuite();
@@ -1,4 +1,11 @@
1
1
  import { runInteractiveListMixinSuite } from '../test-suites/InteractiveListMixin.suite.js';
2
+ import { runMultiLevelListMixinSuite } from '../test-suites/MultiLevelListMixin.suite.js';
3
+ import { runMoreButtonMenuMixinSuite } from '../test-suites/MoreButtonMenuMixin.suite.js';
4
+ import { runLionMenuSuite } from '../test-suites/LionMenu.suite.js';
2
5
  import '@lion/ui/define/lion-menu.js';
6
+ import '@lion/ui/define/lion-menu-overlay.js';
3
7
 
4
8
  runInteractiveListMixinSuite({ tagString: 'lion-menu' });
9
+ runMultiLevelListMixinSuite({ tagString: 'lion-menu', tagChildString: 'lion-menu-overlay' });
10
+ runMoreButtonMenuMixinSuite({ tagString: 'lion-menu' });
11
+ runLionMenuSuite({ tagString: 'lion-menu' });
@@ -0,0 +1,8 @@
1
+ // import { runInteractiveListMixinSuite } from '../test-suites/InteractiveListMixin.suite.js';
2
+ // import { runMultiLevelListMixinSuite } from '../test-suites/MultiLevelListMixin.suite.js';
3
+ import { runMoreButtonMenuMixinSuite } from '../test-suites/MoreButtonMenuMixin.suite.js';
4
+ import '@lion/ui/define/lion-toolbar.js';
5
+
6
+ // runInteractiveListMixinSuite({ tagString: 'lion-toolbar' }); // 1 test fails
7
+ // runMultiLevelListMixinSuite({ tagString: 'lion-toolbar', tagChildString: 'lion-toolbar' }); // hangs
8
+ runMoreButtonMenuMixinSuite({ tagString: 'lion-toolbar' });
@@ -0,0 +1,7 @@
1
+ import { runInteractiveListMixinSuite } from '../test-suites/InteractiveListMixin.suite.js';
2
+ // import { runMultiLevelListMixinSuite } from '../test-suites/MultiLevelListMixin.suite.js';
3
+ import '@lion/ui/define/lion-tree.js';
4
+
5
+ runInteractiveListMixinSuite({ tagString: 'lion-tree' });
6
+ // TODO: fix failing tests. LionTree is missing a close function, should this be coming from the DisclosureMixin?
7
+ // runMultiLevelListMixinSuite({ tagString: 'lion-tree', tagChildString: 'lion-tree' });
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @param {HTMLElement} el
3
+ * @param {string} key
4
+ * @param {string} code
5
+ */
6
+ export function mimicKeyPress(el, key, code = '') {
7
+ el.dispatchEvent(new KeyboardEvent('keydown', { key, code }));
8
+ el.dispatchEvent(new KeyboardEvent('keyup', { key, code }));
9
+ }
@@ -1,6 +1,7 @@
1
1
  import { LitElement } from 'lit';
2
2
  import { expect, html, fixture as _fixture, unsafeStatic, defineCE } from '@open-wc/testing';
3
3
  import { InteractiveListMixin } from '../src/InteractiveListMixin.js';
4
+ import { mimicKeyPress } from '../test-helpers/mimicKeyPress.js';
4
5
 
5
6
  class InteractiveListClass extends InteractiveListMixin(LitElement) {}
6
7
 
@@ -127,6 +128,28 @@ export function runInteractiveListMixinSuite(customConfig) {
127
128
  });
128
129
  });
129
130
 
131
+ describe('preselect', () => {
132
+ it('sets the checkedIndex to 0 by default when noPreselect is false', async () => {
133
+ const el = await fixture(html`
134
+ <${tag}>
135
+ <div role="menuitem" id="item1">Item 1</div>
136
+ </${tag}>
137
+ `);
138
+ if (!el.noPreselect) {
139
+ expect(el.checkedIndex).to.equal(0);
140
+ }
141
+ });
142
+
143
+ it('sets the checkedIndex to -1 when noPreselect is true', async () => {
144
+ const el = await fixture(html`
145
+ <${tag} no-preselect>
146
+ <div role="menuitem" id="item1">Item 1</div>
147
+ </${tag}>
148
+ `);
149
+ expect(el.checkedIndex).to.equal(-1);
150
+ });
151
+ });
152
+
130
153
  describe('Programmatic interaction', () => {
131
154
  // If we have the item line directly in the fixture the linting rule lit-a11y/role-has-required-aria-attrs
132
155
  // will fail. Inside our code we set the required aria attributes, but the linter doesn't know that.
@@ -155,7 +178,6 @@ export function runInteractiveListMixinSuite(customConfig) {
155
178
  ${item(2, 'option')}
156
179
  </${tag}>
157
180
  `);
158
- console.debug('listItems', el.listItems);
159
181
  const item1 = el.listItems[1];
160
182
  expect(item1.getAttribute('aria-selected')).to.equal('false');
161
183
 
@@ -211,7 +233,7 @@ export function runInteractiveListMixinSuite(customConfig) {
211
233
 
212
234
  it('set checked state sets aria-checked attribute with role="menuitemcheckbox"', async () => {
213
235
  const el = await fixture(html`
214
- <${tag}>
236
+ <${tag} multiple-choice>
215
237
  ${item(1, 'menuitemcheckbox')}
216
238
  ${item(2, 'menuitemcheckbox')}
217
239
  </${tag}>
@@ -240,7 +262,7 @@ export function runInteractiveListMixinSuite(customConfig) {
240
262
 
241
263
  it('can set checked state to "mixed" for role="menuitemcheckbox"', async () => {
242
264
  const el = await fixture(html`
243
- <${tag}>
265
+ <${tag} multiple-choice>
244
266
  ${item(1, 'menuitemcheckbox')}
245
267
  ${item(2, 'menuitemcheckbox')}
246
268
  </${tag}>
@@ -256,7 +278,7 @@ export function runInteractiveListMixinSuite(customConfig) {
256
278
 
257
279
  it('does not allow to set checkedIndex or activeIndex to be out of bound', async () => {
258
280
  const el = await fixture(html`
259
- <${tag} has-no-default-selected autocomplete="list">
281
+ <${tag} no-preselect>
260
282
  <div role="menuitem" id="item1">Item 1</div>
261
283
  </${tag}>
262
284
  `);
@@ -284,15 +306,143 @@ export function runInteractiveListMixinSuite(customConfig) {
284
306
  });
285
307
  });
286
308
 
309
+ it('sets aria-current="page" when the location href matches the item href', async () => {
310
+ const el = await fixture(html`
311
+ <${tag}>
312
+ <div role="menuitem" id="item0">
313
+ <a href="${window.location.href}">Item 0</a>
314
+ </div>
315
+ <div role="menuitem" id="item1">
316
+ <a href="#bar">Item 1</a>
317
+ </div>
318
+ </${tag}>
319
+ `);
320
+ const item0 = el.listItems[0];
321
+ expect(item0.getAttribute('aria-current')).to.equal('page');
322
+ });
323
+
287
324
  // Copy tests from ListboxMixin. Later, make this mixin a fundament of the ListboxMixin
288
325
 
289
326
  // orientation
290
327
  // multipleChoice
291
328
  // selectionFollowsFocus
292
329
  // rotateKeyboardNavigation
293
- // noPreselect
294
330
  // _scrollTargetNode
295
331
 
332
+ describe('Interactions', () => {
333
+ describe('Keyboard navigation', () => {
334
+ it('navigates between items on [ArrowDown] [ArrowUp] keys', async () => {
335
+ const el = await fixture(html`
336
+ <${tag}>
337
+ <div role="listitem" id="item-0">
338
+ <button data-invoker>Item 0</button>
339
+ </div>
340
+ <div role="listitem" id="item-1">
341
+ <a href="#baz">Item 1</a>
342
+ </div>
343
+ <div role="listitem" id="item-2">
344
+ <a href="#foobar">Item 2</a>
345
+ </div>
346
+ </${tag}>
347
+ `);
348
+
349
+ const { _listNode } = el;
350
+
351
+ // Normalize
352
+ el.activeIndex = 0;
353
+
354
+ mimicKeyPress(_listNode, 'ArrowDown');
355
+ expect(el.activeIndex).to.be.equal(1);
356
+ mimicKeyPress(_listNode, 'ArrowDown');
357
+ expect(el.activeIndex).to.be.equal(2);
358
+ mimicKeyPress(_listNode, 'ArrowUp');
359
+ expect(el.activeIndex).to.be.equal(1);
360
+ });
361
+
362
+ it('navigates between items on [ArrowRight] [ArrowLeft] keys when orientation is "horizontal" and _activeMode="tabbable-disclosure"', async () => {
363
+ const el = await fixture(html`
364
+ <${tag} orientation="horizontal">
365
+ <div role="listitem" id="item-0">
366
+ <button data-invoker>Item 0</button>
367
+ </div>
368
+ <div role="listitem" id="item-1">
369
+ <a href="#baz">Item 1</a>
370
+ </div>
371
+ <div role="listitem" id="item-2">
372
+ <a href="#foobar">Item 2</a>
373
+ </div>
374
+ </${tag}>
375
+ `);
376
+ el._activeMode = 'tabbable-disclosure';
377
+ const { _listNode } = el;
378
+
379
+ // Normalize
380
+ el.activeIndex = 0;
381
+
382
+ mimicKeyPress(_listNode, 'ArrowRight');
383
+ expect(el.activeIndex).to.be.equal(1);
384
+ mimicKeyPress(_listNode, 'ArrowRight');
385
+ expect(el.activeIndex).to.be.equal(2);
386
+ mimicKeyPress(_listNode, 'ArrowLeft');
387
+ expect(el.activeIndex).to.be.equal(1);
388
+ });
389
+
390
+ it('stops navigation by default at end of option list', async () => {
391
+ const el = await fixture(html`
392
+ <${tag}>
393
+ <div role="listitem" id="item-0">
394
+ <button data-invoker>Item 0</button>
395
+ </div>
396
+ <div role="listitem" id="item-1">
397
+ <a href="#baz">Item 1</a>
398
+ </div>
399
+ <div role="listitem" id="item-2">
400
+ <a href="#foobar">Item 2</a>
401
+ </div>
402
+ </${tag}>
403
+ `);
404
+
405
+ const { _listNode } = el;
406
+
407
+ // Normalize
408
+ el.activeIndex = 0;
409
+
410
+ mimicKeyPress(_listNode, 'ArrowUp');
411
+ expect(el.activeIndex).to.be.equal(0);
412
+
413
+ el.activeIndex = 2;
414
+ mimicKeyPress(_listNode, 'ArrowDown');
415
+ expect(el.activeIndex).to.be.equal(2);
416
+ });
417
+
418
+ it('navigates to first and last option with [Home] and [End] keys', async () => {
419
+ const el = await fixture(html`
420
+ <${tag} orientation="horizontal">
421
+ <div role="listitem" id="item-0">
422
+ <button data-invoker>Item 0</button>
423
+ </div>
424
+ <div role="listitem" id="item-1">
425
+ <a href="#baz">Item 1</a>
426
+ </div>
427
+ <div role="listitem" id="item-2">
428
+ <a href="#foobar">Item 2</a>
429
+ </div>
430
+ </${tag}>
431
+ `);
432
+
433
+ const { _listNode } = el;
434
+
435
+ // Normalize
436
+ el.activeIndex = 2;
437
+
438
+ mimicKeyPress(_listNode, 'Home');
439
+ expect(el.activeIndex).to.equal(0);
440
+ mimicKeyPress(_listNode, 'End');
441
+ expect(el.activeIndex).to.equal(2);
442
+ });
443
+ });
444
+ });
445
+
296
446
  describe('Widget extensions', () => {
297
447
  it('can be extended to [role=listbox]', async () => {});
298
448
  it('can be extended to [role=menu]', async () => {});