@lion/ui 0.22.0 → 0.23.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 (218) hide show
  1. package/README.md +35 -0
  2. package/components/combobox/src/LionCombobox.js +10 -2
  3. package/components/combobox/test/lion-combobox.test.js +31 -0
  4. package/components/form-core/src/choice-group/ChoiceGroupMixin.js +7 -0
  5. package/components/form-core/types/choice-group/ChoiceGroupMixinTypes.ts +5 -0
  6. package/components/input-amount/src/LionInputAmount.js +17 -6
  7. package/components/input-amount/src/formatters.js +5 -3
  8. package/components/input-amount/test/formatters.test.js +88 -74
  9. package/components/input-amount/test/lion-input-amount.test.js +51 -36
  10. package/components/input-tel/test/validators.test.js +9 -0
  11. package/components/listbox/src/ListboxMixin.js +19 -3
  12. package/components/listbox/types/ListboxMixinTypes.ts +7 -1
  13. package/components/menu/README.md +360 -0
  14. package/components/menu/src/InteractiveListMixin.js +867 -0
  15. package/components/menu/src/LionMenu.js +436 -0
  16. package/components/menu/src/MoreButtonMenuMixin.js +295 -0
  17. package/components/menu/src/MultiLevelListMixin.js +312 -0
  18. package/components/menu/src/utils/isInView.js +26 -0
  19. package/components/menu/src/utils/listItemInteractions.js +124 -0
  20. package/components/menu/test/InteractiveListMixin.test.js +3 -0
  21. package/components/menu/test/MoreButtonMenuMixin.test.js +4 -0
  22. package/components/menu/test/MultiLevelListMixin.test.js +3 -0
  23. package/components/menu/test/lion-menu-interactions.test.js +3 -0
  24. package/components/menu/test/lion-menu.test.js +14 -0
  25. package/components/menu/test-helpers/mimicKeyPress.js +9 -0
  26. package/components/menu/test-suites/InteractiveListMixin.suite.js +537 -0
  27. package/components/menu/test-suites/LionMenu.suite.js +114 -0
  28. package/components/menu/test-suites/LionMenuInteractions.suite.js +583 -0
  29. package/components/menu/test-suites/MoreButtonMenuMixin.suite.js +372 -0
  30. package/components/menu/test-suites/MultiLevelListMixin.suite.js +309 -0
  31. package/components/menu/types/InteractiveListMixinTypes.ts +126 -0
  32. package/components/menu/types/MoreButtonMenuMixinTypes.ts +51 -0
  33. package/components/menu/types/MultiLevelListMixinTypes.ts +28 -0
  34. package/components/menu/types/OverlayWithListInvokerMixinTypes.ts +61 -0
  35. package/components/overlays/NEXT.md +49 -0
  36. package/components/overlays/src/OverlayController.js +304 -125
  37. package/components/overlays/src/OverlayMixin.js +81 -17
  38. package/components/overlays/src/utils/adopt-styles.js +1 -1
  39. package/components/overlays/src/utils/is-equal-config.js +15 -6
  40. package/components/overlays/test/OverlayController.test.js +223 -25
  41. package/components/overlays/test-suites/OverlayMixin.suite.js +8 -8
  42. package/components/overlays/types/ArrowMixinTypes.ts +1 -3
  43. package/components/overlays/types/OverlayConfig.ts +20 -5
  44. package/components/overlays/types/OverlayMixinTypes.ts +2 -0
  45. package/components/select-rich/src/LionSelectRich.js +12 -0
  46. package/components/select-rich/test/lion-select-rich-interaction.test.js +53 -0
  47. package/components/select-rich/test/lion-select-rich-upgrade.test.js +30 -0
  48. package/custom-elements.json +3120 -149
  49. package/dist-types/components/combobox/src/LionCombobox.d.ts +1 -1
  50. package/dist-types/components/combobox/src/LionCombobox.d.ts.map +1 -1
  51. package/dist-types/components/dialog/src/LionDialog.d.ts +6 -2
  52. package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
  53. package/dist-types/components/form-core/src/choice-group/ChoiceGroupMixin.d.ts.map +1 -1
  54. package/dist-types/components/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts +5 -0
  55. package/dist-types/components/form-core/types/choice-group/ChoiceGroupMixinTypes.d.ts.map +1 -1
  56. package/dist-types/components/input-amount/src/LionInputAmount.d.ts +3 -1
  57. package/dist-types/components/input-amount/src/LionInputAmount.d.ts.map +1 -1
  58. package/dist-types/components/input-amount/src/formatters.d.ts +6 -4
  59. package/dist-types/components/input-amount/src/formatters.d.ts.map +1 -1
  60. package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts +1 -1
  61. package/dist-types/components/listbox/src/ListboxMixin.d.ts.map +1 -1
  62. package/dist-types/components/listbox/types/ListboxMixinTypes.d.ts +7 -1
  63. package/dist-types/components/listbox/types/ListboxMixinTypes.d.ts.map +1 -1
  64. package/dist-types/components/menu/src/InteractiveListMixin.d.ts +35 -0
  65. package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -0
  66. package/dist-types/components/menu/src/LionMenu.d.ts +129 -0
  67. package/dist-types/components/menu/src/LionMenu.d.ts.map +1 -0
  68. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +8 -0
  69. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +1 -0
  70. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts +7 -0
  71. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -0
  72. package/dist-types/components/menu/src/utils/isInView.d.ts +9 -0
  73. package/dist-types/components/menu/src/utils/isInView.d.ts.map +1 -0
  74. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts +48 -0
  75. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts.map +1 -0
  76. package/dist-types/components/menu/test/InteractiveListMixin.test.d.ts +2 -0
  77. package/dist-types/components/menu/test/InteractiveListMixin.test.d.ts.map +1 -0
  78. package/dist-types/components/menu/test/MoreButtonMenuMixin.test.d.ts +2 -0
  79. package/dist-types/components/menu/test/MoreButtonMenuMixin.test.d.ts.map +1 -0
  80. package/dist-types/components/menu/test/MultiLevelListMixin.test.d.ts +2 -0
  81. package/dist-types/components/menu/test/MultiLevelListMixin.test.d.ts.map +1 -0
  82. package/dist-types/components/menu/test/lion-menu-interactions.test.d.ts +2 -0
  83. package/dist-types/components/menu/test/lion-menu-interactions.test.d.ts.map +1 -0
  84. package/dist-types/components/menu/test/lion-menu.test.d.ts +2 -0
  85. package/dist-types/components/menu/test/lion-menu.test.d.ts.map +1 -0
  86. package/dist-types/components/menu/test-helpers/mimicKeyPress.d.ts +7 -0
  87. package/dist-types/components/menu/test-helpers/mimicKeyPress.d.ts.map +1 -0
  88. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts +11 -0
  89. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +1 -0
  90. package/dist-types/components/menu/test-suites/LionMenu.suite.d.ts +9 -0
  91. package/dist-types/components/menu/test-suites/LionMenu.suite.d.ts.map +1 -0
  92. package/dist-types/components/menu/test-suites/LionMenuInteractions.suite.d.ts +5 -0
  93. package/dist-types/components/menu/test-suites/LionMenuInteractions.suite.d.ts.map +1 -0
  94. package/dist-types/components/menu/test-suites/MoreButtonMenuMixin.suite.d.ts +11 -0
  95. package/dist-types/components/menu/test-suites/MoreButtonMenuMixin.suite.d.ts.map +1 -0
  96. package/dist-types/components/menu/test-suites/MultiLevelListMixin.suite.d.ts +11 -0
  97. package/dist-types/components/menu/test-suites/MultiLevelListMixin.suite.d.ts.map +1 -0
  98. package/dist-types/components/menu/types/InteractiveListMixinTypes.d.ts +72 -0
  99. package/dist-types/components/menu/types/InteractiveListMixinTypes.d.ts.map +1 -0
  100. package/dist-types/components/menu/types/MoreButtonMenuMixinTypes.d.ts +30 -0
  101. package/dist-types/components/menu/types/MoreButtonMenuMixinTypes.d.ts.map +1 -0
  102. package/dist-types/components/menu/types/MultiLevelListMixinTypes.d.ts +16 -0
  103. package/dist-types/components/menu/types/MultiLevelListMixinTypes.d.ts.map +1 -0
  104. package/dist-types/components/menu/types/OverlayWithListInvokerMixinTypes.d.ts +35 -0
  105. package/dist-types/components/menu/types/OverlayWithListInvokerMixinTypes.d.ts.map +1 -0
  106. package/dist-types/components/overlays/src/OverlayController.d.ts +22 -5
  107. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  108. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  109. package/dist-types/components/overlays/src/utils/is-equal-config.d.ts.map +1 -1
  110. package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts.map +1 -1
  111. package/dist-types/components/overlays/types/OverlayConfig.d.ts +12 -2
  112. package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
  113. package/dist-types/components/overlays/types/OverlayMixinTypes.d.ts +1 -0
  114. package/dist-types/components/overlays/types/OverlayMixinTypes.d.ts.map +1 -1
  115. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +13 -2
  116. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  117. package/dist-types/components/select-rich/test/lion-select-rich-interaction.test.d.ts.map +1 -1
  118. package/dist-types/components/select-rich/test/lion-select-rich-upgrade.test.d.ts +2 -0
  119. package/dist-types/components/select-rich/test/lion-select-rich-upgrade.test.d.ts.map +1 -0
  120. package/dist-types/components/tooltip/src/LionTooltip.d.ts +1 -1
  121. package/dist-types/exports/define/lion-item.d.ts +2 -0
  122. package/dist-types/exports/define/lion-item.d.ts.map +1 -0
  123. package/dist-types/exports/define/lion-menu.d.ts +2 -0
  124. package/dist-types/exports/define/lion-menu.d.ts.map +1 -0
  125. package/dist-types/exports/define/lion-menuitem.d.ts +2 -0
  126. package/dist-types/exports/define/lion-menuitem.d.ts.map +1 -0
  127. package/dist-types/exports/menu-test-suites.d.ts +4 -0
  128. package/dist-types/exports/menu-test-suites.d.ts.map +1 -0
  129. package/dist-types/exports/menu.d.ts +3 -0
  130. package/dist-types/exports/menu.d.ts.map +1 -0
  131. package/dist-types/exports/overlays.d.ts +6 -0
  132. package/dist-types/exports/overlays.d.ts.map +1 -1
  133. package/dist-types/tsconfig.tsbuildinfo +1 -1
  134. package/docs/components/checkbox-group/overview.md +2 -2
  135. package/docs/components/collapsible/overview.md +1 -1
  136. package/docs/components/combobox/api-table.md +53 -39
  137. package/docs/components/combobox/overview.md +1 -1
  138. package/docs/components/combobox/use-cases.md +1 -1
  139. package/docs/components/dialog/api-table.md +32 -6
  140. package/docs/components/dialog/overview.md +1 -1
  141. package/docs/components/fieldset/overview.md +5 -5
  142. package/docs/components/form/overview.md +3 -3
  143. package/docs/components/form/use-cases.md +2 -2
  144. package/docs/components/icon/use-cases.md +1 -1
  145. package/docs/components/input/overview.md +1 -1
  146. package/docs/components/input-amount/api-table.md +9 -7
  147. package/docs/components/input-amount/overview.md +3 -3
  148. package/docs/components/input-amount/use-cases.md +15 -4
  149. package/docs/components/input-amount-dropdown/api-table.md +2 -0
  150. package/docs/components/input-date/overview.md +3 -3
  151. package/docs/components/input-datepicker/api-table.md +63 -45
  152. package/docs/components/input-datepicker/overview.md +5 -5
  153. package/docs/components/input-email/overview.md +2 -2
  154. package/docs/components/input-file/overview.md +1 -1
  155. package/docs/components/input-iban/overview.md +2 -2
  156. package/docs/components/input-range/overview.md +2 -2
  157. package/docs/components/input-stepper/overview.md +1 -1
  158. package/docs/components/input-tel/overview.md +1 -1
  159. package/docs/components/input-tel/use-cases.md +6 -6
  160. package/docs/components/input-tel-dropdown/overview.md +1 -1
  161. package/docs/components/listbox/use-cases.md +1 -1
  162. package/docs/components/menu/api-table.md +44 -0
  163. package/docs/components/menu/applyMenuDemoStyles.js +173 -0
  164. package/docs/components/menu/extensions.md +262 -0
  165. package/docs/components/menu/index.md +13 -0
  166. package/docs/components/menu/migration.md +129 -0
  167. package/docs/components/menu/overview.md +448 -0
  168. package/docs/components/menu/src/DemoNavigationBar.js +441 -0
  169. package/docs/components/menu/src/DemoSpinButton.js +215 -0
  170. package/docs/components/menu/src/DemoToolbar.js +101 -0
  171. package/docs/components/menu/src/DemoTree.js +55 -0
  172. package/docs/components/menu/src/navigation-bar-types.js +41 -0
  173. package/docs/components/menu/use-cases.md +74 -0
  174. package/docs/components/radio-group/overview.md +3 -3
  175. package/docs/components/select/use-cases.md +1 -1
  176. package/docs/components/select-rich/api-table.md +51 -33
  177. package/docs/components/select-rich/use-cases.md +2 -2
  178. package/docs/components/tabs/examples.md +1 -1
  179. package/docs/components/tooltip/api-table.md +33 -7
  180. package/docs/fundamentals/index.md +4 -4
  181. package/docs/fundamentals/node-tools/providence-analytics/overview.md +1 -1
  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 +46 -43
  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/define/lion-item.js +3 -0
  206. package/exports/define/lion-menu.js +3 -0
  207. package/exports/define/lion-menuitem.js +3 -0
  208. package/exports/menu-test-suites.js +4 -0
  209. package/exports/menu.js +3 -0
  210. package/exports/overlays.js +6 -0
  211. package/menu-test-suites.js +4 -0
  212. package/menu.js +3 -0
  213. package/overlays.js +6 -0
  214. package/package.json +11 -4
  215. package/vendor/README.md +61 -0
  216. package/vendor/awesome-phonenumber-7.9.0-tlouisse.libphonenumber-9.0.41.tgz +0 -0
  217. package/vscode.html-custom-data.json +76 -8
  218. package/web-types.json +202 -17
@@ -0,0 +1,295 @@
1
+ // eslint-disable-next-line import/no-extraneous-dependencies
2
+ import { css } from 'lit';
3
+
4
+ /**
5
+ * @typedef {import('../types/MoreButtonMenuMixinTypes.js').MoreButtonMenuMixin} MoreButtonMenuMixinType
6
+ * @typedef {import('../types/MoreButtonMenuMixinTypes.js').MoreButtonMenuHost} MoreButtonMenuHostType
7
+ */
8
+
9
+ const styles = css`
10
+ /** More button is hidden by default */
11
+ [data-more-button-wrapper] {
12
+ display: none;
13
+ }
14
+
15
+ [data-more-button-menu][data-open]:not(:has([role='listitem'] [aria-expanded='true'])) {
16
+ position: absolute;
17
+ overflow: visible;
18
+ height: auto;
19
+ width: auto;
20
+ top: 100%;
21
+ }
22
+
23
+ /** N.B. this is a simplified version of sr-only. when we move to overlayctrl, we should build this into overlayctrl */
24
+ [data-more-button-menu] {
25
+ overflow: hidden;
26
+ height: 0;
27
+ width: 0;
28
+ }
29
+
30
+ [role='list']:has(> [data-more-button-wrapper]) > [role='listitem'] > :is(button, a) {
31
+ white-space: nowrap;
32
+ }
33
+ `;
34
+
35
+ // @ts-ignore - JS mixin typing
36
+ /**
37
+ * @type {MoreButtonMenuMixinType}
38
+ * @param {import('@open-wc/dedupe-mixin').Constructor<import('lit').LitElement>} superclass
39
+ */
40
+ // prettier-ignore
41
+ export const MoreButtonMenuMixin = /** @type {MoreButtonMenuMixinType} */ (superclass =>
42
+ class MoreButtonMenuMixinClass extends /** @type {import('@open-wc/dedupe-mixin').Constructor<MoreButtonMenuHostType>} */ (/** @type {any} */ (superclass)) {
43
+ /** @param {...any} args */
44
+ constructor(...args) {
45
+ super(...args);
46
+ this.__resizeTimeout = null;
47
+ this.isMoreButtonShown = null;
48
+ this.handleResize = this.handleResize.bind(this);
49
+ }
50
+
51
+ disconnectedCallback() {
52
+ window.removeEventListener('resize', this.handleResize);
53
+ if (this.__resizeTimeout) {
54
+ clearTimeout(this.__resizeTimeout);
55
+ }
56
+ super.disconnectedCallback?.();
57
+ }
58
+
59
+ _initMoreButtonMenu() {
60
+ this._createMoreButtonWrapper();
61
+ window.addEventListener('resize', this.handleResize);
62
+ this.handleResize();
63
+ }
64
+
65
+ // eslint-disable-next-line class-methods-use-this
66
+ _getDeepActiveElement() {
67
+ let host = document.activeElement || document.body;
68
+ while (host?.shadowRoot?.activeElement) {
69
+ host = host.shadowRoot.activeElement;
70
+ }
71
+ return host;
72
+ }
73
+
74
+ _createMoreButtonWrapper() {
75
+ const moreButtonWrapper = document.createElement('div');
76
+ const style = document.createElement('style');
77
+ style.textContent = styles.cssText;
78
+ moreButtonWrapper.appendChild(style);
79
+ moreButtonWrapper.setAttribute('data-more-button-wrapper', '');
80
+ [...(this.getMoreButtonSlotProjection()?.childNodes ?? [])].forEach(node =>
81
+ moreButtonWrapper.appendChild(node),
82
+ );
83
+ const moreButton = moreButtonWrapper.querySelector('button');
84
+ moreButton?.setAttribute('data-more-button', '');
85
+ moreButton?.setAttribute('tabindex', '-1');
86
+ moreButton?.setAttribute('aria-expanded', 'false');
87
+ const moreButtonMenu = document.createElement('div');
88
+ moreButtonMenu.setAttribute('data-more-button-menu', '');
89
+ moreButtonMenu.setAttribute('tabindex', '-1');
90
+ moreButtonWrapper.appendChild(moreButtonMenu);
91
+ this._listNode.appendChild(moreButtonWrapper);
92
+
93
+ const isElementDirectFocusableItemUnderMoreButtonMenu = (/** @type {HTMLElement} */ element) =>
94
+ (this.listItems.includes(element) && moreButtonMenu.contains(element)) ||
95
+ element === moreButtonMenu;
96
+
97
+ moreButtonMenu.addEventListener(
98
+ 'focusin',
99
+ (/** @type {FocusEvent} */ event) => {
100
+ const target = /** @type {HTMLElement} */ (event?.target);
101
+ if (!isElementDirectFocusableItemUnderMoreButtonMenu(target)) return;
102
+
103
+ // If applicable hide any other L2 menus
104
+ /** @type {any} */ (this.querySelector('[level="2"]:has([data-open])'))?._overlayCtrl?.hide();
105
+
106
+ // A user did `Shift + Tab` from L2 menu to More menu
107
+ if (target?.getAttribute('aria-expanded') === 'true') {
108
+ // close L2 menu
109
+ target?.click();
110
+ }
111
+ moreButton?.setAttribute('aria-expanded', 'true');
112
+ moreButtonMenu.setAttribute('data-open', '');
113
+ },
114
+ );
115
+
116
+ moreButtonMenu.addEventListener('focusout', () => {
117
+ // We need this timeout for Safari/WebKit.
118
+ // Unfortunately, an in progress click gets canceled when overflow:hidden is applied.
119
+ // Although mousedown would work instead, the consuming developer should have the freedom to apply all possible interaction pattterns.
120
+ // Especially, we should be compatible with 'withClickInteraction'
121
+ setTimeout(() => {
122
+ if (isElementDirectFocusableItemUnderMoreButtonMenu(/** @type {HTMLElement} */ (this._getDeepActiveElement()))) return;
123
+ moreButton?.setAttribute('aria-expanded', 'false');
124
+
125
+ moreButtonMenu.removeAttribute('data-open');
126
+ }, 100);
127
+ });
128
+
129
+ moreButton?.addEventListener('click', () => {
130
+ if (moreButtonMenu.getAttribute('data-open') === null) {
131
+ moreButtonMenu?.focus({ preventScroll: true });
132
+ moreButtonMenu.setAttribute('data-open', '');
133
+ }
134
+ });
135
+ }
136
+
137
+ getMoreButtonSlotProjection() {
138
+ return this.querySelector('[slot="more-button"]');
139
+ }
140
+
141
+ getMoreButtonMenu() {
142
+ return this.querySelector('[data-more-button-menu]');
143
+ }
144
+
145
+ getMoreButtonMenuWrapper() {
146
+ return this.querySelector('[data-more-button-wrapper]');
147
+ }
148
+
149
+ displayMoreButton() {
150
+ const wrapper = /** @type {HTMLElement} */ (this.getMoreButtonMenuWrapper());
151
+ if (!wrapper) return;
152
+ wrapper.style.display = 'block';
153
+ this.isMoreButtonShown = true;
154
+ }
155
+
156
+ hideMoreButton() {
157
+ const wrapper = /** @type {HTMLElement} */ (this.getMoreButtonMenuWrapper());
158
+ if (!wrapper) return;
159
+ wrapper.style.display = 'none';
160
+ this.isMoreButtonShown = false;
161
+ }
162
+
163
+ doItemsFit() {
164
+ this.style.display = 'none';
165
+ const parentWidth = /** @type {HTMLElement} */ (this.parentElement).clientWidth;
166
+ this.style.display = '';
167
+ return (
168
+ this._listNode.scrollWidth === this._listNode.clientWidth &&
169
+ this._listNode.scrollWidth <= parentWidth
170
+ );
171
+ }
172
+
173
+ getListItems() {
174
+ const listNode = this._listNode;
175
+ return Array.from(listNode.querySelectorAll(':scope > [role="listitem"]'));
176
+ }
177
+
178
+ /**
179
+ * @param {Node} listItem
180
+ */
181
+ getListItemWithAdjacentNodes = listItem => {
182
+ const markerNodeType = this.nodeType && Node.COMMENT_NODE;
183
+ /** @param {Node | null | undefined} node */
184
+ const isMarkerComment = node => node?.nodeType === markerNodeType && node?.nodeValue === '';
185
+
186
+ const previousNodes = [];
187
+ let { previousSibling } = listItem;
188
+ while (previousSibling) {
189
+ if (previousSibling.nodeType === Node.ELEMENT_NODE) {
190
+ break;
191
+ }
192
+ previousNodes.unshift(previousSibling);
193
+ if (isMarkerComment(previousSibling)) {
194
+ break;
195
+ }
196
+ ({ previousSibling } = previousSibling);
197
+ }
198
+
199
+ const nextNodes = [];
200
+ let { nextSibling } = listItem;
201
+ while (nextSibling) {
202
+ if (nextSibling.nodeType === Node.ELEMENT_NODE) {
203
+ break;
204
+ }
205
+ nextNodes.push(nextSibling);
206
+
207
+ if (isMarkerComment(nextSibling)) {
208
+ break;
209
+ }
210
+
211
+ ({ nextSibling } = nextSibling);
212
+ }
213
+
214
+ const fragment = document.createDocumentFragment();
215
+ const nodes = [...previousNodes, listItem, ...nextNodes];
216
+ nodes.forEach(node => fragment.appendChild(node));
217
+ return fragment;
218
+ };
219
+
220
+ /**
221
+ * @param {{ moreButtonMenuElement: HTMLButtonElement, listItem: HTMLElement }} opts
222
+ */
223
+ moveItemToMoreButtonMenuFromMainMenu({ moreButtonMenuElement, listItem }) {
224
+ const fragment = this.getListItemWithAdjacentNodes(listItem);
225
+ moreButtonMenuElement.prepend(fragment);
226
+ // eslint-disable-next-line no-param-reassign
227
+ listItem.style.display = '';
228
+ this.displayMoreButton();
229
+ }
230
+
231
+ moveAllItemsToMainMenuFromMoreButtonMenu() {
232
+ const moreButtonMenu = this.getMoreButtonMenu();
233
+ const moreButtonMenuWrapper = this.getMoreButtonMenuWrapper();
234
+ if (!moreButtonMenu || !moreButtonMenuWrapper) return;
235
+ if (moreButtonMenu.childNodes.length === 0) {
236
+ return;
237
+ }
238
+ [...moreButtonMenu.childNodes].forEach(node => {
239
+ moreButtonMenuWrapper.before(node);
240
+ });
241
+ }
242
+
243
+ renderAllItemsInMainMenu() {
244
+ this.moveAllItemsToMainMenuFromMoreButtonMenu();
245
+ }
246
+
247
+ hideItemsOneByOneInMainMenuUntilTheyFit() {
248
+ let hiddenItemsCount = 0;
249
+ const mainMenuItems = this.getListItems();
250
+ for (let i = mainMenuItems.length - 1; i >= 0 && !this.doItemsFit(); i -= 1) {
251
+ /** @type {HTMLElement} */ (mainMenuItems[i]).style.display = 'none';
252
+ hiddenItemsCount += 1;
253
+ }
254
+ return hiddenItemsCount;
255
+ }
256
+
257
+ /**
258
+ * @param {number} hiddenItemsCount
259
+ */
260
+ moveHiddenItemsFromMainMenuToMoreButtonMenu(hiddenItemsCount) {
261
+ const moreButtonMenuElement = this.getMoreButtonMenu();
262
+ const listItems = this._listNode.querySelectorAll(':scope > [role="listitem"]');
263
+ for (let i = 0; i < hiddenItemsCount; i += 1) {
264
+ const listItem = listItems[listItems.length - 1 - i];
265
+ // @ts-ignore - the list items of a menu host are elements
266
+ this.moveItemToMoreButtonMenuFromMainMenu({ moreButtonMenuElement, listItem });
267
+ }
268
+ }
269
+
270
+ handleResize() {
271
+ if (this.__resizeTimeout) {
272
+ return;
273
+ }
274
+
275
+ this.style.visibility = 'hidden';
276
+
277
+ this.__resizeTimeout = setTimeout(() => {
278
+ this.__resizeTimeout = null;
279
+
280
+ this.renderAllItemsInMainMenu();
281
+ this.hideMoreButton();
282
+
283
+ if (this.doItemsFit()) {
284
+ this.style.visibility = '';
285
+ return;
286
+ }
287
+
288
+ this.style.visibility = 'hidden';
289
+ this.displayMoreButton();
290
+ const hiddenItemsCount = this.hideItemsOneByOneInMainMenuUntilTheyFit();
291
+ this.moveHiddenItemsFromMainMenuToMoreButtonMenu(hiddenItemsCount);
292
+ this.style.visibility = '';
293
+ }, 50);
294
+ }
295
+ });
@@ -0,0 +1,312 @@
1
+ /* eslint-disable import/no-extraneous-dependencies */
2
+ import { html } from 'lit';
3
+ import { dedupeMixin } from '@open-wc/dedupe-mixin';
4
+
5
+ /**
6
+ * @typedef {import('../types/MultiLevelListMixinTypes.js').MultiLevelListMixin} MultiLevelListMixinType
7
+ */
8
+ // import { DisclosureMixin } from '@lion/ui/collapsible.js';
9
+ import { InteractiveListMixin } from './InteractiveListMixin.js';
10
+ import { isChecked, setChecked } from './utils/listItemInteractions.js';
11
+ /**
12
+ * @typedef {import('./InteractiveListMixin.js').LionItem} LionItem
13
+ * @typedef {import('../types/InteractiveListMixinTypes.js').InteractiveListHost} InteractiveList
14
+ * @typedef {typeof import('../../overlays/types/OverlayMixinTypes.js').OverlayHost} OverlayHost
15
+ * @typedef {import('./LionMenu.js').LionMenu} LionMenu
16
+ */
17
+
18
+ /**
19
+ * All logic that is needed for interactive lists that are allowed to have multiple nested, collapsible levels
20
+ * Applies to [role=menu] and [role=tree]
21
+ */
22
+ // @ts-ignore - mixin implementation
23
+ /**
24
+ * @type {MultiLevelListMixinType}
25
+ * @param {import('@open-wc/dedupe-mixin').Constructor<import('lit').LitElement>} superclass
26
+ */
27
+ // prettier-ignore
28
+ const MultiLevelListMixinImplementation = /** @type {MultiLevelListMixinType} */ (superclass =>
29
+ class MultiLevelListMixin extends InteractiveListMixin(superclass) {
30
+ render() {
31
+ return html`
32
+ <slot name="invoker"></slot>
33
+ <slot name="list"></slot>
34
+ <slot id="list-items-outlet"></slot>
35
+ `;
36
+ }
37
+
38
+ static get properties() {
39
+ return {
40
+ invokerNode: { type: Object },
41
+ behaveAsAccordion: { type: Boolean, attribute: 'behave-as-accordion' },
42
+ level: { type: Number, reflect: true },
43
+ };
44
+ }
45
+
46
+ /**
47
+ * @configure DisclosureMixin
48
+ */
49
+ get _invokerNode() {
50
+ const ctor = /** @type {OverlayHost} */ (this.constructor);
51
+ if (ctor._getFocusableInvokerEl && this.invokerNode) {
52
+ return /** @type {HTMLElement} */ (ctor._getFocusableInvokerEl(this.invokerNode));
53
+ }
54
+ // @ts-ignore
55
+ return super._invokerNode;
56
+ }
57
+
58
+ /**
59
+ * @configure DisclosureMixin
60
+ * This will be compatible with DisclosureMixin and OverlayMixin. It contains the invoked
61
+ * InteractiveList widget (like menu|tree).
62
+ */
63
+ get _contentNode() {
64
+ return this._listNode;
65
+ }
66
+
67
+ constructor() {
68
+ super();
69
+
70
+ /**
71
+ * When an invokerNode is supplied (usually by a parent level menu), it will take precedence
72
+ * over [slot=invoker] and previousElementSibling with [data-invoker] attribute
73
+ * @type {HTMLElement}
74
+ */
75
+ // @ts-ignore - can be undefined initially
76
+ this.invokerNode = undefined;
77
+ /**
78
+ * @configure DisclosureMixin
79
+ */
80
+ this.handleFocus = true;
81
+ /**
82
+ * The parent list if level > 1
83
+ * @type {InteractiveList}
84
+ */
85
+ // @ts-ignore - can be undefined initially
86
+ this.parentList = undefined;
87
+ /**
88
+ * When true, will have a maximum of one submenu open at a time
89
+ */
90
+ this.behaveAsAccordion = false;
91
+ /**
92
+ * The level of nested menus. Will be reflected as attribute for styling purposes
93
+ */
94
+ this.level = 1;
95
+ /**
96
+ * @configure InteractiveListMixin
97
+ */
98
+ this.hasNoDefaultSelected = true;
99
+ /**
100
+ * @configure InteractiveListMixin
101
+ * @type {'activedescendant'|'roving-tabindex'|'tabbable-disclosure'}
102
+ */
103
+ this._activeMode = 'roving-tabindex';
104
+ /**
105
+ * @type {Map<HTMLElement|LionItem,InteractiveList>}
106
+ */
107
+ this._subListMap = new Map();
108
+ }
109
+
110
+ // /**
111
+ // * @param {import('lit-element').PropertyValues } changedProperties
112
+ // */
113
+ // updated(changedProperties) {
114
+ // super.updated(changedProperties);
115
+
116
+ // if (changedProperties.has('invokerNode')) {
117
+ // // TODO: update openableCtrl invokerNode once we make this a ctrl as well
118
+ // }
119
+ // }
120
+
121
+ /**
122
+ * Gets the child InteractiveList, based on item
123
+ * @param {LionItem|HTMLElement} item can be sibling of or parent of InteractiveList
124
+ */
125
+ static _getSubInteractiveList(item) {
126
+ /**
127
+ * Note that, according to W3C spec, the sub level element with [role=menu|menubar|tree]
128
+ * needs to be a sibling of the invoker element with
129
+ * [role=menuitem|menuitemchecbox|menuitemradio|treeitem] (and not the parent)
130
+ *
131
+ * In the example below <lion-menu> is considered as item
132
+ * @example
133
+ * <lion-menuitem>
134
+ * <button slot="invoker"></button>
135
+ * <lion-menu> ... </lion-menu>
136
+ * </lion-menuitem>
137
+ */
138
+ // @ts-ignore - isInteractiveList property check
139
+ const siblingOfInvoker = item.nextElementSibling?.isInteractiveList;
140
+ if (siblingOfInvoker) {
141
+ return item.nextElementSibling;
142
+ }
143
+ /**
144
+ * In the example below div[role=listitem] | [data-item] is considered as item
145
+ * @example
146
+ * <div role="listitem" data-item>
147
+ * <button slot="invoker"></button>
148
+ * <lion-menu> ... </lion-menu>
149
+ * </div>
150
+ */
151
+ const childOfInvoker =
152
+ item.getAttribute?.('role') === 'listitem' || item.hasAttribute?.('data-item');
153
+ if (childOfInvoker) {
154
+ // @ts-ignore - isInteractiveList check
155
+ return Array.from(item.children).find(child => child.isInteractiveList);
156
+ }
157
+ return undefined;
158
+ }
159
+
160
+ /**
161
+ * @param {*} newItems
162
+ */
163
+ _initListItems(newItems) {
164
+ super._initListItems(newItems);
165
+
166
+ const ctor = this.constructor;
167
+ // @ts-ignore - forEach parameter types
168
+ newItems.forEach(item => {
169
+ // @ts-ignore - static method access
170
+ const subList = ctor._getSubInteractiveList(item);
171
+ if (subList) {
172
+ this._subListMap.set(item, subList);
173
+ subList.level = this.level + 1;
174
+ subList.parentList = this;
175
+ subList.invokerNode = item;
176
+ // @ts-ignore - invokerInteraction property
177
+ subList.invokerInteraction = this.invokerInteraction;
178
+ subList._activeMode = this._activeMode;
179
+ }
180
+ });
181
+ }
182
+
183
+ /**
184
+ * @enhance DisclosureMixin
185
+ * @param {Event} [ev]
186
+ */
187
+ toggle(ev) {
188
+ // @ts-ignore - target property
189
+ if (ev && this._listNode.contains(ev.target)) {
190
+ // prevent nested menus (inside invokers) from triggering invoker
191
+ return;
192
+ }
193
+ // @ts-ignore - super.toggle method
194
+ super.toggle();
195
+ }
196
+
197
+ /**
198
+ * @enhance InteractiveListMixin
199
+ * @param {*} ev
200
+ */
201
+ _onListKeyDown(ev) {
202
+ const targetFromSubList = Array.from(this._subListMap).find(
203
+ // @ts-ignore - contains method on InteractiveListHost
204
+ ([, s]) => s === ev.target || s.contains(ev.target),
205
+ );
206
+
207
+ if (targetFromSubList) {
208
+ return;
209
+ }
210
+
211
+ super._onListKeyDown(ev);
212
+
213
+ const subListOfActiveItem = this._subListMap.get(this.activeItem);
214
+ const parentListOfActiveItem = this.parentList;
215
+ const { key } = ev;
216
+
217
+ if (key === 'Escape' && parentListOfActiveItem) {
218
+ // We need to stop here, or else we affect parent menu (handled by OverlayController)
219
+ ev.stopPropagation();
220
+ }
221
+
222
+ switch (key) {
223
+ case 'Enter':
224
+ case ' ':
225
+ // make it work like a button
226
+ this.activeItem?.click();
227
+ break;
228
+ case 'ArrowDown':
229
+ if (
230
+ this.axis === 'inline' &&
231
+ subListOfActiveItem &&
232
+ this._activeMode !== 'tabbable-disclosure'
233
+ ) {
234
+ this.activeItem.click();
235
+ }
236
+ break;
237
+ case 'ArrowRight':
238
+ if (this.axis === 'block' && subListOfActiveItem) {
239
+ this.activeItem.click();
240
+ }
241
+ break;
242
+ case 'ArrowUp':
243
+ if (this.axis === 'inline' && parentListOfActiveItem) {
244
+ // @ts-ignore - close method
245
+ this.close();
246
+ if (/** @type {InteractiveList & LionMenu} */ (parentListOfActiveItem)._onOverlayShow) {
247
+ // @ts-ignore - _onOverlayShow method
248
+ /** @type {OverlayHost} */ (parentListOfActiveItem)._onOverlayShow();
249
+ }
250
+ }
251
+ break;
252
+ case 'ArrowLeft':
253
+ if (this.axis === 'block' && parentListOfActiveItem) {
254
+ // @ts-ignore - close method
255
+ this.close();
256
+ if (/** @type {InteractiveList & LionMenu} */ (parentListOfActiveItem)._onOverlayShow) {
257
+ // @ts-ignore - _onOverlayShow method
258
+ /** @type {OverlayHost} */ (parentListOfActiveItem)._onOverlayShow();
259
+ }
260
+ }
261
+ break;
262
+ /* no default */
263
+ }
264
+ }
265
+
266
+ /**
267
+ * @param {Location} location
268
+ */
269
+ _syncCurrentPageWithLocationHref(location) {
270
+ super._syncCurrentPageWithLocationHref(location);
271
+
272
+ this._subListMap.forEach(subList => {
273
+ subList.listItems.forEach(
274
+ /** @param {LionItem|HTMLElement} item */ item => {
275
+ // @ts-ignore
276
+ if (isChecked(item) && subList._invokerNode) {
277
+ // @ts-ignore
278
+ setChecked(subList._invokerNode);
279
+ }
280
+ },
281
+ );
282
+ });
283
+ }
284
+
285
+ // /**
286
+ // * @enhance DisclosureMixin
287
+ // */
288
+ // _setupDisclosure() {
289
+ // if (!this._invokerNode) return;
290
+
291
+ // super._setupDisclosure();
292
+ // }
293
+
294
+ // /**
295
+ // * @enhance DisclosureMixin
296
+ // */
297
+ // _teardownDisclosure() {
298
+ // if (!this._invokerNode) return;
299
+
300
+ // super._teardownDisclosure();
301
+ // }
302
+
303
+ // /**
304
+ // * @enhance DisclosureMixin
305
+ // */
306
+ // async _onOpenedChanged() {
307
+ // if (!this._invokerNode) return;
308
+
309
+ // await super._onOpenedChanged();
310
+ // }
311
+ });
312
+ 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
+ }