@lion/ui 0.21.1-menu-system.0 → 0.22.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 (185) hide show
  1. package/components/button/src/LionButton.js +0 -4
  2. package/components/form/test/lion-form.test.js +4 -3
  3. package/components/form-core/src/form-group/FormGroupMixin.js +37 -0
  4. package/components/form-core/test-suites/form-group/FormGroupMixin.suite.js +25 -0
  5. package/components/input-amount-dropdown/src/LionInputAmountDropdown.js +2 -2
  6. package/components/input-amount-dropdown/test/lion-input-amount-dropdown-integrations.test.js +12 -3
  7. package/components/input-amount-dropdown/test-suites/LionInputAmountDropdown.suite.js +10 -3
  8. package/components/overlays/src/OverlayController.js +111 -296
  9. package/components/overlays/src/OverlayMixin.js +17 -76
  10. package/components/overlays/src/utils/adopt-styles.js +1 -1
  11. package/components/overlays/src/utils/is-equal-config.js +6 -15
  12. package/components/overlays/test/OverlayController.test.js +24 -144
  13. package/components/overlays/test-suites/OverlayMixin.suite.js +8 -8
  14. package/components/overlays/types/ArrowMixinTypes.ts +3 -1
  15. package/components/overlays/types/OverlayConfig.ts +5 -20
  16. package/components/overlays/types/OverlayMixinTypes.ts +0 -2
  17. package/custom-elements.json +6260 -13587
  18. package/dist-types/components/button/src/LionButton.d.ts +0 -1
  19. package/dist-types/components/button/src/LionButton.d.ts.map +1 -1
  20. package/dist-types/components/combobox/src/LionCombobox.d.ts +1 -1
  21. package/dist-types/components/dialog/src/LionDialog.d.ts +2 -6
  22. package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
  23. package/dist-types/components/form-core/src/form-group/FormGroupMixin.d.ts +1 -0
  24. package/dist-types/components/form-core/src/form-group/FormGroupMixin.d.ts.map +1 -1
  25. package/dist-types/components/form-core/test-suites/form-group/FormGroupMixin.suite.d.ts.map +1 -1
  26. package/dist-types/components/input-amount-dropdown/test-suites/LionInputAmountDropdown.suite.d.ts.map +1 -1
  27. package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts +1 -1
  28. package/dist-types/components/overlays/src/OverlayController.d.ts +5 -22
  29. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  30. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  31. package/dist-types/components/overlays/src/utils/is-equal-config.d.ts.map +1 -1
  32. package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts.map +1 -1
  33. package/dist-types/components/overlays/types/OverlayConfig.d.ts +2 -12
  34. package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
  35. package/dist-types/components/overlays/types/OverlayMixinTypes.d.ts +0 -1
  36. package/dist-types/components/overlays/types/OverlayMixinTypes.d.ts.map +1 -1
  37. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +2 -6
  38. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  39. package/dist-types/components/tooltip/src/LionTooltip.d.ts +1 -1
  40. package/dist-types/exports/overlays.d.ts +0 -6
  41. package/dist-types/exports/overlays.d.ts.map +1 -1
  42. package/dist-types/tsconfig.tsbuildinfo +1 -1
  43. package/docs/components/checkbox-group/overview.md +2 -2
  44. package/docs/components/collapsible/overview.md +1 -1
  45. package/docs/components/combobox/api-table.md +39 -53
  46. package/docs/components/combobox/overview.md +1 -1
  47. package/docs/components/combobox/use-cases.md +1 -1
  48. package/docs/components/dialog/api-table.md +6 -32
  49. package/docs/components/dialog/overview.md +1 -1
  50. package/docs/components/fieldset/overview.md +5 -5
  51. package/docs/components/form/overview.md +3 -3
  52. package/docs/components/form/use-cases.md +2 -2
  53. package/docs/components/icon/use-cases.md +1 -1
  54. package/docs/components/input/overview.md +1 -1
  55. package/docs/components/input-amount/overview.md +3 -3
  56. package/docs/components/input-date/overview.md +3 -3
  57. package/docs/components/input-datepicker/api-table.md +45 -63
  58. package/docs/components/input-datepicker/overview.md +5 -5
  59. package/docs/components/input-email/overview.md +2 -2
  60. package/docs/components/input-file/overview.md +1 -1
  61. package/docs/components/input-iban/overview.md +2 -2
  62. package/docs/components/input-range/overview.md +2 -2
  63. package/docs/components/input-stepper/overview.md +1 -1
  64. package/docs/components/input-tel/overview.md +1 -1
  65. package/docs/components/input-tel/use-cases.md +6 -6
  66. package/docs/components/input-tel-dropdown/overview.md +1 -1
  67. package/docs/components/listbox/use-cases.md +1 -1
  68. package/docs/components/radio-group/overview.md +3 -3
  69. package/docs/components/select/use-cases.md +1 -1
  70. package/docs/components/select-rich/api-table.md +33 -50
  71. package/docs/components/select-rich/use-cases.md +2 -2
  72. package/docs/components/tabs/examples.md +1 -1
  73. package/docs/components/tooltip/api-table.md +7 -33
  74. package/docs/fundamentals/index.md +4 -4
  75. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  76. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  77. package/docs/fundamentals/rationales/accessibility.md +1 -1
  78. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  79. package/docs/fundamentals/systems/core/overview.md +1 -1
  80. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  81. package/docs/fundamentals/systems/form/model-value.md +1 -1
  82. package/docs/fundamentals/systems/form/overview.md +28 -28
  83. package/docs/fundamentals/systems/form/validate.md +2 -2
  84. package/docs/fundamentals/systems/localize/overview.md +4 -4
  85. package/docs/fundamentals/systems/overlays/api-table.md +43 -46
  86. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  87. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  88. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  89. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  90. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  91. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  92. package/docs/guides/how-to/create-a-form.md +4 -4
  93. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  94. package/docs/guides/how-to/index.md +1 -1
  95. package/docs/guides/index.md +8 -8
  96. package/docs/guides/principles/index.md +5 -5
  97. package/docs/guides/principles/styling.md +1 -1
  98. package/exports/overlays.js +0 -6
  99. package/overlays.js +0 -6
  100. package/package.json +6 -2
  101. package/vscode.css-custom-data.json +5 -0
  102. package/vscode.html-custom-data.json +789 -0
  103. package/web-types.json +2016 -0
  104. package/components/menu/CHANGELOG.md +0 -37
  105. package/components/menu/README.md +0 -334
  106. package/components/menu/index.js +0 -2
  107. package/components/menu/lion-menu.js +0 -3
  108. package/components/menu/package.json +0 -46
  109. package/components/menu/requirements.temp.md +0 -12
  110. package/components/menu/src/InteractiveListMixin.js +0 -840
  111. package/components/menu/src/LionMenu.js +0 -421
  112. package/components/menu/src/MoreButtonMenuMixin.js +0 -278
  113. package/components/menu/src/MultiLevelListMixin.js +0 -305
  114. package/components/menu/src/utils/isInView.js +0 -26
  115. package/components/menu/src/utils/listItemInteractions.js +0 -124
  116. package/components/menu/test/InteractiveListMixin.test.js +0 -3
  117. package/components/menu/test/MoreButtonMenuMixin.test.js +0 -4
  118. package/components/menu/test/MultiLevelListMixin.test.js +0 -3
  119. package/components/menu/test/lion-menu-interactions.test.js +0 -3
  120. package/components/menu/test/lion-menu.test.js +0 -14
  121. package/components/menu/test-helpers/mimicKeyPress.js +0 -9
  122. package/components/menu/test-suites/InteractiveListMixin.suite.js +0 -454
  123. package/components/menu/test-suites/LionMenu.suite.js +0 -114
  124. package/components/menu/test-suites/LionMenuInteractions.suite.js +0 -583
  125. package/components/menu/test-suites/MoreButtonMenuMixin.suite.js +0 -372
  126. package/components/menu/test-suites/MultiLevelListMixin.suite.js +0 -303
  127. package/components/menu/types/InteractiveListMixinTypes.d.ts +0 -84
  128. package/components/overlays/NEXT.md +0 -49
  129. package/dist-types/components/menu/index.d.ts +0 -3
  130. package/dist-types/components/menu/index.d.ts.map +0 -1
  131. package/dist-types/components/menu/lion-menu.d.ts +0 -2
  132. package/dist-types/components/menu/lion-menu.d.ts.map +0 -1
  133. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +0 -39
  134. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +0 -1
  135. package/dist-types/components/menu/src/utils/isInView.d.ts +0 -9
  136. package/dist-types/components/menu/src/utils/isInView.d.ts.map +0 -1
  137. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts +0 -48
  138. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts.map +0 -1
  139. package/dist-types/components/menu/test/InteractiveListMixin.test.d.ts +0 -2
  140. package/dist-types/components/menu/test/InteractiveListMixin.test.d.ts.map +0 -1
  141. package/dist-types/components/menu/test/MoreButtonMenuMixin.test.d.ts +0 -2
  142. package/dist-types/components/menu/test/MoreButtonMenuMixin.test.d.ts.map +0 -1
  143. package/dist-types/components/menu/test/MultiLevelListMixin.test.d.ts +0 -2
  144. package/dist-types/components/menu/test/MultiLevelListMixin.test.d.ts.map +0 -1
  145. package/dist-types/components/menu/test/lion-menu-interactions.test.d.ts +0 -2
  146. package/dist-types/components/menu/test/lion-menu-interactions.test.d.ts.map +0 -1
  147. package/dist-types/components/menu/test/lion-menu.test.d.ts +0 -2
  148. package/dist-types/components/menu/test/lion-menu.test.d.ts.map +0 -1
  149. package/dist-types/components/menu/test-helpers/mimicKeyPress.d.ts +0 -7
  150. package/dist-types/components/menu/test-helpers/mimicKeyPress.d.ts.map +0 -1
  151. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts +0 -11
  152. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +0 -1
  153. package/dist-types/components/menu/test-suites/LionMenu.suite.d.ts +0 -9
  154. package/dist-types/components/menu/test-suites/LionMenu.suite.d.ts.map +0 -1
  155. package/dist-types/components/menu/test-suites/LionMenuInteractions.suite.d.ts +0 -5
  156. package/dist-types/components/menu/test-suites/LionMenuInteractions.suite.d.ts.map +0 -1
  157. package/dist-types/components/menu/test-suites/MoreButtonMenuMixin.suite.d.ts +0 -11
  158. package/dist-types/components/menu/test-suites/MoreButtonMenuMixin.suite.d.ts.map +0 -1
  159. package/dist-types/components/menu/test-suites/MultiLevelListMixin.suite.d.ts +0 -11
  160. package/dist-types/components/menu/test-suites/MultiLevelListMixin.suite.d.ts.map +0 -1
  161. package/dist-types/exports/define/lion-item.d.ts +0 -2
  162. package/dist-types/exports/define/lion-item.d.ts.map +0 -1
  163. package/dist-types/exports/define/lion-menu.d.ts +0 -2
  164. package/dist-types/exports/define/lion-menu.d.ts.map +0 -1
  165. package/dist-types/exports/menu-test-suites.d.ts +0 -4
  166. package/dist-types/exports/menu-test-suites.d.ts.map +0 -1
  167. package/dist-types/exports/menu.d.ts +0 -3
  168. package/dist-types/exports/menu.d.ts.map +0 -1
  169. package/docs/components/menu/api-table.md +0 -121
  170. package/docs/components/menu/applyMenuDemoStyles.js +0 -173
  171. package/docs/components/menu/extensions.md +0 -262
  172. package/docs/components/menu/index.md +0 -13
  173. package/docs/components/menu/overview.md +0 -422
  174. package/docs/components/menu/src/DemoNavigationBar.js +0 -441
  175. package/docs/components/menu/src/DemoSpinButton.js +0 -215
  176. package/docs/components/menu/src/DemoToolbar.js +0 -101
  177. package/docs/components/menu/src/DemoTree.js +0 -55
  178. package/docs/components/menu/src/navigation-bar-types.js +0 -41
  179. package/docs/components/menu/use-cases.md +0 -74
  180. package/exports/define/lion-item.js +0 -3
  181. package/exports/define/lion-menu.js +0 -3
  182. package/exports/menu-test-suites.js +0 -4
  183. package/exports/menu.js +0 -3
  184. package/menu-test-suites.js +0 -4
  185. package/menu.js +0 -3
@@ -1,840 +0,0 @@
1
- /* eslint-disable max-classes-per-file */
2
- /* eslint-disable import/no-extraneous-dependencies */
3
- import { html, css, LitElement } from 'lit';
4
- import { SlotMixin, DisabledMixin, uuid } from '@lion/ui/core.js';
5
- import { dedupeMixin } from '@open-wc/dedupe-mixin';
6
- import { MoreButtonMenuMixin } from './MoreButtonMenuMixin.js';
7
- import { isInView } from './utils/isInView.js';
8
- import {
9
- isDisabled,
10
- isChecked,
11
- setChecked,
12
- toggleChecked,
13
- isActive,
14
- setActive,
15
- getValue,
16
- } from './utils/listItemInteractions.js';
17
-
18
- /**
19
- * @typedef {import('../types/InteractiveListMixinTypes.js').InteractiveListItemRole} InteractiveListItemRole
20
- */
21
-
22
- // TODO: consider renaming to FocusGroupMixin
23
-
24
- // TODO: make all available in controller/directive (same logic with an elegant prop-to-host-mapping)
25
-
26
- /**
27
- * @param {Element} potentialFocusable
28
- * @returns {Element|null}
29
- */
30
- function isFocusableElement(potentialFocusable) {
31
- // @ts-ignore - returns Element or falsy value
32
- return (
33
- potentialFocusable &&
34
- (potentialFocusable.hasAttribute('tabindex') ||
35
- ['BUTTON', 'A'].includes(potentialFocusable.tagName))
36
- );
37
- }
38
-
39
- /**
40
- * Lightweight component supporting InteractiveListItemRole as a type.
41
- *
42
- * Supports:
43
- * - 1. menu item
44
- * <lion-item> Item label </lion-item>
45
- * - 2. sub menu
46
- * <lion-item>
47
- * <button slot="invoker"> Item label </button>
48
- * <lion-menu slot="subitems"> ... </lion-menu>
49
- * </lion-item>
50
- *
51
- * Sets the role property on the right element. For case 1, this would be the item itself,
52
- * for case 2 it would be the [slot=invoker].
53
- */
54
- export class LionItem extends LitElement {
55
- /** @type {any} */
56
- static get properties() {
57
- return {
58
- type: { type: String },
59
- };
60
- }
61
-
62
- get _invokerNode() {
63
- return /** @type {HTMLElement[]} */ (Array.from(this.children)).find(
64
- child => child.slot === 'invoker',
65
- );
66
- }
67
-
68
- static styles = [
69
- css`
70
- :host {
71
- display: block;
72
- }
73
- `,
74
- ];
75
-
76
- constructor() {
77
- super();
78
- /** @type {InteractiveListItemRole |''} */
79
- this.type = '';
80
- }
81
-
82
- connectedCallback() {
83
- super.connectedCallback();
84
-
85
- if (!this._computedType) {
86
- this._computedType = this.type || 'listitem';
87
- }
88
- if (this._invokerNode) {
89
- this._invokerNode.setAttribute('role', this._computedType);
90
- } else {
91
- this.setAttribute('role', this._computedType);
92
- }
93
- }
94
-
95
- render() {
96
- return html`
97
- <slot name="invoker"></slot>
98
- <slot></slot>
99
- `;
100
- }
101
- }
102
-
103
- export class LionMenuitem extends LionItem {
104
- connectedCallback() {
105
- this._computedType = `menuitem${this.type}`;
106
- super.connectedCallback();
107
- }
108
- }
109
- customElements.define('lion-menuitem', LionMenuitem);
110
-
111
- /**
112
- * Handles keyboard interaction plus bookkeeping for active and selected states.
113
- * Can be used as a fundament for:
114
- * - [role="menu|menubar"] having children with [role="menuitem|menuitemcheckbox|menuitemradio"]
115
- * - [role="listbox"] having children with [role="option"]
116
- * - [role="toolbar"] having children with [role="radiogroup|radio|group|checkbox|spinbutton"]
117
- * - [role="tree"] having children with [role="group|treeitem"]
118
- *
119
- * An interactive list accepts two types of items:
120
- * - 'smart' items: responsible for registering themselves and reflecting accessibility properties,
121
- * for example LionOption
122
- * - 'regular' items with a [role="menuitem|menuitemcheckbox|menuitemradio|option|radio|checkbox|treeitem|listitem]
123
- *
124
- * Features:
125
- * - keyboard navigation according to [aria specs](https://www.w3.org/TR/wai-aria-practices-1.1) for role
126
- * - types
127
- *
128
- * @param {import('@open-wc/dedupe-mixin').Constructor<import('lit').LitElement>} superclass
129
- */
130
- const InteractiveListMixinImplementation = superclass =>
131
- // @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
132
- class InteractiveListMixin extends DisabledMixin(SlotMixin(MoreButtonMenuMixin(superclass))) {
133
- /** @type {any} */
134
- static styles = [
135
- css`
136
- :host {
137
- display: block;
138
- }
139
-
140
- :host([hidden]) {
141
- display: none;
142
- }
143
-
144
- :host([disabled]) {
145
- color: #adadad;
146
- }
147
-
148
- :host([orientation='horizontal']) ::slotted([slot='list']) {
149
- display: flex;
150
- }
151
- `,
152
- ];
153
-
154
- render() {
155
- return html`
156
- <slot name="list"></slot>
157
- <slot></slot>
158
- `;
159
- }
160
-
161
- static get properties() {
162
- return {
163
- // TODO: align with name of open-ui => "inline|block" in focusgroup
164
- orientation: { type: String, reflect: true },
165
- // TODO: align with name of platform => ("multiple" in select, selection not part of focusgroup)
166
- multipleChoice: { type: Boolean, attribute: 'multiple-choice' },
167
- // TODO: align with name of open-ui
168
- selectionFollowsFocus: { type: Boolean, attribute: 'selection-follows-focus' },
169
- // TODO: align with name of open-ui => "wrap" in focusgroup
170
- rotateKeyboardNavigation: { type: Boolean, attribute: 'rotate-keyboard-navigation' },
171
- // TODO: align with name of open-ui
172
- noPreselect: { type: Boolean, attribute: 'no-preselect' },
173
-
174
- // TODO: implement, for now we start with only more-menu. See instructions in code about more menu.
175
- // Values:
176
- // - 'more-menu'
177
- // - 'scroll-buttons' (think of those material design tab bars)
178
- // - 'scroll-bar' (puts overflow: scroll on it) (think of tab bars, tables etc.)
179
- itemWrap: { type: Boolean, attribute: 'item-wrap' },
180
-
181
- _activateOnTypedChars: Boolean,
182
- _hasSmartListItems: Boolean,
183
- _activeMode: String,
184
- };
185
- }
186
-
187
- get slots() {
188
- return {
189
- ...super.slots,
190
- list: () => document.createElement('div'),
191
- };
192
- }
193
-
194
- /**
195
- * This is the (usually focusable, unless combobox) element that will contain [aria-activedescendant]
196
- * Depending on the children, role will be "listbox|menu|menubar|tree|toolbar"
197
- * @type {HTMLElement}
198
- */
199
- get _listNode() {
200
- return /** @type {HTMLElement} */ (Array.from(this.children).find(c => c.slot === 'list'));
201
- }
202
-
203
- get _scrollTargetNode() {
204
- return this;
205
- }
206
-
207
- /**
208
- * Place in the template that contains children with ._childrenRoles
209
- * @type {HTMLSlotElement}
210
- */
211
- get _listItemsSlot() {
212
- const slots = Array.from(
213
- /** @type {ShadowRoot} */ (this.shadowRoot).querySelectorAll('slot'),
214
- );
215
- return /** @type {HTMLSlotElement} */ (slots.find(s => !s.name));
216
- }
217
-
218
- /**
219
- * When used with FormRegistrarMixin, children will register themselves. Otherwise,
220
- * __listItems are gathered via a slotchange listener
221
- * @type {HTMLElement[]}
222
- */
223
- get listItems() {
224
- // @ts-ignore
225
- return this.__listItems || this.formElements;
226
- }
227
-
228
- /**
229
- * The roles to look for when children are registered to listItems
230
- * @overridable
231
- * @type {InteractiveListItemRole[]}
232
- */
233
- // eslint-disable-next-line class-methods-use-this
234
- get _childrenRoles() {
235
- // @ts-ignore - string type in array of InteractiveListItemRole
236
- return [...this._interactiveChildrenRoles, 'listitem', 'group'];
237
- }
238
-
239
- // eslint-disable-next-line class-methods-use-this
240
- get _interactiveChildrenRoles() {
241
- return ['menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'treeitem'];
242
- }
243
-
244
- get activeIndex() {
245
- return this.listItems.findIndex(el => isActive(el));
246
- }
247
-
248
- set activeIndex(index) {
249
- const activeItem = this.listItems[index];
250
-
251
- if (!activeItem) return;
252
-
253
- const prevActiveEl = this.activeItem;
254
- // This can be a focusable element or a wrapper thereof
255
- const el = this.listItems[index];
256
-
257
- // Unset siblings
258
- this.listItems.forEach(item => {
259
- setActive(item, true);
260
- });
261
- setActive(el);
262
-
263
- // Determine focusableEl and prevFocusableEl
264
-
265
- let focusableEl;
266
- let prevFocusableEl;
267
-
268
- if (this._activeMode === 'activedescendant') {
269
- focusableEl = el;
270
- prevFocusableEl = prevActiveEl;
271
- }
272
- // activeMode is 'roving-tabindex' or 'tabbable-disclosure'
273
- else {
274
- // are we a focusable element or a wrapper thereof?
275
- focusableEl = isFocusableElement(el) ? el : el.firstElementChild;
276
- prevFocusableEl =
277
- prevActiveEl &&
278
- (isFocusableElement(prevActiveEl) ? prevActiveEl : prevActiveEl.firstElementChild);
279
- }
280
-
281
- if (!focusableEl) return;
282
-
283
- // Update 'active mode'
284
- if (this._activeMode === 'activedescendant') {
285
- this._listNode.setAttribute('aria-activedescendant', focusableEl.id);
286
- } else if (
287
- this._activeMode === 'roving-tabindex' ||
288
- this._activeMode === 'tabbable-disclosure'
289
- ) {
290
- // @ts-ignore - Element extends HTMLElement for focus
291
- focusableEl.focus();
292
- }
293
-
294
- // @ts-ignore - focusableEl is safely used as HTMLElement here
295
- if (this.orientation === 'vertical' && !isInView(this._scrollTargetNode, focusableEl)) {
296
- focusableEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
297
- }
298
-
299
- if (this._activeMode === 'roving-tabindex') {
300
- if (prevFocusableEl) {
301
- prevFocusableEl.setAttribute('tabindex', '-1');
302
- }
303
- focusableEl.setAttribute('tabindex', '0');
304
- }
305
- }
306
-
307
- get activeItem() {
308
- return this.listItems[this.activeIndex];
309
- }
310
-
311
- /**
312
- * @type {number | number[]}
313
- */
314
- get checkedIndex() {
315
- if (!this.multipleChoice) {
316
- return this.listItems.findIndex(o => isChecked(o));
317
- }
318
- return this.listItems.filter(o => isChecked(o)).map(o => this.listItems.indexOf(o));
319
- }
320
-
321
- /**
322
- * @deprecated
323
- * This setter exists for backwards compatibility of single choice groups.
324
- * A setter api would be confusing for a multipleChoice group. Use `setCheckedIndex` instead.
325
- * @param {number} index
326
- */
327
- set checkedIndex(index) {
328
- this.setCheckedIndex(index);
329
- }
330
-
331
- /**
332
- * When `multipleChoice` is false, will toggle, else will check provided index
333
- * @param {Number} index
334
- */
335
- setCheckedIndex(index) {
336
- const item = this.listItems[index];
337
- if (!item) return;
338
- if (!this.multipleChoice) {
339
- // Uncheck all
340
- this.listItems.forEach(listItem => {
341
- setChecked(listItem, true);
342
- });
343
- setChecked(this.listItems[index]);
344
- } else {
345
- toggleChecked(this.listItems[index]);
346
- }
347
- }
348
-
349
- /**
350
- * Since we cannot check instanceof with mixins, use this getter instead.
351
- * This way, the outside world can confirm the type of an element
352
- */
353
- // eslint-disable-next-line class-methods-use-this
354
- get isInteractiveList() {
355
- return true;
356
- }
357
-
358
- constructor() {
359
- super();
360
-
361
- // TODO: copy descriptions from ListboxMixin and ChoiceGroupMixin to here
362
- this.orientation = 'vertical';
363
- this.multipleChoice = false;
364
- this.selectionFollowsFocus = false;
365
- this.rotateKeyboardNavigation = false;
366
-
367
- /**
368
- * By default, checkedIndex is set to 0. When noPreselect is true,
369
- * checkedIndex will not be set
370
- * @type {boolean}
371
- */
372
- this.noPreselect = false;
373
-
374
- /**
375
- * Whenever possible (for [role="listbox|menu"], usually with one level) we use an
376
- * [active descendant](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_focus_activedescendant).
377
- * For [role=toolbar], we use a [roving tabindex](https://www.w3.org/TR/wai-aria-practices-1.1/#kbd_roving_tabindex)
378
- * Other examples:
379
- * - [role="menubar"](https://www.w3.org/TR/wai-aria-practices-1.1/examples/menubar/menubar-1/menubar-1.html)
380
- * - [role="tree"](https://www.w3.org/TR/wai-aria-practices-1.1/examples/treeview/treeview-1/treeview-1b.html)
381
- * For disclosure menus, we need keyboard navigation, but treat every list item
382
- * (more precisely its anchor child) as a tab stop.
383
- * @type {'activedescendant'|'roving-tabindex'|'tabbable-disclosure'|'none'}
384
- */
385
- this._activeMode = 'activedescendant';
386
-
387
- /**
388
- * When children are 'smart' (like LionOption), checked and active states will be set
389
- * to them and they are responsible for setting [aria-selected].
390
- * When false, `get listItems()` needs to be configured to point to the list items.
391
- * @type {boolean}
392
- */
393
- this._hasSmartChildren = false;
394
-
395
- /**
396
- * The role that will be put on _listNode
397
- * @type {'menu'|'menubar'|'listbox'|'tree'|'toolbar'}
398
- */
399
- this._listRole = 'menu';
400
-
401
- /**
402
- * @type {HTMLElement[]}
403
- */
404
- this.__listItems = [];
405
-
406
- /**
407
- * By default, a typed character (or a sequence thereof) will be matched against
408
- * the values of list items. The first and closest match, becomes active
409
- * (focused or activedescendant depending on _matchMode)
410
- * @type {boolean}
411
- */
412
- this._activateOnTypedChars = false;
413
-
414
- /**
415
- * The pending char sequence that will set active list item
416
- * @type {number}
417
- */
418
- this._activateOnCharTimeout = 2000;
419
-
420
- /**
421
- * @type {string[]}
422
- */
423
- this.__typedChars = [];
424
-
425
- this._onListClick = this._onListClick.bind(this);
426
- this._onListKeyUp = this._onListKeyUp.bind(this);
427
- this._onListKeyDown = this._onListKeyDown.bind(this);
428
- this._onListFocusIn = this._onListFocusIn.bind(this);
429
- }
430
-
431
- connectedCallback() {
432
- super.connectedCallback();
433
- window.addEventListener('popstate', this._onPopState);
434
- }
435
-
436
- disconnectedCallback() {
437
- super.disconnectedCallback();
438
- window.removeEventListener('popstate', this._onPopState);
439
- }
440
-
441
- /**
442
- * @param {import('lit').PropertyValues } changedProperties
443
- */
444
- firstUpdated(changedProperties) {
445
- super.firstUpdated(changedProperties);
446
- this._setupList();
447
- }
448
-
449
- /**
450
- * @param {Node[]} nodes
451
- */
452
- #identifyNewItemsAndInitListItems(nodes) {
453
- /**
454
- *
455
- * @param {HTMLElement} node
456
- * @param {{ newItems: HTMLElement[], level?: number }} opts
457
- * @returns
458
- */
459
- const handleInteractiveListAdditionLevel = (node, { newItems, level = 1 }) => {
460
- // Move list item to element with aria-activedescendant ([role="listbox|menu|menubar"])
461
- if (level === 1) {
462
- this._listNode.appendChild(node);
463
- if (this._hasSmartChildren || !(node instanceof Element)) {
464
- return;
465
- }
466
- }
467
- /**
468
- * When [role=group], we can have a grouped set within a level, like multiple
469
- * menuitemradios or -checkboxes.
470
- * When no role at all, we might deal with a wrapped node (for instance triggering a
471
- * submenu)
472
- * Alternatively, when _activeMode is ['tabbable-disclosure'](https://www.w3.org/TR/wai-aria-practices-1.1/examples/disclosure/disclosure-navigation.html),
473
- * we search for a[href]
474
- */
475
- if (node.getAttribute('role') === 'group') {
476
- // @ts-ignore - HTMLCollection is iterable
477
- for (const child of node.children) {
478
- // @ts-ignore
479
- handleInteractiveListAdditionLevel(child, { newItems, level: level + 1 });
480
- }
481
- return;
482
- }
483
- // Also support scenarios like these: https://adrianroselli.com/2019/06/link-disclosure-widget-navigation.html
484
- // TODO: For now, we only allow direct children. Should we consider deeper nesting?
485
- const focusableChildren = Array.from(node.children).filter(c => isFocusableElement(c));
486
- if (focusableChildren.length) {
487
- for (const focusableChild of focusableChildren) {
488
- // @ts-ignore - focusableChild is HTMLElement
489
- newItems.push(focusableChild);
490
- }
491
- } else {
492
- // Then we must be focusable (either via activedescendant, tabindex)
493
- newItems.push(node);
494
- }
495
- };
496
-
497
- /** @type {HTMLElement[]} */
498
- const newItems = [];
499
-
500
- // Move items to _listNode and add interactive node references (with [role]) to listItems
501
- nodes.forEach(node => {
502
- // @ts-expect-error - node type compatibility
503
- handleInteractiveListAdditionLevel(node, { newItems });
504
- });
505
-
506
- // @ts-expect-error
507
- if (this.itemWrap && nodes.length) {
508
- // @ts-expect-error
509
- this._initMoreButtonMenu();
510
- }
511
-
512
- newItems.forEach(item => {
513
- if (!this.__listItems.includes(item)) {
514
- this.__listItems.push(item);
515
- }
516
- });
517
-
518
- this._initListItems(newItems);
519
- }
520
-
521
- _setupList() {
522
- // /**
523
- // * @param {HTMLElement} node
524
- // */
525
- // const shouldAddItem = node => {
526
- // const role = /** @type {InteractiveListItemRole|'group'} */ (node.getAttribute('role'));
527
- // return (
528
- // (role && this._interactiveChildrenRoles.includes(role)) || node.hasAttribute('data-item')
529
- // );
530
- // };
531
-
532
- this._listItemsSlot.addEventListener('slotchange', () => {
533
- // N.B. we do not use `this._listItemsSlot.assignedNodes()`, as it omits comment nodes.
534
- // Comment nodes are really important for lit-html)
535
- const nodes = Array.from(
536
- (this._listItemsSlot.assignedSlot || this).childNodes || [],
537
- // @ts-expect-error
538
- ).filter(n => !n.slot);
539
- this.#identifyNewItemsAndInitListItems(nodes);
540
- });
541
-
542
- this._listNode.setAttribute('role', this._listRole);
543
- this._listNode.setAttribute(
544
- 'tabindex',
545
- this._activeMode === 'tabbable-disclosure' ? '-1' : '0',
546
- );
547
- this._listNode.addEventListener('click', this._onListClick);
548
- this._listNode.addEventListener('keyup', this._onListKeyUp);
549
- this._listNode.addEventListener('keydown', this._onListKeyDown);
550
- this._listNode.addEventListener('focusin', this._onListFocusIn);
551
-
552
- // TODO: add "more menu" functionality here... when itemWrap is set. We only support "more-menu" (for now):
553
- // 1. we can measure the width (if orientation is horizontal) of this._listNode
554
- // and see how its children fit, moving them to an overlayController
555
- // having hideVisually as hide mechanism (opening on focus, closing on blur).
556
- // N.B. this._listNode has `display:flex`. This means we need to add `text-wrap: nowrap;`
557
- // to avoid elems going over two lines, or give them a max-height based on measuring the height of one item.
558
- // 2. N.B. hideVisually needs to be implemented in OverlayController.
559
- // 3. when the more menu opens a next level, MultiLevelListMixin will be applied to the host as well.
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:
561
- // https://jsfiddle.net/a4ocktfp/19/
562
- }
563
-
564
- /**
565
- * When _listItemsSlot receives new items, they are initialized via this method
566
- * @param {LionItem[]|HTMLElement[]} newItems the newly added items needing initialization
567
- */
568
- _initListItems(newItems) {
569
- if (this._activeMode === 'roving-tabindex') {
570
- // Make item focusable, but not part of tab sequence
571
- newItems.forEach(item => item.setAttribute?.('tabindex', '-1'));
572
- } else if (this._activeMode === 'activedescendant') {
573
- newItems.forEach(item => {
574
- // eslint-disable-next-line no-param-reassign
575
- item.id = item.id || `${this.localName}-item-${uuid()}`;
576
- });
577
- } // for 'tabbable-disclosure', we assume items are already focusable and part of tab sequence
578
-
579
- if (!this.noPreselect && this.checkedIndex === -1) {
580
- this.checkedIndex = 0;
581
- }
582
- newItems.forEach(item => {
583
- if (!item.hasAttribute('checked')) {
584
- setChecked(item, true);
585
- } else {
586
- setChecked(item);
587
- }
588
- });
589
-
590
- this._syncCurrentPageWithLocationHref(window.location);
591
- }
592
-
593
- /**
594
- * Handles various keyboard controls
595
- * @param {KeyboardEvent} ev - the keydown event object
596
- */
597
- _onListKeyDown(ev) {
598
- if (isDisabled(this)) {
599
- return;
600
- }
601
-
602
- const charHandled = this.__onKeydownActivateTypedChars(ev);
603
- if (charHandled) {
604
- return;
605
- }
606
-
607
- const { key } = ev;
608
-
609
- switch (key) {
610
- case 'Enter':
611
- case ' ':
612
- ev.preventDefault();
613
- // ev.stopPropagation(); // prevent nested menus from conflicting
614
- this.setCheckedIndex(this.activeIndex);
615
- break;
616
- case 'ArrowUp':
617
- ev.preventDefault();
618
- if (this.orientation === 'vertical' || this._activeMode === 'tabbable-disclosure') {
619
- this.activeIndex = this._getPreviousEnabledOption(this.activeIndex);
620
- }
621
- break;
622
- case 'ArrowLeft':
623
- if (this.orientation === 'horizontal' || this._activeMode === 'tabbable-disclosure') {
624
- this.activeIndex = this._getPreviousEnabledOption(this.activeIndex);
625
- }
626
- break;
627
- case 'ArrowDown':
628
- ev.preventDefault();
629
- if (this.orientation === 'vertical' || this._activeMode === 'tabbable-disclosure') {
630
- this.activeIndex = this._getNextEnabledOption(this.activeIndex);
631
- }
632
- break;
633
- case 'ArrowRight':
634
- if (this.orientation === 'horizontal' || this._activeMode === 'tabbable-disclosure') {
635
- this.activeIndex = this._getNextEnabledOption(this.activeIndex);
636
- }
637
- break;
638
- case 'Home':
639
- ev.preventDefault();
640
- this.activeIndex = this._getNextEnabledOption(0, 0);
641
- break;
642
- case 'End':
643
- ev.preventDefault();
644
- this.activeIndex = this._getPreviousEnabledOption(this.listItems.length - 1, 0);
645
- break;
646
- /* no default */
647
- }
648
-
649
- const keys = ['ArrowUp', 'ArrowDown', 'Home', 'End'];
650
- if (keys.includes(key) && this.selectionFollowsFocus && !this.multipleChoice) {
651
- this.setCheckedIndex(this.activeIndex);
652
- }
653
- }
654
-
655
- /**
656
- * @param {KeyboardEvent} ev - the keydown event object
657
- * @returns {boolean} whether a character key or a number was typed
658
- */
659
- __onKeydownActivateTypedChars(ev) {
660
- if (!this._activateOnTypedChars) {
661
- return false;
662
- }
663
-
664
- const { key, code } = ev;
665
-
666
- if (code.startsWith('Key') || code.startsWith('Digit') || code.startsWith('Numpad')) {
667
- this.__typedChars.push(key);
668
- // TODO: schedule in updated loop for perf
669
- const chars = this.__typedChars.join('');
670
- const matchItem = this.listItems.find(item =>
671
- this._matchCharsAgainstItem({ chars: [chars], item }),
672
- );
673
- // @ts-ignore - matchItem might be undefined
674
- this.activeIndex = this.listItems.indexOf(matchItem);
675
-
676
- // @ts-ignore - __clearCharTimeout can be a function
677
- if (typeof this.__clearCharTimeout === 'function') {
678
- // @ts-ignore
679
- this.__clearCharTimeout();
680
- }
681
- this.__clearCharTimeout = setTimeout(() => {
682
- this.__typedChars = [];
683
- }, this._activateOnCharTimeout);
684
-
685
- return true;
686
- }
687
- return false;
688
- }
689
-
690
- /**
691
- * @overridable
692
- * @param {{chars: string[], item: HTMLElement}} config
693
- */
694
- // eslint-disable-next-line class-methods-use-this
695
- _matchCharsAgainstItem({ chars, item }) {
696
- const value = getValue(item).toLowerCase();
697
- return value.startsWith(chars);
698
- }
699
-
700
- /**
701
- * @param {KeyboardEvent} ev
702
- */
703
- __preventScrollingWithArrowKeys(ev) {
704
- if (this.disabled) {
705
- return;
706
- }
707
- const { key } = ev;
708
- switch (key) {
709
- case 'ArrowUp':
710
- case 'ArrowDown':
711
- case 'Home':
712
- case 'End':
713
- ev.preventDefault();
714
- /* no default */
715
- }
716
- }
717
-
718
- /**
719
- * @overridable
720
- * @param {FocusEvent} ev
721
- */
722
- _onListFocusIn(ev) {
723
- /** @type {HTMLElement} */
724
- // @ts-ignore - target is EventTarget
725
- const { target } = ev;
726
- // const item = /** @type {HTMLElement} */ (target.closest('[role]'));
727
- // @ts-ignore - target is HTMLElement
728
- const foundIndex = this.listItems.indexOf(target);
729
- if (foundIndex > -1 && foundIndex !== this.activeIndex) {
730
- this.activeIndex = foundIndex;
731
- }
732
- }
733
-
734
- /**
735
- * @overridable
736
- * @param {MouseEvent} ev
737
- */
738
- // eslint-disable-next-line class-methods-use-this, no-unused-vars
739
- _onListClick(ev) {
740
- /** @type {HTMLElement} */
741
- // @ts-ignore - target is EventTarget
742
- const { target } = ev;
743
- const item = /** @type {HTMLElement} */ (target.closest('[role]')); // TODO: we might need a more specific contract
744
- const foundIndex = this.listItems.indexOf(item);
745
- if (foundIndex > -1) {
746
- this.activeIndex = foundIndex;
747
- this.setCheckedIndex(foundIndex);
748
- }
749
- }
750
-
751
- /**
752
- * @overridable
753
- * @param {KeyboardEvent} ev
754
- */
755
- // eslint-disable-next-line class-methods-use-this, no-unused-vars
756
- _onListKeyUp(ev) {
757
- if (this.disabled) {
758
- return;
759
- }
760
- const { key } = ev;
761
- // eslint-disable-next-line default-case
762
- switch (key) {
763
- case 'ArrowUp':
764
- case 'ArrowDown':
765
- case 'Home':
766
- case 'End':
767
- case ' ':
768
- case 'Enter':
769
- ev.preventDefault();
770
- }
771
- }
772
-
773
- /**
774
- * @param {number} currentIndex
775
- * @param {number} offset
776
- */
777
- __getNextOption(currentIndex, offset) {
778
- /**
779
- * @param {number} i
780
- */
781
- const until = i => (offset === 1 ? i < this.listItems.length : i >= 0);
782
-
783
- for (let i = currentIndex + offset; until(i); i += offset) {
784
- if (this.listItems[i] && !isDisabled(this.listItems[i])) {
785
- return i;
786
- }
787
- }
788
-
789
- if (this.rotateKeyboardNavigation) {
790
- const startIndex = offset === -1 ? this.listItems.length - 1 : 0;
791
- for (let i = startIndex; until(i); i += 1) {
792
- if (this.listItems[i] && !isDisabled(this.listItems[i])) {
793
- return i;
794
- }
795
- }
796
- }
797
- return currentIndex;
798
- }
799
-
800
- /**
801
- * @param {number} currentIndex
802
- * @param {number} [offset=1]
803
- */
804
- _getNextEnabledOption(currentIndex, offset = 1) {
805
- return this.__getNextOption(currentIndex, offset);
806
- }
807
-
808
- /**
809
- * @param {number} currentIndex
810
- * @param {number} [offset=-1]
811
- */
812
- _getPreviousEnabledOption(currentIndex, offset = -1) {
813
- return this.__getNextOption(currentIndex, offset);
814
- }
815
-
816
- /**
817
- * @param {Location} location
818
- */
819
- _syncCurrentPageWithLocationHref(location) {
820
- this.listItems.forEach(item => {
821
- if (/** @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) {
826
- setChecked(item);
827
- } else {
828
- setChecked(item, true);
829
- }
830
- }
831
- });
832
- }
833
-
834
- /** @protected */
835
- _onPopState = () => {
836
- this._syncCurrentPageWithLocationHref(document.location);
837
- };
838
- };
839
- // @ts-ignore
840
- export const InteractiveListMixin = dedupeMixin(InteractiveListMixinImplementation);