@lion/ui 0.17.0 → 0.18.0-menu-system.1

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 (218) hide show
  1. package/collapsible.js +1 -0
  2. package/components/button/src/LionButton.js +4 -0
  3. package/components/button/test-suites/LionButton.suite.js +1 -0
  4. package/components/calendar/src/LionCalendar.js +2 -0
  5. package/components/collapsible/src/DisclosureMixin.js +313 -0
  6. package/components/collapsible/types/DisclosureMixinTypes.ts +48 -0
  7. package/components/combobox/test/lion-combobox.test.js +1 -0
  8. package/components/form-core/src/form-group/FormGroupMixin.js +7 -0
  9. package/components/form-core/src/registration/FormRegistrarMixin.js +11 -0
  10. package/components/form-core/test/FormControlMixin.test.js +1 -0
  11. package/components/form-core/test/validate/Validator.test.js +2 -0
  12. package/components/form-core/test-suites/ValidateMixin.suite.js +2 -0
  13. package/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.js +1 -0
  14. package/components/input-datepicker/src/localizeNamespaceLoader.js +1 -0
  15. package/components/input-datepicker/test/lion-input-datepicker.test.js +5 -6
  16. package/components/localize/test/date/formatDate.test.js +1 -0
  17. package/components/menu/CHANGELOG.md +37 -0
  18. package/components/menu/README.md +335 -0
  19. package/components/menu/index.js +2 -0
  20. package/components/menu/lion-menu-overlay.js +3 -0
  21. package/components/menu/lion-menu.js +3 -0
  22. package/components/menu/lion-spinbutton.js +3 -0
  23. package/components/menu/lion-toolbar.js +3 -0
  24. package/components/menu/lion-tree.js +3 -0
  25. package/components/menu/package.json +46 -0
  26. package/components/menu/requirements.temp.md +12 -0
  27. package/components/menu/src/InteractiveListMixin.js +805 -0
  28. package/components/menu/src/LionMenu.js +250 -0
  29. package/components/menu/src/LionMenuHybrid.js +46 -0
  30. package/components/menu/src/LionMenuOverlay.js +155 -0
  31. package/components/menu/src/LionSpinbutton.js +213 -0
  32. package/components/menu/src/LionToolbar.js +94 -0
  33. package/components/menu/src/LionTree.js +52 -0
  34. package/components/menu/src/MoreButtonMenuMixin.js +297 -0
  35. package/components/menu/src/MultiLevelListMixin.js +274 -0
  36. package/components/menu/src/utils/isInView.js +26 -0
  37. package/components/menu/src/utils/listItemInteractions.js +104 -0
  38. package/components/menu/src/utils/uuid.js +3 -0
  39. package/components/menu/test/lion-menu.test.js +3 -0
  40. package/components/menu/test-suites/InteractiveListMixin.suite.js +142 -0
  41. package/components/menu/types/InteractiveListMixinTypes.d.ts +82 -0
  42. package/components/navigation-bar/README.md +1 -0
  43. package/components/navigation-bar/src/LionNavigationBar.js +439 -0
  44. package/components/navigation-bar/src/types.js +41 -0
  45. package/components/overlays/NEXT.md +49 -0
  46. package/components/overlays/src/OverlayController.js +244 -111
  47. package/components/overlays/src/OverlayMixin.js +58 -11
  48. package/components/overlays/src/OverlaysManager.js +2 -2
  49. package/components/overlays/src/utils/adopt-styles.js +3 -2
  50. package/components/overlays/src/utils/get-focusable-elements.js +4 -0
  51. package/components/overlays/src/utils/is-equal-config.js +10 -6
  52. package/components/overlays/test/OverlayController.test.js +5 -3
  53. package/components/overlays/test/utils-tests/is-equal-config.test.js +1 -1
  54. package/components/overlays/test-suites/OverlayMixin.suite.js +8 -8
  55. package/components/overlays/types/OverlayConfig.ts +16 -5
  56. package/components/select-rich/test/lion-select-rich.test.js +3 -0
  57. package/custom-elements.json +32144 -19531
  58. package/dist-types/components/button/src/LionButton.d.ts +1 -0
  59. package/dist-types/components/button/src/LionButton.d.ts.map +1 -1
  60. package/dist-types/components/button/test-suites/LionButton.suite.d.ts.map +1 -1
  61. package/dist-types/components/calendar/src/LionCalendar.d.ts.map +1 -1
  62. package/dist-types/components/collapsible/src/DisclosureMixin.d.ts +3 -0
  63. package/dist-types/components/collapsible/src/DisclosureMixin.d.ts.map +1 -0
  64. package/dist-types/components/collapsible/types/DisclosureMixinTypes.d.ts +24 -0
  65. package/dist-types/components/collapsible/types/DisclosureMixinTypes.d.ts.map +1 -0
  66. package/dist-types/components/dialog/src/LionDialog.d.ts +3 -1
  67. package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
  68. package/dist-types/components/form-core/src/form-group/FormGroupMixin.d.ts.map +1 -1
  69. package/dist-types/components/form-core/src/registration/FormRegistrarMixin.d.ts.map +1 -1
  70. package/dist-types/components/form-core/test-suites/ValidateMixin.suite.d.ts.map +1 -1
  71. package/dist-types/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.d.ts.map +1 -1
  72. package/dist-types/components/input-datepicker/src/localizeNamespaceLoader.d.ts.map +1 -1
  73. package/dist-types/components/menu/index.d.ts +3 -0
  74. package/dist-types/components/menu/index.d.ts.map +1 -0
  75. package/dist-types/components/menu/lion-menu-overlay.d.ts +2 -0
  76. package/dist-types/components/menu/lion-menu-overlay.d.ts.map +1 -0
  77. package/dist-types/components/menu/lion-menu.d.ts +2 -0
  78. package/dist-types/components/menu/lion-menu.d.ts.map +1 -0
  79. package/dist-types/components/menu/lion-spinbutton.d.ts +2 -0
  80. package/dist-types/components/menu/lion-spinbutton.d.ts.map +1 -0
  81. package/dist-types/components/menu/lion-toolbar.d.ts +2 -0
  82. package/dist-types/components/menu/lion-toolbar.d.ts.map +1 -0
  83. package/dist-types/components/menu/lion-tree.d.ts +2 -0
  84. package/dist-types/components/menu/lion-tree.d.ts.map +1 -0
  85. package/dist-types/components/menu/src/InteractiveListMixin.d.ts +611 -0
  86. package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -0
  87. package/dist-types/components/menu/src/LionMenu.d.ts +563 -0
  88. package/dist-types/components/menu/src/LionMenu.d.ts.map +1 -0
  89. package/dist-types/components/menu/src/LionMenuHybrid.d.ts +19 -0
  90. package/dist-types/components/menu/src/LionMenuHybrid.d.ts.map +1 -0
  91. package/dist-types/components/menu/src/LionMenuOverlay.d.ts +1007 -0
  92. package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +1 -0
  93. package/dist-types/components/menu/src/LionSpinbutton.d.ts +58 -0
  94. package/dist-types/components/menu/src/LionSpinbutton.d.ts.map +1 -0
  95. package/dist-types/components/menu/src/LionToolbar.d.ts +8 -0
  96. package/dist-types/components/menu/src/LionToolbar.d.ts.map +1 -0
  97. package/dist-types/components/menu/src/LionTree.d.ts +445 -0
  98. package/dist-types/components/menu/src/LionTree.d.ts.map +1 -0
  99. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +31 -0
  100. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +1 -0
  101. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts +493 -0
  102. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -0
  103. package/dist-types/components/menu/src/utils/isInView.d.ts +9 -0
  104. package/dist-types/components/menu/src/utils/isInView.d.ts.map +1 -0
  105. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts +42 -0
  106. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts.map +1 -0
  107. package/dist-types/components/menu/src/utils/uuid.d.ts +2 -0
  108. package/dist-types/components/menu/src/utils/uuid.d.ts.map +1 -0
  109. package/dist-types/components/menu/test/lion-menu.test.d.ts +2 -0
  110. package/dist-types/components/menu/test/lion-menu.test.d.ts.map +1 -0
  111. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts +13 -0
  112. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +1 -0
  113. package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts +238 -0
  114. package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts.map +1 -0
  115. package/dist-types/components/navigation-bar/src/types.d.ts +85 -0
  116. package/dist-types/components/navigation-bar/src/types.d.ts.map +1 -0
  117. package/dist-types/components/overlays/src/OverlayController.d.ts +9 -13
  118. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  119. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  120. package/dist-types/components/overlays/src/utils/get-focusable-elements.d.ts.map +1 -1
  121. package/dist-types/components/overlays/src/utils/is-equal-config.d.ts.map +1 -1
  122. package/dist-types/components/overlays/types/OverlayConfig.d.ts +10 -2
  123. package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
  124. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +8 -2
  125. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  126. package/dist-types/exports/define/lion-item.d.ts +2 -0
  127. package/dist-types/exports/define/lion-item.d.ts.map +1 -0
  128. package/dist-types/exports/define/lion-menu-overlay.d.ts +2 -0
  129. package/dist-types/exports/define/lion-menu-overlay.d.ts.map +1 -0
  130. package/dist-types/exports/define/lion-menu.d.ts +2 -0
  131. package/dist-types/exports/define/lion-menu.d.ts.map +1 -0
  132. package/dist-types/exports/define/lion-navigation-bar.d.ts +2 -0
  133. package/dist-types/exports/define/lion-navigation-bar.d.ts.map +1 -0
  134. package/dist-types/exports/define/lion-toolbar.d.ts +2 -0
  135. package/dist-types/exports/define/lion-toolbar.d.ts.map +1 -0
  136. package/dist-types/exports/define/lion-tree.d.ts +2 -0
  137. package/dist-types/exports/define/lion-tree.d.ts.map +1 -0
  138. package/dist-types/exports/menu.d.ts +7 -0
  139. package/dist-types/exports/menu.d.ts.map +1 -0
  140. package/dist-types/exports/navigation-bar.d.ts +2 -0
  141. package/dist-types/exports/navigation-bar.d.ts.map +1 -0
  142. package/dist-types/exports/overlays.d.ts +3 -0
  143. package/dist-types/exports/overlays.d.ts.map +1 -1
  144. package/dist-types/tsconfig.tsbuildinfo +1 -1
  145. package/docs/components/checkbox-group/overview.md +2 -2
  146. package/docs/components/collapsible/overview.md +1 -1
  147. package/docs/components/combobox/api-table.md +21 -8
  148. package/docs/components/combobox/overview.md +1 -1
  149. package/docs/components/combobox/use-cases.md +1 -1
  150. package/docs/components/dialog/api-table.md +31 -6
  151. package/docs/components/dialog/overview.md +1 -1
  152. package/docs/components/fieldset/overview.md +5 -5
  153. package/docs/components/form/overview.md +3 -3
  154. package/docs/components/form/use-cases.md +2 -2
  155. package/docs/components/icon/use-cases.md +1 -1
  156. package/docs/components/input/overview.md +1 -1
  157. package/docs/components/input-amount/overview.md +3 -3
  158. package/docs/components/input-date/overview.md +3 -3
  159. package/docs/components/input-datepicker/api-table.md +21 -4
  160. package/docs/components/input-datepicker/overview.md +5 -5
  161. package/docs/components/input-email/overview.md +2 -2
  162. package/docs/components/input-file/overview.md +1 -1
  163. package/docs/components/input-iban/overview.md +2 -2
  164. package/docs/components/input-range/overview.md +2 -2
  165. package/docs/components/input-stepper/overview.md +1 -1
  166. package/docs/components/input-tel/overview.md +1 -1
  167. package/docs/components/input-tel/use-cases.md +6 -6
  168. package/docs/components/input-tel-dropdown/overview.md +1 -1
  169. package/docs/components/listbox/use-cases.md +1 -1
  170. package/docs/components/menu/api-table.md +450 -0
  171. package/docs/components/menu/applyMenuDemoStyles.js +173 -0
  172. package/docs/components/menu/extensions.md +235 -0
  173. package/docs/components/menu/index.md +13 -0
  174. package/docs/components/menu/overview.md +422 -0
  175. package/docs/components/radio-group/overview.md +3 -3
  176. package/docs/components/select/use-cases.md +1 -1
  177. package/docs/components/select-rich/api-table.md +21 -5
  178. package/docs/components/select-rich/use-cases.md +2 -2
  179. package/docs/components/tabs/examples.md +1 -1
  180. package/docs/components/tooltip/api-table.md +36 -11
  181. package/docs/fundamentals/index.md +4 -4
  182. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  183. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  184. package/docs/fundamentals/rationales/accessibility.md +1 -1
  185. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  186. package/docs/fundamentals/systems/core/overview.md +1 -1
  187. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  188. package/docs/fundamentals/systems/form/model-value.md +1 -1
  189. package/docs/fundamentals/systems/form/overview.md +28 -28
  190. package/docs/fundamentals/systems/form/validate.md +2 -2
  191. package/docs/fundamentals/systems/localize/overview.md +4 -4
  192. package/docs/fundamentals/systems/overlays/api-table.md +34 -34
  193. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  194. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  195. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  196. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  197. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  198. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  199. package/docs/guides/how-to/create-a-form.md +4 -4
  200. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  201. package/docs/guides/how-to/index.md +1 -1
  202. package/docs/guides/index.md +8 -8
  203. package/docs/guides/principles/index.md +5 -5
  204. package/docs/guides/principles/styling.md +1 -1
  205. package/exports/collapsible.js +1 -0
  206. package/exports/define/lion-item.js +3 -0
  207. package/exports/define/lion-menu-overlay.js +3 -0
  208. package/exports/define/lion-menu.js +3 -0
  209. package/exports/define/lion-navigation-bar.js +3 -0
  210. package/exports/define/lion-toolbar.js +3 -0
  211. package/exports/define/lion-tree.js +3 -0
  212. package/exports/menu.js +7 -0
  213. package/exports/navigation-bar.js +1 -0
  214. package/exports/overlays.js +6 -0
  215. package/menu.js +7 -0
  216. package/navigation-bar.js +1 -0
  217. package/overlays.js +6 -0
  218. package/package.json +1 -1
@@ -0,0 +1,274 @@
1
+ /* eslint-disable import/no-extraneous-dependencies */
2
+ import { html } from 'lit';
3
+ import { dedupeMixin } from '@open-wc/dedupe-mixin';
4
+ // import { DisclosureMixin } from '@lion/ui/collapsible.js';
5
+ import { InteractiveListMixin } from './InteractiveListMixin.js';
6
+
7
+ /**
8
+ * @typedef {import('./InteractiveListMixin.js').LionItem} LionItem
9
+ * @typedef {import('../types/InteractiveListMixinTypes.js').InteractiveListHost} InteractiveList
10
+ */
11
+
12
+ /**
13
+ * All logic that is needed for interactive lists that are allowed to have multiple nested, collapsible levels
14
+ * Applies to [role=menu] and [role=tree]
15
+ */
16
+ // @ts-ignore - mixin implementation
17
+ const MultiLevelListMixinImplementation = superclass =>
18
+ // @ts-ignore - mixin class extension
19
+ class extends InteractiveListMixin(superclass) {
20
+ render() {
21
+ return html`
22
+ <slot name="invoker"></slot>
23
+ <slot name="list"></slot>
24
+ <slot id="list-items-outlet"></slot>
25
+ `;
26
+ }
27
+
28
+ static get properties() {
29
+ return {
30
+ invokerNode: Object,
31
+ behaveAsAccordion: { type: Boolean, attribute: 'behave-as-accordion' },
32
+ level: { type: Number, reflect: true },
33
+ };
34
+ }
35
+
36
+ /**
37
+ * @configure DisclosureMixin
38
+ */
39
+ get _invokerNode() {
40
+ // @ts-ignore - accessing static method and super property
41
+ return this.constructor._getFocusableInvokerEl(this.invokerNode) || super._invokerNode;
42
+ }
43
+
44
+ /**
45
+ * @configure DisclosureMixin
46
+ * This will be compatible with DisclosureMixin and OverlayMixin. It contains the invoked
47
+ * InteractiveList widget (like menu|tree).
48
+ */
49
+ get _contentNode() {
50
+ return this._listNode;
51
+ }
52
+
53
+ constructor() {
54
+ super();
55
+
56
+ /**
57
+ * When an invokerNode is supplied (usually by a parent level menu), it will take precedence
58
+ * over [slot=invoker] and previousElementSibling with [data-invoker] attribute
59
+ * @type {HTMLElement}
60
+ */
61
+ // @ts-ignore - can be undefined initially
62
+ this.invokerNode = undefined;
63
+ /**
64
+ * @configure DisclosureMixin
65
+ */
66
+ this.handleFocus = true;
67
+ /**
68
+ * The parent list if level > 1
69
+ * @type {InteractiveList}
70
+ */
71
+ // @ts-ignore - can be undefined initially
72
+ this.parentList = undefined;
73
+ /**
74
+ * When true, will have a maximum of one submenu open at a time
75
+ */
76
+ this.behaveAsAccordion = false;
77
+ /**
78
+ * The level of nested menus. Will be reflected as attribute for styling purposes
79
+ */
80
+ this.level = 1;
81
+ /**
82
+ * @configure InteractiveListMixin
83
+ */
84
+ this.noPreselect = true;
85
+ /**
86
+ * @configure InteractiveListMixin
87
+ */
88
+ this._activeMode = 'roving-tabindex';
89
+ /**
90
+ * @type {Map<HTMLElement|LionItem,InteractiveList>}
91
+ */
92
+ this._subListMap = new Map();
93
+ }
94
+
95
+ // /**
96
+ // * @param {import('lit-element').PropertyValues } changedProperties
97
+ // */
98
+ // updated(changedProperties) {
99
+ // super.updated(changedProperties);
100
+
101
+ // if (changedProperties.has('invokerNode')) {
102
+ // // TODO: update openableCtrl invokerNode once we make this a ctrl as well
103
+ // }
104
+ // }
105
+
106
+ /**
107
+ * Gets the child InteractiveList, based on item
108
+ * @param {LionItem|HTMLElement} item can be sibling of or parent of InteractiveList
109
+ */
110
+ static _getSubInteractiveList(item) {
111
+ /**
112
+ * Note that, according to W3C spec, the sub level element with [role=menu|menubar|tree]
113
+ * needs to be a sibling of the invoker element with
114
+ * [role=menuitem|menuitemchecbox|menuitemradio|treeitem] (and not the parent)
115
+ *
116
+ * In the example below <lion-menu> is considered as item
117
+ * @example
118
+ * <lion-menuitem>
119
+ * <button slot="invoker"></button>
120
+ * <lion-menu> ... </lion-menu>
121
+ * </lion-menuitem>
122
+ */
123
+ // @ts-ignore - isInteractiveList property check
124
+ const siblingOfInvoker = item.nextElementSibling?.isInteractiveList;
125
+ if (siblingOfInvoker) {
126
+ return item.nextElementSibling;
127
+ }
128
+ /**
129
+ * In the example below div[role=listitem] | [data-item] is considered as item
130
+ * @example
131
+ * <div role="listitem" data-item>
132
+ * <button slot="invoker"></button>
133
+ * <lion-menu> ... </lion-menu>
134
+ * </div>
135
+ */
136
+ const childOfInvoker =
137
+ item.getAttribute?.('role') === 'listitem' || item.hasAttribute?.('data-item');
138
+ if (childOfInvoker) {
139
+ // @ts-ignore - isInteractiveList check
140
+ return Array.from(item.children).find(child => child.isInteractiveList);
141
+ }
142
+ return undefined;
143
+ }
144
+
145
+ /**
146
+ * @param {*} newItems
147
+ */
148
+ _initListItems(newItems) {
149
+ super._initListItems(newItems);
150
+
151
+ const ctor = this.constructor;
152
+ // @ts-ignore - forEach parameter types
153
+ newItems.forEach(item => {
154
+ // @ts-ignore - static method access
155
+ const subList = ctor._getSubInteractiveList(item);
156
+ if (subList) {
157
+ this._subListMap.set(item, subList);
158
+ subList.level = this.level + 1;
159
+ subList.parentList = this;
160
+ subList.invokerNode = item;
161
+ // @ts-ignore - invokerInteraction property
162
+ subList.invokerInteraction = this.invokerInteraction;
163
+ subList._activeMode = this._activeMode;
164
+ }
165
+ });
166
+ }
167
+
168
+ /**
169
+ * @enhance DisclosureMixin
170
+ * @param {Event} [ev]
171
+ */
172
+ toggle(ev) {
173
+ // @ts-ignore - target property
174
+ if (ev && this._listNode.contains(ev.target)) {
175
+ // prevent nested menus (inside invokers) from triggering invoker
176
+ return;
177
+ }
178
+ // @ts-ignore - super.toggle method
179
+ super.toggle();
180
+ }
181
+
182
+ /**
183
+ * @enhance InteractiveListMixin
184
+ * @param {*} ev
185
+ */
186
+ _onListKeyDown(ev) {
187
+ const targetFromsubList = Array.from(this._subListMap).find(
188
+ // @ts-ignore - contains method on InteractiveListHost
189
+ ([, s]) => s === ev.target || s.contains(ev.target),
190
+ );
191
+
192
+ if (targetFromsubList) {
193
+ return;
194
+ }
195
+
196
+ super._onListKeyDown(ev);
197
+
198
+ const subListOfActiveItem = this._subListMap.get(this.activeItem);
199
+ const parentListOfActiveItem = this.parentList;
200
+ const { key } = ev;
201
+
202
+ if (key === 'Escape' && parentListOfActiveItem) {
203
+ // We need to stop here, or else we affect parent menu (handled by OverlayController)
204
+ ev.stopPropagation();
205
+ }
206
+
207
+ switch (key) {
208
+ case 'Enter':
209
+ case ' ':
210
+ // make it work like a button
211
+ this.activeItem?.click();
212
+ break;
213
+ case 'ArrowDown':
214
+ if (
215
+ this.orientation === 'horizontal' &&
216
+ subListOfActiveItem &&
217
+ this._activeMode !== 'disclosure'
218
+ ) {
219
+ this.activeItem.click();
220
+ }
221
+ break;
222
+ case 'ArrowRight':
223
+ if (this.orientation === 'vertical' && subListOfActiveItem) {
224
+ this.activeItem.click();
225
+ }
226
+ break;
227
+ case 'ArrowUp':
228
+ if (this.orientation === 'horizontal' && parentListOfActiveItem) {
229
+ // @ts-ignore - close method
230
+ this.close();
231
+ // @ts-ignore - _onOverlayShow method
232
+ parentListOfActiveItem._onOverlayShow();
233
+ }
234
+ break;
235
+ case 'ArrowLeft':
236
+ if (this.orientation === 'vertical' && parentListOfActiveItem) {
237
+ // @ts-ignore - close method
238
+ this.close();
239
+ // @ts-ignore - _onOverlayShow method
240
+ parentListOfActiveItem._onOverlayShow();
241
+ }
242
+ break;
243
+ /* no default */
244
+ }
245
+ }
246
+
247
+ // /**
248
+ // * @enhance DisclosureMixin
249
+ // */
250
+ // _setupDisclosure() {
251
+ // if (!this._invokerNode) return;
252
+
253
+ // super._setupDisclosure();
254
+ // }
255
+
256
+ // /**
257
+ // * @enhance DisclosureMixin
258
+ // */
259
+ // _teardownDisclosure() {
260
+ // if (!this._invokerNode) return;
261
+
262
+ // super._teardownDisclosure();
263
+ // }
264
+
265
+ // /**
266
+ // * @enhance DisclosureMixin
267
+ // */
268
+ // async _onOpenedChanged() {
269
+ // if (!this._invokerNode) return;
270
+
271
+ // await super._onOpenedChanged();
272
+ // }
273
+ };
274
+ export const MultiLevelListMixin = dedupeMixin(MultiLevelListMixinImplementation);
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Determines whether element is visible within container
3
+ *
4
+ * @param {HTMLElement} container
5
+ * @param {HTMLElement} element
6
+ * @param {Boolean} [partial]
7
+ */
8
+ export function isInView(container, element, partial = false) {
9
+ const cTop = container.scrollTop;
10
+ const cBottom = cTop + container.clientHeight;
11
+ const eTop = element.offsetTop;
12
+ const eBottom = eTop + element.clientHeight;
13
+ const isTotal = eTop >= cTop && eBottom <= cBottom;
14
+ let isPartial;
15
+
16
+ if (partial === true) {
17
+ isPartial = (eTop < cTop && eBottom > cTop) || (eBottom > cBottom && eTop < cBottom);
18
+ } else if (typeof partial === 'number') {
19
+ if (eTop < cTop && eBottom > cTop) {
20
+ isPartial = ((eBottom - cTop) * 100) / element.clientHeight > partial;
21
+ } else if (eBottom > cBottom && eTop < cBottom) {
22
+ isPartial = ((cBottom - eTop) * 100) / element.clientHeight > partial;
23
+ }
24
+ }
25
+ return isTotal || isPartial;
26
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Set of universal interactive list item state methods that are both compatible with
3
+ * 'smart' (LionOption) and 'regular' items
4
+ */
5
+
6
+ /**
7
+ * @param {HTMLElement} item
8
+ */
9
+ export function isDisabled(item) {
10
+ return item.hasAttribute('disabled') || item.getAttribute('aria-disabled') === 'true';
11
+ }
12
+
13
+ /**
14
+ * @param {HTMLElement} item
15
+ */
16
+ export function setDisabled(item, unset = false) {
17
+ if (unset) {
18
+ item.removeAttribute('disabled');
19
+ item.setAttribute('aria-disabled', 'false');
20
+ } else {
21
+ item.setAttribute('disabled', '');
22
+ item.setAttribute('aria-disabled', 'true');
23
+ }
24
+ }
25
+
26
+ /**
27
+ * @param {HTMLElement} item
28
+ */
29
+ export function toggleDisabled(item) {
30
+ setDisabled(item, isDisabled(item));
31
+ }
32
+
33
+ /**
34
+ * @param {HTMLElement} item
35
+ */
36
+ export function isChecked(item) {
37
+ return item.hasAttribute('checked') || item.getAttribute('aria-selected') === 'true';
38
+ }
39
+
40
+ /**
41
+ * @param {HTMLElement} item
42
+ * @param {boolean} [shouldSet=false]
43
+ */
44
+ export function setChecked(item, shouldSet = true) {
45
+ const ariaAttr = item.getAttribute('role') === 'option' ? 'selected' : 'checked';
46
+ if (shouldSet) {
47
+ item.setAttribute('checked', '');
48
+ item.setAttribute(`aria-${ariaAttr}`, 'true');
49
+ } else {
50
+ item.removeAttribute('checked');
51
+ item.setAttribute(`aria-${ariaAttr}`, 'false');
52
+ }
53
+ }
54
+
55
+ /**
56
+ * @param {HTMLElement} item
57
+ */
58
+ export function toggleChecked(item) {
59
+ setChecked(item, !isChecked(item));
60
+ }
61
+
62
+ /**
63
+ * @param {HTMLElement} item
64
+ */
65
+ export function isActive(item) {
66
+ return item.hasAttribute('active');
67
+ }
68
+
69
+ /**
70
+ * @param {HTMLElement} item
71
+ */
72
+ export function setActive(item, unset = false) {
73
+ if (unset) {
74
+ item.removeAttribute('active');
75
+ } else {
76
+ item.setAttribute('active', '');
77
+ }
78
+ }
79
+
80
+ /**
81
+ * @param {HTMLElement} item
82
+ */
83
+ function getDirectTextContent(item) {
84
+ return Array.from(item.childNodes)
85
+ .filter(n => n.nodeName === '#text')
86
+ .map(n => n.textContent)
87
+ .join('')
88
+ .trim();
89
+ }
90
+
91
+ /**
92
+ * @param {HTMLElement} item
93
+ */
94
+ export function getValue(item) {
95
+ return (
96
+ // @ts-ignore - choiceValue is a custom property
97
+ item.choiceValue ||
98
+ // @ts-ignore - value might not exist on all HTMLElements
99
+ item.value ||
100
+ item.getAttribute('value') ||
101
+ item.getAttribute('data-value') ||
102
+ getDirectTextContent(item)
103
+ );
104
+ }
@@ -0,0 +1,3 @@
1
+ export function uuid() {
2
+ return Math.random().toString(36).substr(2, 10);
3
+ }
@@ -0,0 +1,3 @@
1
+ import { runInteractiveListMixinSuite } from '../test-suites/InteractiveListMixin.suite.js';
2
+
3
+ runInteractiveListMixinSuite();
@@ -0,0 +1,142 @@
1
+ import { LitElement } from 'lit';
2
+ import { expect, html, fixture, unsafeStatic, defineCE } from '@open-wc/testing';
3
+ import { InteractiveListMixin } from '../src/InteractiveListMixin.js';
4
+
5
+ class InteractiveListClass extends InteractiveListMixin(LitElement) {}
6
+
7
+ /**
8
+ * @typedef {import('../types/InteractiveListMixinTypes.js').InteractiveListItemRole} InteractiveListItemRole
9
+ */
10
+
11
+ /** @type {InteractiveListItemRole[]} */
12
+ const supportedRoles = [
13
+ 'menuitem',
14
+ 'menuitemcheckbox',
15
+ 'menuitemradio',
16
+ 'option',
17
+ 'treeitem',
18
+ 'radio',
19
+ 'checkbox',
20
+ ];
21
+
22
+ /**
23
+ * @typedef {Object} TestConfig
24
+ * @property {string} tagString
25
+ */
26
+
27
+ /**
28
+ * @param {TestConfig} [customConfig]
29
+ */
30
+ export function runInteractiveListMixinSuite(customConfig) {
31
+ const cfg = {
32
+ tagString: customConfig?.tagString || defineCE(InteractiveListClass),
33
+ };
34
+
35
+ const { tagString } = cfg;
36
+ const tag = unsafeStatic(tagString);
37
+
38
+ describe('InteractiveListMixin', () => {
39
+ it(`supports items with role="${supportedRoles.join('|')}"`, async () => {
40
+ const el = await fixture(html`
41
+ <${tag} name="foo">
42
+ ${supportedRoles.map(role => html` <div role="${role}"></div> `)}
43
+ </${tag}>
44
+ `);
45
+ // @ts-ignore - test element with InteractiveListMixin properties
46
+ expect(el.listItems.length).to.equal(supportedRoles.length);
47
+ supportedRoles.forEach((role, i) => {
48
+ // @ts-ignore - test element with InteractiveListMixin properties
49
+ expect(el.listItems[i].getAttribute('role')).to.equal(role);
50
+ });
51
+ });
52
+
53
+ it('moves elements to a private [slot=list] for optimal DX and a11y', async () => {
54
+ const el = await fixture(html`
55
+ <${tag} name="foo">
56
+ <div role="menuitem" id="item1">Item 1</div>
57
+ <div role="menuitem" id="item2">Item 2</div>
58
+ </${tag}>
59
+ `);
60
+ // @ts-ignore - test element with InteractiveListMixin properties
61
+ const listSlotNode = Array.from(el.children).find(c => c.slot === 'list');
62
+ // @ts-ignore - accessing children property
63
+ const listItemEls = Array.from(listSlotNode.children);
64
+ // @ts-ignore - test element with InteractiveListMixin properties
65
+ expect(listItemEls.includes(el.listItems[0])).to.be.true;
66
+ // @ts-ignore - test element with InteractiveListMixin properties
67
+ expect(listItemEls.includes(el.listItems[1])).to.be.true;
68
+ });
69
+
70
+ it('inherits from DisclosureMixin', async () => {});
71
+
72
+ describe('Active mode', async () => {
73
+ it('supports "activedescendant" pattern', async () => {
74
+ const el = await fixture(html`
75
+ <${tag} name="foo" ._activeMode="${'activedescendant'}">
76
+ <div role="menuitem" id="item1">Item 1</div>
77
+ <div role="menuitem" id="item2">Item 2</div>
78
+ </${tag}>
79
+ `);
80
+ // @ts-ignore - test element with InteractiveListMixin properties
81
+ el.checkedIndex = 0;
82
+ // @ts-ignore - test element with InteractiveListMixin properties
83
+ const listSlotNode = Array.from(el.children).find(c => c.slot === 'list');
84
+
85
+ // @ts-ignore - test element with InteractiveListMixin properties
86
+ // @ts-ignore - test element with InteractiveListMixin properties
87
+ expect(el.listItems[0].hasAttribute('id')).to.be.true;
88
+ // @ts-ignore - test element with InteractiveListMixin properties
89
+ expect(el.listItems[1].hasAttribute('id')).to.be.true;
90
+ expect(listSlotNode?.hasAttribute('aria-activedescendant')).to.be.false;
91
+ // @ts-ignore - test element with InteractiveListMixin properties
92
+ el.activeIndex = 1;
93
+ expect(listSlotNode?.hasAttribute('aria-activedescendant')).to.be.true;
94
+ });
95
+
96
+ it('supports "roving-tabindex" pattern', async () => {
97
+ const el = await fixture(html`
98
+ <${tag} name="foo" ._activeMode="${'roving-tabindex'}">
99
+ <div role="menuitem" id="item1">Item 1</div>
100
+ <div role="menuitem" id="item2">Item 2</div>
101
+ </${tag}>
102
+ `);
103
+
104
+ // @ts-ignore - test element with InteractiveListMixin properties
105
+ el.activeIndex = 0;
106
+ // @ts-ignore - test element with InteractiveListMixin properties
107
+ expect(el.listItems[0].getAttribute('tabindex')).to.equal('0');
108
+ // @ts-ignore - test element with InteractiveListMixin properties
109
+ expect(el.listItems[1].getAttribute('tabindex')).to.equal('-1');
110
+
111
+ // @ts-ignore - test element with InteractiveListMixin properties
112
+ el.activeIndex = 1;
113
+ // @ts-ignore - test element with InteractiveListMixin properties
114
+ expect(el.listItems[0].getAttribute('tabindex')).to.equal('-1');
115
+ // @ts-ignore - test element with InteractiveListMixin properties
116
+ expect(el.listItems[1].getAttribute('tabindex')).to.equal('0');
117
+
118
+ // @ts-ignore - test element with InteractiveListMixin properties
119
+ el.activeIndex = 1;
120
+ // @ts-ignore - test element with InteractiveListMixin properties
121
+ expect(document.activeElement).to.equal(el.listItems[1]);
122
+ });
123
+ });
124
+
125
+ // Copy tests from ListboxMixin. Later, make this mixin a fundament of the ListboxMixin
126
+
127
+ // orientation
128
+ // multipleChoice
129
+ // selectionFollowsFocus
130
+ // rotateKeyboardNavigation
131
+ // noPreselect
132
+ // _scrollTargetNode
133
+
134
+ describe('Widget extensions', () => {
135
+ it('can be extended to [role=listbox]', async () => {});
136
+ it('can be extended to [role=menu]', async () => {});
137
+ it('can be extended to [role=menubar]', async () => {});
138
+ it('can be extended to [role=toolbar]', async () => {});
139
+ it('can be extended to [role=tree]', async () => {});
140
+ });
141
+ });
142
+ }
@@ -0,0 +1,82 @@
1
+ import { Constructor } from '@open-wc/dedupe-mixin';
2
+ import { LitElement } from 'lit';
3
+ import { SlotHost } from '../../core/types/SlotMixinTypes.js';
4
+ import { DisabledHost } from '../../core/types/DisabledMixinTypes.js';
5
+
6
+ export type InteractiveListItemRole =
7
+ | 'menuitem'
8
+ | 'menuitemcheckbox'
9
+ | 'menuitemradio'
10
+ | 'option'
11
+ | 'treeitem'
12
+ | 'radio'
13
+ | 'checkbox';
14
+
15
+ export declare class InteractiveListHost {
16
+ /**
17
+ * When true, will synchronize activedescendant and selected element on
18
+ * arrow key navigation.
19
+ * This behavior can usually be seen on <select> on the Windows platform.
20
+ * Note that this behavior cannot be used when multiple-choice is true.
21
+ * See: https://www.w3.org/TR/wai-aria-practices/#kbd_selection_follows_focus
22
+ */
23
+ public selectionFollowsFocus: Boolean;
24
+ /**
25
+ * Will give first option active state when navigated to the next option from
26
+ * last option.
27
+ */
28
+ public rotateKeyboardNavigation: Boolean;
29
+ /**
30
+ * Informs screen reader and affects keyboard navigation.
31
+ * By default 'vertical'
32
+ */
33
+ public orientation: 'vertical' | 'horizontal';
34
+
35
+ public hasNoDefaultSelected: boolean;
36
+
37
+ public singleOption: boolean;
38
+
39
+ public checkedIndex: number | number[];
40
+
41
+ public activeIndex: number;
42
+
43
+ public listItems: HTMLElement[];
44
+
45
+ public setCheckedIndex(index: number): void;
46
+
47
+ protected _scrollTargetNode: HTMLElement;
48
+
49
+ protected _listNode: HTMLElement;
50
+
51
+ // private __setupListboxNode(): void;
52
+
53
+ protected _getPreviousEnabledOption(currentIndex: number, offset?: number): number;
54
+
55
+ protected _getNextEnabledOption(currentIndex: number, offset?: number): number;
56
+
57
+ protected _onListKeyDown(ev: KeyboardEvent): void;
58
+
59
+ protected _onListKeyUp(ev: KeyboardEvent): void;
60
+
61
+ // protected _setupListboxNode(): void;
62
+
63
+ // protected _teardownListboxNode(): void;
64
+
65
+ protected _onListClick(ev: MouseEvent): void;
66
+
67
+ // protected _setupListboxInteractions(): void;
68
+
69
+ // protected _onChildActiveChanged(ev: Event): void;
70
+ }
71
+
72
+ export declare function InteractiveListHostImplementation<T extends Constructor<LitElement>>(
73
+ superclass: T,
74
+ ): T &
75
+ Constructor<InteractiveListHost> &
76
+ InteractiveListHost &
77
+ Constructor<DisabledHost> &
78
+ typeof DisabledHost &
79
+ Constructor<SlotHost> &
80
+ typeof SlotHost;
81
+
82
+ export type InteractiveListMixin = typeof InteractiveListHostImplementation;
@@ -0,0 +1 @@
1
+ N.B. api is very very beta