@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
@@ -1,6 +1,5 @@
1
1
  import { dedupeMixin } from '@open-wc/dedupe-mixin';
2
2
  import { OverlayController } from './OverlayController.js';
3
- import { isEqualConfig } from './utils/is-equal-config.js';
4
3
 
5
4
  /**
6
5
  * @typedef {<T>(ingOverlayHost:T) => void} PostProcessor
@@ -28,14 +27,12 @@ export function _addOverlayMixinPostProcessor(postProcessor) {
28
27
  * @type {OverlayMixin}
29
28
  * @param {import('@open-wc/dedupe-mixin').Constructor<import('lit').LitElement>} superclass
30
29
  */
30
+ // @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
31
31
  export const OverlayMixinImplementation = superclass => {
32
32
  class OverlayMixin extends superclass {
33
33
  static get properties() {
34
34
  return {
35
- opened: {
36
- type: Boolean,
37
- reflect: true,
38
- },
35
+ opened: { type: Boolean, reflect: true },
39
36
  };
40
37
  }
41
38
 
@@ -69,13 +66,13 @@ export const OverlayMixinImplementation = superclass => {
69
66
 
70
67
  /** @param {OverlayConfig} value */
71
68
  set config(value) {
72
- const shouldUpdate = !isEqualConfig(this.config, value);
73
-
74
- if (this._overlayCtrl && shouldUpdate) {
69
+ // The OverlayController already guards against redundant updates
70
+ // (see OverlayController#updateConfig), so we delegate unconditionally here.
71
+ if (this._overlayCtrl) {
75
72
  this._overlayCtrl.updateConfig(value);
76
73
  }
77
74
  this.__config = value;
78
- if (this._overlayCtrl && shouldUpdate) {
75
+ if (this._overlayCtrl) {
79
76
  this.__syncToOverlayController();
80
77
  }
81
78
  }
@@ -160,6 +157,8 @@ export const OverlayMixinImplementation = superclass => {
160
157
  */
161
158
  // eslint-disable-next-line class-methods-use-this
162
159
  _setupOpenCloseListeners() {
160
+ // Keeps the close-overlay event private for the controller.
161
+ // (we already have )
163
162
  /**
164
163
  * @param {{ stopPropagation: () => void; }} ev
165
164
  */
@@ -217,12 +216,6 @@ export const OverlayMixinImplementation = superclass => {
217
216
  // @ts-expect-error
218
217
  static enabledWarnings = super.enabledWarnings?.filter(w => w !== 'change-in-update') || [];
219
218
 
220
- get _overlayInvokerNode() {
221
- return /** @type {HTMLElement | undefined} */ (
222
- Array.from(this.children).find(child => child.slot === 'invoker')
223
- );
224
- }
225
-
226
219
  /**
227
220
  * @overridable
228
221
  */
@@ -255,13 +248,81 @@ export const OverlayMixinImplementation = superclass => {
255
248
  );
256
249
  }
257
250
 
251
+ /**
252
+ * Returns the element that can actually receive focus inside the passed
253
+ * invoker (wrapper) element: the element itself when it is focusable,
254
+ * otherwise the first focusable descendant.
255
+ * @param {Element|null|undefined} focusableElOrWrapper
256
+ * @returns {Element|null}
257
+ */
258
+ /**
259
+ * @param {Element | null | undefined} focusableElOrWrapper
260
+ * @returns {Element | null}
261
+ */
262
+ static _getFocusableInvokerEl(focusableElOrWrapper) {
263
+ if (!focusableElOrWrapper) return null;
264
+ const focusableSelector = '[tabindex], button, a[href], [role=button]';
265
+ return focusableElOrWrapper.matches(focusableSelector)
266
+ ? focusableElOrWrapper
267
+ : focusableElOrWrapper.querySelector(focusableSelector);
268
+ }
269
+
270
+ /**
271
+ * The node that opens/closes this overlay.
272
+ *
273
+ * Resolved in a stable, declarative way (independent of the exact DOM order)
274
+ * by looking, in order, for:
275
+ * 1. a child with `[slot="invoker"]`
276
+ * 2. any element in the same root that opts in via `[data-invoker]` and
277
+ * references this host by id (`for="<id>"`)
278
+ * 3. a preceding sibling that opts in via `[data-invoker]`
279
+ * @protected
280
+ */
281
+ get _overlayInvokerNode() {
282
+ if (!this.__invokerNode) {
283
+ this.__invokerNode = this._getInvokerNode();
284
+ }
285
+ return this.__invokerNode;
286
+ }
287
+
288
+ /**
289
+ * @protected
290
+ * @returns {Element|null}
291
+ */
292
+ _getInvokerNode() {
293
+ const ctor = /** @type {typeof OverlayMixin} */ (this.constructor);
294
+
295
+ const slottedNode = Array.from(this.children).find(child => child.slot === 'invoker');
296
+ if (slottedNode) {
297
+ return ctor._getFocusableInvokerEl(slottedNode);
298
+ }
299
+
300
+ if (this.id) {
301
+ // Reference the invoker declaratively: `<button data-invoker for="my-menu">`
302
+ const root = /** @type {Document|ShadowRoot} */ (this.getRootNode());
303
+ const explicitInvoker = root.querySelector?.(`[data-invoker][for="${this.id}"]`);
304
+ if (explicitInvoker) {
305
+ return ctor._getFocusableInvokerEl(explicitInvoker);
306
+ }
307
+ }
308
+
309
+ // Fall back to a preceding sibling that opted in via [data-invoker]
310
+ const { previousElementSibling } = this;
311
+ return previousElementSibling && previousElementSibling.hasAttribute('data-invoker')
312
+ ? ctor._getFocusableInvokerEl(previousElementSibling)
313
+ : null;
314
+ }
315
+
258
316
  /** @protected */
259
317
  _setupOverlayCtrl() {
260
318
  if (this.#hasSetup) return;
319
+
320
+ const invokerNode = this._overlayInvokerNode;
321
+
261
322
  const config = {
262
323
  contentNode: this._overlayContentNode,
263
324
  contentWrapperNode: this._overlayContentWrapperNode,
264
- invokerNode: this._overlayInvokerNode,
325
+ invokerNode: invokerNode && invokerNode instanceof HTMLElement ? invokerNode : undefined,
265
326
  referenceNode: this._overlayReferenceNode,
266
327
  backdropNode: this._overlayBackdropNode,
267
328
  };
@@ -282,6 +343,8 @@ export const OverlayMixinImplementation = superclass => {
282
343
 
283
344
  /** @protected */
284
345
  _teardownOverlayCtrl() {
346
+ // Make sure that dynamic behavior (e.g. responsive) is possible by allowing multiple setups and teardowns of the overlay controller.
347
+ this.#hasSetup = false;
285
348
  if (!this._overlayCtrl) return;
286
349
 
287
350
  this._teardownOpenCloseListeners();
@@ -434,6 +497,7 @@ export const OverlayMixinImplementation = superclass => {
434
497
  for (const postProcessor of overlayMixinPostProcessors) {
435
498
  postProcessor(OverlayMixin);
436
499
  }
437
- return /** @type {* & OverlayMixin} */ (OverlayMixin);
500
+ // @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
501
+ return OverlayMixin;
438
502
  };
439
503
  export const OverlayMixin = dedupeMixin(OverlayMixinImplementation);
@@ -124,7 +124,7 @@ export function adoptStyle(renderRoot, style, { teardown = false } = {}) {
124
124
  if (!teardown) {
125
125
  // @ts-ignore
126
126
  // eslint-disable-next-line no-param-reassign
127
- renderRoot.adoptedStyleSheets = [...renderRoot.adoptedStyleSheets, sheet];
127
+ renderRoot.adoptedStyleSheets = [...(renderRoot.adoptedStyleSheets || []), sheet];
128
128
  } else if (renderRoot.adoptedStyleSheets.includes(sheet)) {
129
129
  // @ts-ignore
130
130
  // eslint-disable-next-line no-param-reassign
@@ -12,15 +12,24 @@
12
12
  */
13
13
  export function isEqualConfig(a, b) {
14
14
  if (typeof a !== 'object' || typeof b !== 'object' || a === null || b === null) {
15
+ if (typeof a === 'function' && typeof b === 'function') {
16
+ return /** @type {Function} */ (a).toString() === /** @type {Function} */ (b).toString();
17
+ }
15
18
  return a === b;
16
19
  }
17
- const aObj = /** @type {Record<string, any>} */ (a);
18
- const bObj = /** @type {Record<string, any>} */ (b);
19
- const aProps = Object.keys(aObj);
20
- const bProps = Object.keys(bObj);
20
+ if (a instanceof Node && b instanceof Node) {
21
+ return a === b;
22
+ }
23
+
24
+ const aProps = Object.keys(a);
25
+ const bProps = Object.keys(b);
21
26
  if (aProps.length !== bProps.length) {
22
27
  return false;
23
28
  }
24
- const isEqual = /** @param {string} prop */ prop => isEqualConfig(aObj[prop], bObj[prop]);
25
- return aProps.every(isEqual);
29
+ return aProps.every(
30
+ prop =>
31
+ Object.prototype.hasOwnProperty.call(b, prop) &&
32
+ // @ts-ignore - dynamic property access
33
+ isEqualConfig(a[prop], b[prop]),
34
+ );
26
35
  }
@@ -115,26 +115,35 @@ const withLocalTestConfig = () =>
115
115
  */
116
116
  async function createNestedEscControllers(parentContent) {
117
117
  const childContent = /** @type {HTMLDivElement} */ (parentContent.querySelector('div[id]'));
118
- // Assert valid fixure
119
- const isValidFixture =
120
- (parentContent.id.startsWith('parent-overlay--hidesOnEsc') ||
121
- parentContent.id.startsWith('parent-overlay--hidesOnOutsideEsc')) &&
122
- (childContent.id.startsWith('child-overlay--hidesOnEsc') ||
123
- childContent.id.startsWith('child-overlay--hidesOnOutsideEsc'));
124
- if (!isValidFixture) {
125
- throw new Error('Provide a valid fixture');
126
- }
127
118
 
128
119
  if (parentContent.hasAttribute('data-convert-to-shadow-root')) {
129
120
  const shadowRootParent = parentContent.attachShadow({ mode: 'open' });
130
121
  shadowRootParent.appendChild(childContent);
131
122
  }
132
123
 
124
+ const parentConfig = {};
125
+ const childConfig = {};
126
+
133
127
  const parentHasOutsideOnEsc = parentContent.id.startsWith('parent-overlay--hidesOnOutsideEsc');
134
128
  const childHasOutsideOnEsc = childContent.id.startsWith('child-overlay--hidesOnOutsideEsc');
135
129
 
136
- const parentConfig = parentHasOutsideOnEsc ? { hidesOnOutsideEsc: true } : { hidesOnEsc: true };
137
- const childConfig = childHasOutsideOnEsc ? { hidesOnOutsideEsc: true } : { hidesOnEsc: true };
130
+ if (parentHasOutsideOnEsc) {
131
+ parentConfig.hidesOnOutsideEsc = true;
132
+ } else {
133
+ parentConfig.hidesOnEsc = true;
134
+ }
135
+ if (childHasOutsideOnEsc) {
136
+ childConfig.hidesOnOutsideEsc = true;
137
+ } else {
138
+ childConfig.hidesOnEsc = true;
139
+ }
140
+
141
+ const parentHasSyncChildrenCloseState =
142
+ parentContent.dataset.syncChildrenCloseState !== undefined;
143
+
144
+ if (parentHasSyncChildrenCloseState) {
145
+ parentConfig.syncChildrenCloseState = true;
146
+ }
138
147
 
139
148
  const parentOverlay = new OverlayController({
140
149
  ...withGlobalTestConfig(),
@@ -283,8 +292,9 @@ describe('OverlayController', () => {
283
292
  contentNode,
284
293
  });
285
294
  };
295
+
286
296
  expect(createOverlayController).to.throw(
287
- '[OverlayController] Could not find a render target, since the provided contentNode is not connected to the DOM. Make sure that it is connected, e.g. by doing "document.body.appendChild(contentNode)", before passing it on.',
297
+ '[OverlayController] Could not find a render target, makes sure contentNode has a parent element (or contentWrapperNode is connected)',
288
298
  );
289
299
  });
290
300
 
@@ -346,6 +356,21 @@ describe('OverlayController', () => {
346
356
  await ctrl.hide();
347
357
  expect(ctrl.isShown).to.be.false;
348
358
  });
359
+
360
+ it('cleans up the dom structure it created', async () => {
361
+ const contentNode = /** @type {HTMLElement} */ (fixtureSync(html`<div>my content</div>`));
362
+ const ctrl = new OverlayController({
363
+ placementMode: 'global',
364
+ contentNode,
365
+ });
366
+ await ctrl.show();
367
+ expect(ctrl.contentWrapperNode).to.exist;
368
+ expect(ctrl.__wrappingDialogNode).to.exist;
369
+
370
+ await ctrl.teardown();
371
+ expect(ctrl.contentWrapperNode).to.not.exist;
372
+ expect(ctrl.__wrappingDialogNode).to.not.exist;
373
+ });
349
374
  });
350
375
 
351
376
  describe('Node Configuration', () => {
@@ -403,7 +428,7 @@ describe('OverlayController', () => {
403
428
  { ignoreAttributes: ['closedby'] },
404
429
  );
405
430
 
406
- expect(el).lightDom.to.equal(`<div slot="content">projected</div>`);
431
+ expect(el).lightDom.to.equal(`<div data-content="" slot="content">projected</div>`);
407
432
  });
408
433
  });
409
434
 
@@ -425,7 +450,7 @@ describe('OverlayController', () => {
425
450
  `
426
451
  <dialog data-overlay-outer-wrapper="" open="" role="none" style="${wrappingDialogNodeStyle}">
427
452
  <div data-id="content-wrapper">
428
- <div id="content">non projected</div>
453
+ <div data-content="" id="content">non projected</div>
429
454
  </div>
430
455
  </dialog>
431
456
  `,
@@ -434,6 +459,84 @@ describe('OverlayController', () => {
434
459
  });
435
460
  });
436
461
 
462
+ describe('When .contentWrapperNode is provided in a host that is not connected yet', () => {
463
+ /** @param {{ nested?: boolean }} [opts] */
464
+ function createDisconnectedHost({ nested = false } = {}) {
465
+ const host = document.createElement('div');
466
+ const shadowRoot = host.attachShadow({ mode: 'open' });
467
+ const wrapper = document.createElement('div');
468
+ wrapper.setAttribute('data-id', 'content-wrapper');
469
+ wrapper.innerHTML = '<slot name="content"></slot>';
470
+ if (nested) {
471
+ const section = document.createElement('section');
472
+ section.appendChild(wrapper);
473
+ shadowRoot.appendChild(section);
474
+ } else {
475
+ shadowRoot.appendChild(wrapper);
476
+ }
477
+ const content = document.createElement('div');
478
+ content.slot = 'content';
479
+ content.textContent = 'projected';
480
+ host.appendChild(content);
481
+ return { host, shadowRoot, wrapper, content };
482
+ }
483
+
484
+ it('does not throw and keeps the projected slot inside the wrapper', async () => {
485
+ const { host, shadowRoot, wrapper, content } = createDisconnectedHost();
486
+ expect(wrapper.isConnected).to.be.false;
487
+ const slot = /** @type {HTMLSlotElement} */ (content.assignedSlot);
488
+
489
+ const ctrl = new OverlayController({
490
+ ...withGlobalTestConfig(),
491
+ contentNode: content,
492
+ contentWrapperNode: wrapper,
493
+ });
494
+
495
+ expect(ctrl.contentWrapperNode).to.equal(wrapper);
496
+ expect(wrapper.parentElement?.tagName).to.equal('DIALOG');
497
+ expect(slot.parentElement).to.equal(wrapper);
498
+ expect(content.parentElement).to.equal(host);
499
+ expect(shadowRoot.querySelectorAll('slot').length).to.equal(1);
500
+ });
501
+
502
+ it('restores the same relocated slot at its original position on teardown', async () => {
503
+ const { host, shadowRoot, wrapper, content } = createDisconnectedHost({ nested: true });
504
+ const section = /** @type {HTMLElement} */ (shadowRoot.querySelector('section'));
505
+ const slot = /** @type {HTMLSlotElement} */ (content.assignedSlot);
506
+ const ctrl = new OverlayController({
507
+ ...withGlobalTestConfig(),
508
+ contentNode: content,
509
+ contentWrapperNode: wrapper,
510
+ });
511
+
512
+ ctrl.teardown();
513
+
514
+ expect(content.parentElement).to.equal(host);
515
+ expect(wrapper.parentElement).to.equal(section);
516
+ expect(slot.parentElement).to.equal(wrapper);
517
+ expect(shadowRoot.querySelector('dialog')).to.be.null;
518
+ expect(shadowRoot.querySelector('slot')).to.equal(slot);
519
+ });
520
+
521
+ it('survives repeated updateConfig calls and keeps slot ownership', async () => {
522
+ const { host, shadowRoot, wrapper, content } = createDisconnectedHost();
523
+ const slot = /** @type {HTMLSlotElement} */ (content.assignedSlot);
524
+ const ctrl = new OverlayController({
525
+ ...withGlobalTestConfig(),
526
+ contentNode: content,
527
+ contentWrapperNode: wrapper,
528
+ });
529
+
530
+ expect(() => ctrl.updateConfig({ trapsKeyboardFocus: true })).to.not.throw();
531
+ expect(() => ctrl.updateConfig({ trapsKeyboardFocus: false })).to.not.throw();
532
+
533
+ expect(content.parentElement).to.equal(host);
534
+ expect(shadowRoot.querySelectorAll('slot').length).to.equal(1);
535
+ expect(shadowRoot.querySelector('slot')).to.equal(slot);
536
+ expect(wrapper.contains(slot)).to.be.true;
537
+ });
538
+ });
539
+
437
540
  describe('When .contenWrapperNode provided', async () => {
438
541
  it('keeps the .contentWrapperNode for style application and wraps a <dialog role="none"> for top layer paints', async () => {
439
542
  const tagString = defineCE(
@@ -620,6 +723,7 @@ describe('OverlayController', () => {
620
723
  });
621
724
  });
622
725
 
726
+ // TODO: make features modular (so they can be treeshaken) and test their lifecycle within this desrcibe
623
727
  describe('Feature Configuration', () => {
624
728
  describe('trapsKeyboardFocus', () => {
625
729
  it('focuses the overlay on show', async () => {
@@ -854,7 +958,7 @@ describe('OverlayController', () => {
854
958
  )
855
959
  );
856
960
  const { parentOverlay, childOverlay } = await createNestedEscControllers(parentContent);
857
- mimicEscapePress(childOverlay.contentNode);
961
+ await mimicEscapePress(childOverlay.contentNode);
858
962
 
859
963
  expect(parentOverlay.isShown).to.be.true;
860
964
  expect(childOverlay.isShown).to.be.true;
@@ -863,10 +967,7 @@ describe('OverlayController', () => {
863
967
 
864
968
  describe('Nested hidesOnEsc / hidesOnOutsideEsc', () => {
865
969
  describe('Parent has hidesOnEsc and child has hidesOnOutsideEsc', () => {
866
- // TODO: This test is flaky. We need to investigate why the child overlay
867
- // is not staying shown after the parent overlay is hidden. The failing line
868
- // is highlighted below.
869
- it.skip('on [Escape] press in child overlay: parent hides, child stays shown', async () => {
970
+ it('on [Escape] press in child overlay: parent hides, child stays shown', async () => {
870
971
  const parentContent = /** @type {HTMLDivElement} */ (
871
972
  await fixture(
872
973
  html` <!-- -->
@@ -877,12 +978,16 @@ describe('OverlayController', () => {
877
978
  );
878
979
  const { parentOverlay, childOverlay } = await createNestedEscControllers(parentContent);
879
980
  await mimicEscapePress(childOverlay.contentNode);
880
- await waitUntil(() => !parentOverlay.isShown);
881
-
882
- // TODO: This is the failing line ("sometimes").
883
- // The child overlay is not staying shown after the parent overlay is hidden.
884
- await waitUntil(() => childOverlay.isShown);
885
-
981
+ await childOverlay._showComplete;
982
+ await parentOverlay._showComplete;
983
+ await childOverlay._hideComplete;
984
+ await parentOverlay._hideComplete;
985
+ if (!childOverlay.isShown) {
986
+ await waitUntil(() => childOverlay.isShown);
987
+ }
988
+ if (parentOverlay.isShown) {
989
+ await waitUntil(() => !parentOverlay.isShown);
990
+ }
886
991
  await childOverlay.teardown();
887
992
  await parentOverlay.teardown();
888
993
  });
@@ -988,6 +1093,7 @@ describe('OverlayController', () => {
988
1093
  });
989
1094
  });
990
1095
  });
1096
+
991
1097
  describe('hidesOnOutsideClick', () => {
992
1098
  it('hides on outside click', async () => {
993
1099
  const contentNode = /** @type {HTMLElement} */ (await fixture('<div>Content</div>'));
@@ -1708,6 +1814,97 @@ describe('OverlayController', () => {
1708
1814
  expect(ctrl1.hasActiveBackdrop).to.be.true;
1709
1815
  });
1710
1816
  });
1817
+
1818
+ describe('elementToFocusOnShow', () => {
1819
+ it('adds tabindex="-1" to the content node when elementToFocusOnShow is true', async () => {
1820
+ const ctrl = new OverlayController({
1821
+ ...withGlobalTestConfig(),
1822
+ isBlocking: false,
1823
+ elementToFocusOnShow: true,
1824
+ });
1825
+ const contentNode = /** @type {HTMLElement} */ (await fixture('<div>Content</div>'));
1826
+ ctrl.updateConfig({ contentNode });
1827
+ await ctrl.show();
1828
+ expect(contentNode.getAttribute('tabindex')).to.equal('-1');
1829
+ });
1830
+
1831
+ it('makes contentNode the root of "next tab flow"', async () => {
1832
+ const ctrl = new OverlayController({
1833
+ ...withGlobalTestConfig(),
1834
+ isBlocking: false,
1835
+ elementToFocusOnShow: true,
1836
+ });
1837
+ const contentNode = /** @type {HTMLElement} */ (
1838
+ await fixture('<div><button>Button</button></div>')
1839
+ );
1840
+ ctrl.updateConfig({ contentNode });
1841
+ await ctrl.show();
1842
+ const button = /** @type {HTMLButtonElement} */ (contentNode.querySelector('button'));
1843
+ button.focus();
1844
+ expect(isActiveElement(button)).to.be.true;
1845
+ });
1846
+ });
1847
+
1848
+ describe('syncChildrenCloseState', () => {
1849
+ it('does not close all children of nested controllers when syncChildrenCloseState is false', async () => {
1850
+ const parentContent = /** @type {HTMLDivElement} */ (
1851
+ await fixture(
1852
+ html` <!-- -->
1853
+ <div data-sync-children-close-state id="parent-overlay">
1854
+ <div id="child-overlay">we open our child and it will close when parent does</div>
1855
+ </div>`,
1856
+ )
1857
+ );
1858
+ const { parentOverlay, childOverlay } = await createNestedEscControllers(parentContent);
1859
+ expect(parentOverlay.isShown).to.be.true;
1860
+ expect(childOverlay.isShown).to.be.true;
1861
+
1862
+ await parentOverlay.hide();
1863
+ await childOverlay._hideComplete;
1864
+ expect(parentOverlay.isShown).to.be.false;
1865
+ expect(childOverlay.isShown).to.be.false;
1866
+ });
1867
+
1868
+ it('does not close all children of nested controllers when syncChildrenCloseState is false', async () => {
1869
+ const parentContent = /** @type {HTMLDivElement} */ (
1870
+ await fixture(
1871
+ html` <!-- -->
1872
+ <div id="parent-overlay">
1873
+ <div id="child-overlay">
1874
+ we open our child and it will not close when parent does
1875
+ </div>
1876
+ </div>`,
1877
+ )
1878
+ );
1879
+ const { parentOverlay, childOverlay } = await createNestedEscControllers(parentContent);
1880
+ expect(parentOverlay.isShown).to.be.true;
1881
+ expect(childOverlay.isShown).to.be.true;
1882
+
1883
+ await parentOverlay.hide();
1884
+ expect(parentOverlay.isShown).to.be.false;
1885
+ expect(childOverlay.isShown).to.be.true;
1886
+ });
1887
+ });
1888
+
1889
+ describe('isActivated', () => {
1890
+ it('allows to conditionally disable overlay/openable functionality (for responsive context like nav menus, accordions/collapsibles vs plain headings/content etc.)', async () => {
1891
+ const ctrl = new OverlayController({
1892
+ ...withGlobalTestConfig(),
1893
+ isActivated: false,
1894
+ });
1895
+ expect(ctrl.__hasSetup).to.be.false;
1896
+ ctrl.updateConfig({
1897
+ isActivated: true,
1898
+ });
1899
+ expect(ctrl.__hasSetup).to.be.true;
1900
+ });
1901
+ });
1902
+
1903
+ // TODO
1904
+ describe('hideVisually', () => {});
1905
+
1906
+ // TODO
1907
+ describe('requireConnectedNodes', () => {});
1711
1908
  });
1712
1909
 
1713
1910
  describe('Show / Hide / Toggle', () => {
@@ -2298,6 +2495,7 @@ describe('OverlayController', () => {
2298
2495
  });
2299
2496
 
2300
2497
  it('should run with scroll prevention', async () => {
2498
+ await overlayControllerPreventsScroll.hide();
2301
2499
  await overlayControllerPreventsScroll.show();
2302
2500
 
2303
2501
  expect(
@@ -540,13 +540,13 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
540
540
  it('works with cache directive when reconnected', async () => {
541
541
  class CachingContext extends LitElement {
542
542
  static properties = {
543
- switched: { type: Boolean },
543
+ showOverlay: { type: Boolean },
544
544
  };
545
545
 
546
546
  constructor() {
547
547
  super();
548
548
 
549
- this.switched = false;
549
+ this.showOverlay = true;
550
550
  }
551
551
 
552
552
  get overlayEl() {
@@ -555,12 +555,12 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
555
555
 
556
556
  render() {
557
557
  return html`${cache(
558
- this.switched
559
- ? html`something else`
560
- : html`<${tag} id="myOverlay">
558
+ this.showOverlay
559
+ ? html`<${tag} id="myOverlay">
561
560
  <div slot="content">content b</div>
562
561
  <button slot="invoker">invoker button b</button>
563
- </${tag}>`,
562
+ </${tag}>`
563
+ : html`something else`,
564
564
  )}`;
565
565
  }
566
566
  }
@@ -581,7 +581,7 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
581
581
  // @ts-expect-error [allow-protected] in tests
582
582
  const setupSpy = sinon.spy(el.overlayEl, '_setupOverlayCtrl');
583
583
 
584
- el.switched = true;
584
+ el.showOverlay = false;
585
585
  // render the new content
586
586
  await el.updateComplete;
587
587
  // wait for the teardown to complete
@@ -591,7 +591,7 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
591
591
  expect(teardownSpy.callCount).to.equal(1);
592
592
  expect(setupSpy.callCount).to.equal(0);
593
593
 
594
- el.switched = false;
594
+ el.showOverlay = true;
595
595
  await el.updateComplete;
596
596
  expect(setupSpy.callCount).to.equal(1);
597
597
 
@@ -26,9 +26,7 @@ export declare class ArrowHost {
26
26
 
27
27
  protected _defineOverlayConfig(): OverlayConfig;
28
28
 
29
- protected _getPopperArrowConfig(
30
- popperConfigToExtendFrom: Partial<PopperOptions>,
31
- ): Partial<PopperOptions>;
29
+ protected _getPopperArrowConfig(popperConfigToExtendFrom: Partial<PopperOptions>): Partial<PopperOptions>;
32
30
 
33
31
  private __setupRepositionCompletePromise(): void;
34
32
 
@@ -21,7 +21,6 @@ export type OverlayPhase =
21
21
  | 'add'
22
22
  | 'remove';
23
23
 
24
-
25
24
  export interface ViewportConfig {
26
25
  placement: ViewportPlacement;
27
26
  }
@@ -97,14 +96,30 @@ export interface OverlayConfig {
97
96
  /** By default, the tooltip content is a 'description' for the invoker (uses aria-describedby) Setting this property to 'label' makes the content function as a label (via aria-labelledby) */
98
97
  invokerRelation?: 'label' | 'description';
99
98
 
100
- /** render a div instead of dialog */
101
- _noDialogEl?: Boolean;
102
-
103
99
  /**
104
100
  * Determines the conditions hiding/showing should be based on. It gets the OverlayController as input and returns an object with
105
101
  * functions with Overlay phases as keys
106
102
  */
107
103
  visibilityTriggerFunction?: Function;
108
- }
109
104
 
105
+ /**
106
+ * Whether the overlay content should receive focus when shown.
107
+ */
108
+ elementToFocusOnShow?: boolean;
109
+
110
+ /*
111
+ * By default, we function either as overlay or disclosure. In some cases, this should be dynamically set (think of responsive changes).
112
+ * or it depends on the context (a top level menu is not openable (unless it's a context menu), its children menus are).
113
+ */
114
+ isActivated?: boolean;
115
+
116
+ /**
117
+ * By default, an error is thrown when contentNode is not connected to the DOM.
118
+ * This allows to prepare nodes and later connect them to dom.
119
+ */
120
+ requireConnectedNodes?: boolean;
121
+
122
+ syncChildrenCloseState?: boolean;
110
123
 
124
+ _shouldTeardownDomStructure?: boolean;
125
+ }
@@ -17,6 +17,8 @@ export interface DefineOverlayConfig {
17
17
  }
18
18
 
19
19
  export declare class OverlayHost {
20
+ static _getFocusableInvokerEl(focusableElOrWrapper: Element | null | undefined): Element | null;
21
+
20
22
  opened: Boolean;
21
23
  get config(): Partial<OverlayConfig>;
22
24
  set config(value: Partial<OverlayConfig>);