@lion/ui 0.16.1 → 0.17.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 (250) hide show
  1. package/collapsible.js +1 -0
  2. package/components/button/src/LionButton.js +5 -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/src/utils/makeMatchingTextBold.js +3 -1
  8. package/components/combobox/test/lion-combobox.test.js +105 -0
  9. package/components/form-core/src/form-group/FormGroupMixin.js +7 -0
  10. package/components/form-core/src/registration/FormRegistrarMixin.js +11 -0
  11. package/components/form-core/test/FormControlMixin.test.js +1 -0
  12. package/components/form-core/test/validate/Validator.test.js +2 -0
  13. package/components/form-core/test-suites/ValidateMixin.suite.js +2 -0
  14. package/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.js +1 -0
  15. package/components/input-datepicker/src/LionInputDatepicker.js +6 -2
  16. package/components/input-datepicker/src/localizeNamespaceLoader.js +1 -0
  17. package/components/input-datepicker/test/lion-input-datepicker.test.js +17 -2
  18. package/components/input-datepicker/translations/bg.js +1 -1
  19. package/components/input-datepicker/translations/cs.js +1 -1
  20. package/components/input-datepicker/translations/de.js +1 -1
  21. package/components/input-datepicker/translations/en.js +1 -1
  22. package/components/input-datepicker/translations/es.js +1 -1
  23. package/components/input-datepicker/translations/fr.js +1 -1
  24. package/components/input-datepicker/translations/hu.js +1 -1
  25. package/components/input-datepicker/translations/it.js +1 -1
  26. package/components/input-datepicker/translations/nl.js +1 -1
  27. package/components/input-datepicker/translations/pl.js +1 -1
  28. package/components/input-datepicker/translations/ro.js +1 -1
  29. package/components/input-datepicker/translations/ru.js +1 -1
  30. package/components/input-datepicker/translations/sk.js +1 -1
  31. package/components/input-datepicker/translations/tr.js +1 -1
  32. package/components/input-datepicker/translations/uk.js +1 -1
  33. package/components/input-datepicker/translations/zh.js +1 -1
  34. package/components/input-stepper/src/LionInputStepper.js +15 -7
  35. package/components/input-stepper/test-suites/lion-input-stepper.suite.js +28 -4
  36. package/components/localize/test/date/formatDate.test.js +1 -0
  37. package/components/menu/CHANGELOG.md +37 -0
  38. package/components/menu/README.md +335 -0
  39. package/components/menu/index.js +2 -0
  40. package/components/menu/lion-menu-overlay.js +3 -0
  41. package/components/menu/lion-menu.js +3 -0
  42. package/components/menu/lion-spinbutton.js +3 -0
  43. package/components/menu/lion-toolbar.js +3 -0
  44. package/components/menu/lion-tree.js +3 -0
  45. package/components/menu/package.json +46 -0
  46. package/components/menu/requirements.temp.md +12 -0
  47. package/components/menu/src/InteractiveListMixin.js +796 -0
  48. package/components/menu/src/LionMenu.js +250 -0
  49. package/components/menu/src/LionMenuHybrid.js +46 -0
  50. package/components/menu/src/LionMenuOverlay.js +155 -0
  51. package/components/menu/src/LionSpinbutton.js +213 -0
  52. package/components/menu/src/LionToolbar.js +94 -0
  53. package/components/menu/src/LionTree.js +52 -0
  54. package/components/menu/src/MultiLevelListMixin.js +274 -0
  55. package/components/menu/src/utils/isInView.js +26 -0
  56. package/components/menu/src/utils/listItemInteractions.js +104 -0
  57. package/components/menu/src/utils/uuid.js +3 -0
  58. package/components/menu/test/lion-menu.test.js +3 -0
  59. package/components/menu/test-suites/InteractiveListMixin.suite.js +142 -0
  60. package/components/menu/types/InteractiveListMixinTypes.d.ts +82 -0
  61. package/components/navigation-bar/README.md +1 -0
  62. package/components/navigation-bar/src/LionNavigationBar.js +422 -0
  63. package/components/navigation-bar/src/types.js +41 -0
  64. package/components/overlays/NEXT.md +49 -0
  65. package/components/overlays/src/OverlayController.js +249 -150
  66. package/components/overlays/src/OverlayMixin.js +76 -12
  67. package/components/overlays/src/OverlaysManager.js +98 -0
  68. package/components/overlays/src/overlayShadowDomStyle.js +2 -2
  69. package/components/overlays/src/utils/adopt-styles.js +2 -1
  70. package/components/overlays/src/utils/get-focusable-elements.js +4 -0
  71. package/components/overlays/src/utils/is-equal-config.js +10 -6
  72. package/components/overlays/test/OverlayController.test.js +120 -7
  73. package/components/overlays/test/utils-tests/is-equal-config.test.js +1 -1
  74. package/components/overlays/test-suites/OverlayMixin.suite.js +8 -8
  75. package/components/overlays/types/OverlayConfig.ts +25 -4
  76. package/components/select-rich/test/lion-select-rich.test.js +3 -0
  77. package/custom-elements.json +12950 -2486
  78. package/dist-types/components/button/src/LionButton.d.ts +1 -0
  79. package/dist-types/components/button/src/LionButton.d.ts.map +1 -1
  80. package/dist-types/components/button/test-suites/LionButton.suite.d.ts.map +1 -1
  81. package/dist-types/components/calendar/src/LionCalendar.d.ts.map +1 -1
  82. package/dist-types/components/collapsible/src/DisclosureMixin.d.ts +3 -0
  83. package/dist-types/components/collapsible/src/DisclosureMixin.d.ts.map +1 -0
  84. package/dist-types/components/collapsible/types/DisclosureMixinTypes.d.ts +24 -0
  85. package/dist-types/components/collapsible/types/DisclosureMixinTypes.d.ts.map +1 -0
  86. package/dist-types/components/combobox/src/utils/makeMatchingTextBold.d.ts.map +1 -1
  87. package/dist-types/components/dialog/src/LionDialog.d.ts +3 -1
  88. package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
  89. package/dist-types/components/form-core/src/form-group/FormGroupMixin.d.ts.map +1 -1
  90. package/dist-types/components/form-core/src/registration/FormRegistrarMixin.d.ts.map +1 -1
  91. package/dist-types/components/form-core/test-suites/ValidateMixin.suite.d.ts.map +1 -1
  92. package/dist-types/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.d.ts.map +1 -1
  93. package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts.map +1 -1
  94. package/dist-types/components/input-datepicker/src/localizeNamespaceLoader.d.ts.map +1 -1
  95. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts +4 -2
  96. package/dist-types/components/input-stepper/src/LionInputStepper.d.ts.map +1 -1
  97. package/dist-types/components/input-stepper/test-suites/lion-input-stepper.suite.d.ts.map +1 -1
  98. package/dist-types/components/menu/index.d.ts +3 -0
  99. package/dist-types/components/menu/index.d.ts.map +1 -0
  100. package/dist-types/components/menu/lion-menu-overlay.d.ts +2 -0
  101. package/dist-types/components/menu/lion-menu-overlay.d.ts.map +1 -0
  102. package/dist-types/components/menu/lion-menu.d.ts +2 -0
  103. package/dist-types/components/menu/lion-menu.d.ts.map +1 -0
  104. package/dist-types/components/menu/lion-spinbutton.d.ts +2 -0
  105. package/dist-types/components/menu/lion-spinbutton.d.ts.map +1 -0
  106. package/dist-types/components/menu/lion-toolbar.d.ts +2 -0
  107. package/dist-types/components/menu/lion-toolbar.d.ts.map +1 -0
  108. package/dist-types/components/menu/lion-tree.d.ts +2 -0
  109. package/dist-types/components/menu/lion-tree.d.ts.map +1 -0
  110. package/dist-types/components/menu/src/InteractiveListMixin.d.ts +611 -0
  111. package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -0
  112. package/dist-types/components/menu/src/LionMenu.d.ts +563 -0
  113. package/dist-types/components/menu/src/LionMenu.d.ts.map +1 -0
  114. package/dist-types/components/menu/src/LionMenuHybrid.d.ts +19 -0
  115. package/dist-types/components/menu/src/LionMenuHybrid.d.ts.map +1 -0
  116. package/dist-types/components/menu/src/LionMenuOverlay.d.ts +1007 -0
  117. package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +1 -0
  118. package/dist-types/components/menu/src/LionSpinbutton.d.ts +58 -0
  119. package/dist-types/components/menu/src/LionSpinbutton.d.ts.map +1 -0
  120. package/dist-types/components/menu/src/LionToolbar.d.ts +8 -0
  121. package/dist-types/components/menu/src/LionToolbar.d.ts.map +1 -0
  122. package/dist-types/components/menu/src/LionTree.d.ts +445 -0
  123. package/dist-types/components/menu/src/LionTree.d.ts.map +1 -0
  124. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts +493 -0
  125. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -0
  126. package/dist-types/components/menu/src/utils/isInView.d.ts +9 -0
  127. package/dist-types/components/menu/src/utils/isInView.d.ts.map +1 -0
  128. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts +42 -0
  129. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts.map +1 -0
  130. package/dist-types/components/menu/src/utils/uuid.d.ts +2 -0
  131. package/dist-types/components/menu/src/utils/uuid.d.ts.map +1 -0
  132. package/dist-types/components/menu/test/lion-menu.test.d.ts +2 -0
  133. package/dist-types/components/menu/test/lion-menu.test.d.ts.map +1 -0
  134. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts +13 -0
  135. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +1 -0
  136. package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts +238 -0
  137. package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts.map +1 -0
  138. package/dist-types/components/navigation-bar/src/types.d.ts +85 -0
  139. package/dist-types/components/navigation-bar/src/types.d.ts.map +1 -0
  140. package/dist-types/components/overlays/src/OverlayController.d.ts +10 -20
  141. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  142. package/dist-types/components/overlays/src/OverlayMixin.d.ts +3 -0
  143. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  144. package/dist-types/components/overlays/src/OverlaysManager.d.ts +31 -0
  145. package/dist-types/components/overlays/src/OverlaysManager.d.ts.map +1 -1
  146. package/dist-types/components/overlays/src/utils/get-focusable-elements.d.ts.map +1 -1
  147. package/dist-types/components/overlays/src/utils/is-equal-config.d.ts.map +1 -1
  148. package/dist-types/components/overlays/types/OverlayConfig.d.ts +11 -2
  149. package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
  150. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +12 -2
  151. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  152. package/dist-types/exports/define/lion-item.d.ts +2 -0
  153. package/dist-types/exports/define/lion-item.d.ts.map +1 -0
  154. package/dist-types/exports/define/lion-menu-overlay.d.ts +2 -0
  155. package/dist-types/exports/define/lion-menu-overlay.d.ts.map +1 -0
  156. package/dist-types/exports/define/lion-menu.d.ts +2 -0
  157. package/dist-types/exports/define/lion-menu.d.ts.map +1 -0
  158. package/dist-types/exports/define/lion-navigation-bar.d.ts +2 -0
  159. package/dist-types/exports/define/lion-navigation-bar.d.ts.map +1 -0
  160. package/dist-types/exports/define/lion-toolbar.d.ts +2 -0
  161. package/dist-types/exports/define/lion-toolbar.d.ts.map +1 -0
  162. package/dist-types/exports/define/lion-tree.d.ts +2 -0
  163. package/dist-types/exports/define/lion-tree.d.ts.map +1 -0
  164. package/dist-types/exports/menu.d.ts +7 -0
  165. package/dist-types/exports/menu.d.ts.map +1 -0
  166. package/dist-types/exports/navigation-bar.d.ts +2 -0
  167. package/dist-types/exports/navigation-bar.d.ts.map +1 -0
  168. package/dist-types/exports/overlays.d.ts +3 -0
  169. package/dist-types/exports/overlays.d.ts.map +1 -1
  170. package/dist-types/exports/types/overlays.d.ts +1 -0
  171. package/dist-types/exports/types/overlays.d.ts.map +1 -1
  172. package/dist-types/tsconfig.tsbuildinfo +1 -1
  173. package/docs/components/checkbox-group/overview.md +2 -2
  174. package/docs/components/collapsible/overview.md +1 -1
  175. package/docs/components/combobox/overview.md +1 -1
  176. package/docs/components/combobox/use-cases.md +1 -1
  177. package/docs/components/dialog/overview.md +1 -1
  178. package/docs/components/fieldset/overview.md +5 -5
  179. package/docs/components/form/overview.md +3 -3
  180. package/docs/components/form/use-cases.md +2 -2
  181. package/docs/components/icon/use-cases.md +1 -1
  182. package/docs/components/index.md +1 -1
  183. package/docs/components/input/overview.md +1 -1
  184. package/docs/components/input-amount/overview.md +3 -3
  185. package/docs/components/input-date/overview.md +3 -3
  186. package/docs/components/input-datepicker/overview.md +5 -5
  187. package/docs/components/input-datepicker/use-cases.md +16 -1
  188. package/docs/components/input-email/overview.md +2 -2
  189. package/docs/components/input-file/overview.md +1 -1
  190. package/docs/components/input-iban/overview.md +2 -2
  191. package/docs/components/input-range/api-table.md +1 -1
  192. package/docs/components/input-range/overview.md +2 -2
  193. package/docs/components/input-stepper/api-table.md +2 -2
  194. package/docs/components/input-stepper/overview.md +1 -1
  195. package/docs/components/input-stepper/use-cases.md +26 -0
  196. package/docs/components/input-tel/overview.md +1 -1
  197. package/docs/components/input-tel/use-cases.md +6 -6
  198. package/docs/components/input-tel-dropdown/overview.md +1 -1
  199. package/docs/components/listbox/use-cases.md +1 -1
  200. package/docs/components/menu/api-table.md +450 -0
  201. package/docs/components/menu/applyMenuDemoStyles.js +173 -0
  202. package/docs/components/menu/extensions.md +216 -0
  203. package/docs/components/menu/index.md +13 -0
  204. package/docs/components/menu/overview.md +422 -0
  205. package/docs/components/radio-group/overview.md +3 -3
  206. package/docs/components/select/use-cases.md +1 -1
  207. package/docs/components/select-rich/api-table.md +1 -1
  208. package/docs/components/select-rich/use-cases.md +2 -2
  209. package/docs/components/tabs/examples.md +1 -1
  210. package/docs/fundamentals/index.md +4 -4
  211. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  212. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  213. package/docs/fundamentals/rationales/accessibility.md +1 -1
  214. package/docs/fundamentals/systems/core/SlotMixin.md +18 -18
  215. package/docs/fundamentals/systems/core/overview.md +1 -1
  216. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  217. package/docs/fundamentals/systems/form/interaction-states.md +0 -1
  218. package/docs/fundamentals/systems/form/model-value.md +1 -1
  219. package/docs/fundamentals/systems/form/overview.md +28 -28
  220. package/docs/fundamentals/systems/form/validate.md +2 -2
  221. package/docs/fundamentals/systems/localize/overview.md +4 -4
  222. package/docs/fundamentals/systems/localize/text.md +1 -1
  223. package/docs/fundamentals/systems/overlays/api-table.md +92 -45
  224. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  225. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  226. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  227. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  228. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  229. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  230. package/docs/guides/how-to/create-a-form.md +4 -4
  231. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  232. package/docs/guides/how-to/index.md +14 -0
  233. package/docs/guides/index.md +8 -8
  234. package/docs/guides/principles/index.md +5 -5
  235. package/docs/guides/principles/styling.md +1 -2
  236. package/exports/collapsible.js +1 -0
  237. package/exports/define/lion-item.js +3 -0
  238. package/exports/define/lion-menu-overlay.js +3 -0
  239. package/exports/define/lion-menu.js +3 -0
  240. package/exports/define/lion-navigation-bar.js +3 -0
  241. package/exports/define/lion-toolbar.js +3 -0
  242. package/exports/define/lion-tree.js +3 -0
  243. package/exports/menu.js +6 -0
  244. package/exports/navigation-bar.js +1 -0
  245. package/exports/overlays.js +6 -0
  246. package/exports/types/overlays.ts +1 -0
  247. package/menu.js +6 -0
  248. package/navigation-bar.js +1 -0
  249. package/overlays.js +6 -0
  250. package/package.json +1 -1
@@ -2,6 +2,18 @@ import { dedupeMixin } from '@open-wc/dedupe-mixin';
2
2
  import { OverlayController } from './OverlayController.js';
3
3
  import { isEqualConfig } from './utils/is-equal-config.js';
4
4
 
5
+ /**
6
+ * @typedef {<T>(ingOverlayHost:T) => void} PostProcessor
7
+ */
8
+
9
+ /** @type {PostProcessor[]} */
10
+ const overlayMixinPostProcessors = [];
11
+
12
+ /** @param {PostProcessor} postProcessor */
13
+ export function _addOverlayMixinPostProcessor(postProcessor) {
14
+ overlayMixinPostProcessors.push(postProcessor);
15
+ }
16
+
5
17
  /**
6
18
  * @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig
7
19
  * @typedef {import('../types/OverlayMixinTypes.js').DefineOverlayConfig} DefineOverlayConfig
@@ -16,15 +28,12 @@ import { isEqualConfig } from './utils/is-equal-config.js';
16
28
  * @type {OverlayMixin}
17
29
  * @param {import('@open-wc/dedupe-mixin').Constructor<import('lit').LitElement>} superclass
18
30
  */
19
- export const OverlayMixinImplementation = superclass =>
31
+ export const OverlayMixinImplementation = superclass => {
20
32
  // @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
21
33
  class OverlayMixin extends superclass {
22
34
  static get properties() {
23
35
  return {
24
- opened: {
25
- type: Boolean,
26
- reflect: true,
27
- },
36
+ opened: { type: Boolean, reflect: true },
28
37
  };
29
38
  }
30
39
 
@@ -50,6 +59,17 @@ export const OverlayMixinImplementation = superclass =>
50
59
  this.open = this.open.bind(this);
51
60
  /** @type {EventListener} */
52
61
  this.close = this.close.bind(this);
62
+
63
+ // // By default, we go for disclosure behavior
64
+ // // TODO: in the future, bring disclosure behavior to a controller (and therefore directive).
65
+ // // Take inspiration from VisibilityToggleCtrl of portal elements
66
+ // /**
67
+ // * Terminology aligned with https://open-ui.org/components/openable.explainer/
68
+ // * @type {'disclosure'|'overlay'}
69
+ // */
70
+ // this.openableMode = 'disclosure';
71
+
72
+ // allow hybrid disclosure/overlay components
53
73
  }
54
74
 
55
75
  get config() {
@@ -58,6 +78,7 @@ export const OverlayMixinImplementation = superclass =>
58
78
 
59
79
  /** @param {OverlayConfig} value */
60
80
  set config(value) {
81
+ // TODO: built-in in overlayCtrl now... delete here
61
82
  const shouldUpdate = !isEqualConfig(this.config, value);
62
83
 
63
84
  if (this._overlayCtrl && shouldUpdate) {
@@ -149,6 +170,8 @@ export const OverlayMixinImplementation = superclass =>
149
170
  */
150
171
  // eslint-disable-next-line class-methods-use-this
151
172
  _setupOpenCloseListeners() {
173
+ // Keeps the close-overlay event private for the controller.
174
+ // (we already have )
152
175
  /**
153
176
  * @param {{ stopPropagation: () => void; }} ev
154
177
  */
@@ -206,11 +229,11 @@ export const OverlayMixinImplementation = superclass =>
206
229
  // @ts-expect-error
207
230
  static enabledWarnings = super.enabledWarnings?.filter(w => w !== 'change-in-update') || [];
208
231
 
209
- get _overlayInvokerNode() {
210
- return /** @type {HTMLElement | undefined} */ (
211
- Array.from(this.children).find(child => child.slot === 'invoker')
212
- );
213
- }
232
+ // get _overlayInvokerNode() {
233
+ // return /** @type {HTMLElement | undefined} */ (
234
+ // Array.from(this.children).find(child => child.slot === 'invoker')
235
+ // );
236
+ // }
214
237
 
215
238
  /**
216
239
  * @overridable
@@ -244,13 +267,46 @@ export const OverlayMixinImplementation = superclass =>
244
267
  );
245
268
  }
246
269
 
270
+ /**
271
+ * @param {Element} focusableElOrWrapper
272
+ * @returns {Element|null}
273
+ */
274
+ static _getFocusableInvokerEl(focusableElOrWrapper) {
275
+ // return focusableElOrWrapper;
276
+ return (
277
+ focusableElOrWrapper &&
278
+ (focusableElOrWrapper.hasAttribute('tabindex') || focusableElOrWrapper.tagName === 'BUTTON'
279
+ ? focusableElOrWrapper
280
+ : focusableElOrWrapper.querySelector('[role=button], button'))
281
+ );
282
+ }
283
+
284
+ get _overlayInvokerNode() {
285
+ const ctor = /** @type {typeof OverlayMixin} */ (this.constructor);
286
+ if (!this.__invokerNode) {
287
+ const slottedNode = Array.from(this.children).find(child => child.slot === 'invoker');
288
+ if (slottedNode) {
289
+ this.__invokerNode = ctor._getFocusableInvokerEl(slottedNode);
290
+ } else {
291
+ // Look for preceeding sibling with [data-invoker] attribute, and try to find a focusable element in it (either itself or a child)
292
+ this.__invokerNode =
293
+ this.previousElementSibling?.hasAttribute('data-invoker') &&
294
+ ctor._getFocusableInvokerEl(this.previousElementSibling);
295
+ }
296
+ }
297
+ return this.__invokerNode;
298
+ }
299
+
247
300
  /** @protected */
248
301
  _setupOverlayCtrl() {
249
302
  if (this.#hasSetup) return;
303
+
304
+ const invokerNode = this._overlayInvokerNode;
305
+
250
306
  const config = {
251
307
  contentNode: this._overlayContentNode,
252
308
  contentWrapperNode: this._overlayContentWrapperNode,
253
- invokerNode: this._overlayInvokerNode,
309
+ invokerNode: invokerNode && invokerNode instanceof HTMLElement ? invokerNode : undefined,
254
310
  referenceNode: this._overlayReferenceNode,
255
311
  backdropNode: this._overlayBackdropNode,
256
312
  };
@@ -270,6 +326,8 @@ export const OverlayMixinImplementation = superclass =>
270
326
 
271
327
  /** @protected */
272
328
  _teardownOverlayCtrl() {
329
+ // Make sure that dynamic behavior (e.g. responsive) is possible by allowing multiple setups and teardowns of the overlay controller.
330
+ this.#hasSetup = false;
273
331
  if (!this._overlayCtrl) return;
274
332
 
275
333
  this._teardownOpenCloseListeners();
@@ -418,5 +476,11 @@ export const OverlayMixinImplementation = superclass =>
418
476
  await this.updateComplete;
419
477
  return !this.isConnected;
420
478
  }
421
- };
479
+ }
480
+ for (const postProcessor of overlayMixinPostProcessors) {
481
+ postProcessor(OverlayMixin);
482
+ }
483
+ // @ts-ignore https://github.com/microsoft/TypeScript/issues/36821#issuecomment-588375051
484
+ return OverlayMixin;
485
+ };
422
486
  export const OverlayMixin = dedupeMixin(OverlayMixinImplementation);
@@ -3,6 +3,7 @@ import { browserDetection } from '@lion/ui/core.js';
3
3
  /**
4
4
  * @typedef {import('lit').CSSResult} CSSResult
5
5
  * @typedef {import('./OverlayController.js').OverlayController} OverlayController
6
+ * @typedef {import('@lion/ui/types/overlays.js').OverlayPhase} OverlayPhase
6
7
  */
7
8
 
8
9
  import { overlayDocumentStyle } from './overlayDocumentStyle.js';
@@ -53,6 +54,31 @@ export class OverlaysManager {
53
54
  * @private
54
55
  */
55
56
  this.__blockingMap = new WeakMap();
57
+ /**
58
+ * @private
59
+ * @type {{
60
+ * preventScrollCount: number;
61
+ * clientWidth: number | undefined;
62
+ * clientHeight: number | undefined;
63
+ * marginRightInline: string | undefined;
64
+ * marginBottomInline: string | undefined;
65
+ * marginRight: number | undefined;
66
+ * marginBottom: number | undefined;
67
+ * }}
68
+ */
69
+ this.__bodySizeVars = {
70
+ preventScrollCount: 0,
71
+ clientWidth: undefined,
72
+ clientHeight: undefined,
73
+ marginRightInline: undefined,
74
+ marginBottomInline: undefined,
75
+ marginRight: undefined,
76
+ marginBottom: undefined,
77
+ };
78
+
79
+ this.__forTesting = {
80
+ bodySizeVars: this.__bodySizeVars,
81
+ };
56
82
 
57
83
  if (!OverlaysManager.__globalStyleNode) {
58
84
  OverlaysManager.__globalStyleNode = OverlaysManager.__createGlobalStyleNode();
@@ -119,6 +145,13 @@ export class OverlaysManager {
119
145
  this.__list = [];
120
146
  this.__shownList = [];
121
147
  this._siblingsInert = false;
148
+ this.__bodySizeVars.preventScrollCount = 0;
149
+ this.__bodySizeVars.clientWidth = undefined;
150
+ this.__bodySizeVars.clientHeight = undefined;
151
+ this.__bodySizeVars.marginRightInline = undefined;
152
+ this.__bodySizeVars.marginBottomInline = undefined;
153
+ this.__bodySizeVars.marginRight = undefined;
154
+ this.__bodySizeVars.marginBottom = undefined;
122
155
 
123
156
  if (OverlaysManager.__globalStyleNode) {
124
157
  document.head.removeChild(
@@ -136,6 +169,71 @@ export class OverlaysManager {
136
169
 
137
170
  /** PreventsScroll */
138
171
 
172
+ /**
173
+ * @param {{ phase: OverlayPhase }} config
174
+ */
175
+ requestToKeepBodySize({ phase }) {
176
+ switch (phase) {
177
+ case 'before-show':
178
+ // Tracks the amount of active preventsScroll overlays.
179
+ // Only the first overlay captures body metrics and only the last one restores them.
180
+ if (this.__bodySizeVars.preventScrollCount === 0) {
181
+ this.__bodySizeVars.clientWidth = document.body.clientWidth;
182
+ this.__bodySizeVars.clientHeight = document.body.clientHeight;
183
+ this.__bodySizeVars.marginRightInline = document.body.style.marginRight;
184
+ this.__bodySizeVars.marginBottomInline = document.body.style.marginBottom;
185
+ }
186
+ this.__bodySizeVars.preventScrollCount += 1;
187
+ break;
188
+ case 'show': {
189
+ if (this.__bodySizeVars.preventScrollCount === 1) {
190
+ if (window.getComputedStyle) {
191
+ const bodyStyle = window.getComputedStyle(document.body);
192
+ this.__bodySizeVars.marginRight = parseInt(
193
+ bodyStyle.getPropertyValue('margin-right'),
194
+ 10,
195
+ );
196
+ this.__bodySizeVars.marginBottom = parseInt(
197
+ bodyStyle.getPropertyValue('margin-bottom'),
198
+ 10,
199
+ );
200
+ } else {
201
+ this.__bodySizeVars.marginRight = 0;
202
+ this.__bodySizeVars.marginBottom = 0;
203
+ }
204
+ const scrollbarWidth =
205
+ document.body.clientWidth - /** @type {number} */ (this.__bodySizeVars.clientWidth);
206
+ const scrollbarHeight =
207
+ document.body.clientHeight - /** @type {number} */ (this.__bodySizeVars.clientHeight);
208
+ const newMarginRight = this.__bodySizeVars.marginRight + scrollbarWidth;
209
+ const newMarginBottom = this.__bodySizeVars.marginBottom + scrollbarHeight;
210
+ // @ts-expect-error [external]: CSS not yet typed
211
+ if (window.CSS?.number && document.body.attributeStyleMap?.set) {
212
+ // @ts-ignore - CSS Typed OM not fully typed
213
+ document.body.attributeStyleMap.set('margin-right', CSS.px(newMarginRight));
214
+ // @ts-ignore - CSS Typed OM not fully typed
215
+ document.body.attributeStyleMap.set('margin-bottom', CSS.px(newMarginBottom));
216
+ } else {
217
+ document.body.style.marginRight = `${newMarginRight}px`;
218
+ document.body.style.marginBottom = `${newMarginBottom}px`;
219
+ }
220
+ }
221
+ break;
222
+ }
223
+ case 'hide':
224
+ case 'teardown':
225
+ if (this.__bodySizeVars.preventScrollCount > 0) {
226
+ this.__bodySizeVars.preventScrollCount -= 1;
227
+ if (this.__bodySizeVars.preventScrollCount === 0) {
228
+ document.body.style.marginRight = this.__bodySizeVars.marginRightInline || '';
229
+ document.body.style.marginBottom = this.__bodySizeVars.marginBottomInline || '';
230
+ }
231
+ }
232
+ break;
233
+ /* no default */
234
+ }
235
+ }
236
+
139
237
  // eslint-disable-next-line class-methods-use-this
140
238
  requestToPreventScroll() {
141
239
  const { isIOS, isMacSafari } = browserDetection;
@@ -113,11 +113,11 @@ export const overlayShadowDomStyle = css`
113
113
  }
114
114
 
115
115
  @media screen and (prefers-reduced-motion: reduce) {
116
- .overlays .overlays__backdrop--animation-in {
116
+ .overlays__backdrop--animation-in {
117
117
  animation: overlays-backdrop-fade-in 1ms;
118
118
  }
119
119
 
120
- .overlays .overlays__backdrop--animation-out {
120
+ .overlays__backdrop--animation-out {
121
121
  animation: overlays-backdrop-fade-out 1ms;
122
122
  }
123
123
  }
@@ -56,7 +56,8 @@ function adoptStyleWhenAdoptedStylesheetsNotSupported(
56
56
  const styleEl = document.createElement('style');
57
57
  // keep notation, so it's not renamed in minification/build
58
58
  // eslint-disable-next-line dot-notation
59
- const nonce = window['litNonce'];
59
+ // @ts-ignore - litNonce is a custom property
60
+ const nonce = window.litNonce;
60
61
  if (nonce !== undefined) {
61
62
  styleEl.setAttribute('nonce', nonce);
62
63
  }
@@ -8,6 +8,7 @@ import { isVisible } from './is-visible.js';
8
8
  import { sortByTabIndex } from './sort-by-tabindex.js';
9
9
 
10
10
  // IE11 supports matches as 'msMatchesSelector'
11
+ /** @type {'matches' | 'msMatchesSelector'} */
11
12
  const matchesFunc = 'matches' in Element.prototype ? 'matches' : 'msMatchesSelector';
12
13
 
13
14
  /**
@@ -16,11 +17,14 @@ const matchesFunc = 'matches' in Element.prototype ? 'matches' : 'msMatchesSelec
16
17
  */
17
18
  function isFocusable(element) {
18
19
  // Elements that cannot be focused if they have [disabled] attribute.
20
+ // @ts-ignore - msMatchesSelector is legacy
19
21
  if (element[matchesFunc]('input, select, textarea, button, object')) {
22
+ // @ts-ignore
20
23
  return element[matchesFunc](':not([disabled])');
21
24
  }
22
25
 
23
26
  // Elements that can be focused even if they have [disabled] attribute.
27
+ // @ts-ignore
24
28
  return element[matchesFunc]('a[href], area[href], iframe, [tabindex], [contentEditable]');
25
29
  }
26
30
 
@@ -12,13 +12,17 @@
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 aProps = Object.keys(a);
18
- const bProps = Object.keys(b);
19
- if (aProps.length !== bProps.length) {
20
- return false;
20
+ if (a instanceof Node && b instanceof Node) {
21
+ return a === b;
21
22
  }
22
- const isEqual = /** @param {string} prop */ prop => isEqualConfig(a[prop], b[prop]);
23
- return aProps.every(isEqual);
23
+
24
+ return Array.from(new Set([...Object.keys(a), ...Object.keys(b)])).every(prop =>
25
+ // @ts-ignore - dynamic property access
26
+ isEqualConfig(a[prop], b[prop], prop),
27
+ );
24
28
  }
@@ -283,8 +283,9 @@ describe('OverlayController', () => {
283
283
  contentNode,
284
284
  });
285
285
  };
286
+
286
287
  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.',
288
+ '[OverlayController] Could not find a render target, makes sure contentNode has a parent element (or contentWrapperNode is connected)',
288
289
  );
289
290
  });
290
291
 
@@ -403,7 +404,7 @@ describe('OverlayController', () => {
403
404
  { ignoreAttributes: ['closedby'] },
404
405
  );
405
406
 
406
- expect(el).lightDom.to.equal(`<div slot="content">projected</div>`);
407
+ expect(el).lightDom.to.equal(`<div data-content="" slot="content">projected</div>`);
407
408
  });
408
409
  });
409
410
 
@@ -425,7 +426,7 @@ describe('OverlayController', () => {
425
426
  `
426
427
  <dialog data-overlay-outer-wrapper="" open="" role="none" style="${wrappingDialogNodeStyle}">
427
428
  <div data-id="content-wrapper">
428
- <div id="content">non projected</div>
429
+ <div data-content="" id="content">non projected</div>
429
430
  </div>
430
431
  </dialog>
431
432
  `,
@@ -1384,6 +1385,109 @@ describe('OverlayController', () => {
1384
1385
  await ctrl1.hide();
1385
1386
  expect(Array.from(document.body.classList)).to.contain('overlays-scroll-lock');
1386
1387
  });
1388
+
1389
+ it('does not accumulate body margins when nested overlays have preventsScroll', async () => {
1390
+ const ctrl0 = new OverlayController({
1391
+ ...withGlobalTestConfig(),
1392
+ preventsScroll: true,
1393
+ });
1394
+ const ctrl1 = new OverlayController({
1395
+ ...withGlobalTestConfig(),
1396
+ preventsScroll: true,
1397
+ });
1398
+
1399
+ const originalMarginRight = document.body.style.marginRight;
1400
+
1401
+ await ctrl0.show();
1402
+ const marginAfterFirst = document.body.style.marginRight;
1403
+
1404
+ await ctrl1.show();
1405
+ const marginAfterSecond = document.body.style.marginRight;
1406
+
1407
+ // The margin should NOT increase further when second overlay opens
1408
+ expect(marginAfterSecond).to.equal(marginAfterFirst);
1409
+
1410
+ await ctrl1.hide();
1411
+ // After hiding second, first still prevents scroll — margin stays
1412
+ expect(document.body.style.marginRight).to.equal(marginAfterFirst);
1413
+
1414
+ await ctrl0.hide();
1415
+ // After hiding all, original margin is restored
1416
+ expect(document.body.style.marginRight).to.equal(originalMarginRight);
1417
+ });
1418
+
1419
+ it('restores body margin when overlay with preventsScroll is torn down while shown', async () => {
1420
+ const ctrl = new OverlayController({
1421
+ ...withGlobalTestConfig(),
1422
+ preventsScroll: true,
1423
+ });
1424
+
1425
+ const originalMarginRight = document.body.style.marginRight;
1426
+
1427
+ await ctrl.show();
1428
+ const marginAfterShow = document.body.style.marginRight;
1429
+ // Margin should be set after show
1430
+ expect(marginAfterShow).to.not.equal(originalMarginRight);
1431
+
1432
+ ctrl.teardown();
1433
+ // Margin should be restored after teardown
1434
+ expect(document.body.style.marginRight).to.equal(originalMarginRight);
1435
+ });
1436
+
1437
+ it('does not break body margin when teardown() is called on a hidden overlay', async () => {
1438
+ const ctrl0 = new OverlayController({
1439
+ ...withGlobalTestConfig(),
1440
+ preventsScroll: true,
1441
+ });
1442
+ const ctrl1 = new OverlayController({
1443
+ ...withGlobalTestConfig(),
1444
+ preventsScroll: true,
1445
+ });
1446
+
1447
+ const originalMarginRight = document.body.style.marginRight;
1448
+
1449
+ // Show first overlay
1450
+ await ctrl0.show();
1451
+ const marginAfterFirst = document.body.style.marginRight;
1452
+ expect(marginAfterFirst).to.not.equal(originalMarginRight);
1453
+
1454
+ // Teardown second overlay that was never shown
1455
+ ctrl1.teardown();
1456
+ // Margin should still be set by first overlay
1457
+ expect(document.body.style.marginRight).to.equal(marginAfterFirst);
1458
+
1459
+ // Hide first overlay
1460
+ await ctrl0.hide();
1461
+ // Now margin should be restored
1462
+ expect(document.body.style.marginRight).to.equal(originalMarginRight);
1463
+ });
1464
+
1465
+ it('does not break body margin when updateConfig() is called on a hidden overlay', async () => {
1466
+ const ctrl = new OverlayController({
1467
+ ...withGlobalTestConfig(),
1468
+ preventsScroll: true,
1469
+ });
1470
+
1471
+ const originalMarginRight = document.body.style.marginRight;
1472
+
1473
+ // updateConfig calls teardown internally
1474
+ ctrl.updateConfig({
1475
+ ...withGlobalTestConfig(),
1476
+ preventsScroll: true,
1477
+ });
1478
+
1479
+ // Margin should not be affected
1480
+ expect(document.body.style.marginRight).to.equal(originalMarginRight);
1481
+
1482
+ // Show after updateConfig
1483
+ await ctrl.show();
1484
+ const marginAfterShow = document.body.style.marginRight;
1485
+ expect(marginAfterShow).to.not.equal(originalMarginRight);
1486
+
1487
+ // Hide should restore
1488
+ await ctrl.hide();
1489
+ expect(document.body.style.marginRight).to.equal(originalMarginRight);
1490
+ });
1387
1491
  });
1388
1492
 
1389
1493
  describe('hasBackdrop', () => {
@@ -2180,15 +2284,24 @@ describe('OverlayController', () => {
2180
2284
  it('should not run with scroll prevention', async () => {
2181
2285
  await overlayControllerNoPrevent.show();
2182
2286
 
2183
- expect(overlayControllerNoPrevent.__bodyMarginRightInline).to.equal(undefined);
2184
- expect(overlayControllerNoPrevent.__bodyMarginRight).to.equal(undefined);
2287
+ expect(
2288
+ overlayControllerNoPrevent.manager.__forTesting.bodySizeVars.marginRightInline,
2289
+ ).to.equal(undefined);
2290
+ expect(overlayControllerNoPrevent.manager.__forTesting.bodySizeVars.marginRight).to.equal(
2291
+ undefined,
2292
+ );
2185
2293
  });
2186
2294
 
2187
2295
  it('should run with scroll prevention', async () => {
2296
+ await overlayControllerPreventsScroll.hide();
2188
2297
  await overlayControllerPreventsScroll.show();
2189
2298
 
2190
- expect(overlayControllerPreventsScroll.__bodyMarginRightInline).to.not.equal(undefined);
2191
- expect(overlayControllerPreventsScroll.__bodyMarginRight).to.not.equal(undefined);
2299
+ expect(
2300
+ overlayControllerPreventsScroll.manager.__forTesting.bodySizeVars.marginRightInline,
2301
+ ).to.not.equal(undefined);
2302
+ expect(
2303
+ overlayControllerPreventsScroll.manager.__forTesting.bodySizeVars.marginRight,
2304
+ ).to.not.equal(undefined);
2192
2305
  });
2193
2306
  });
2194
2307
  });
@@ -48,7 +48,7 @@ describe('isEqualConfig()', () => {
48
48
  expect(isEqualConfig({ ...config, isBlocking: true }, config)).eql(false);
49
49
  });
50
50
 
51
- it('regards missing props different from ones with undefined value', () => {
51
+ it.skip('regards missing props different from ones with undefined value', () => {
52
52
  const config = TestConfig();
53
53
  expect(isEqualConfig(config, { ...config, referenceNode: undefined })).eql(false);
54
54
  });
@@ -537,13 +537,13 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
537
537
  it('works with cache directive when reconnected', async () => {
538
538
  class CachingContext extends LitElement {
539
539
  static properties = {
540
- switched: { type: Boolean },
540
+ showOverlay: { type: Boolean },
541
541
  };
542
542
 
543
543
  constructor() {
544
544
  super();
545
545
 
546
- this.switched = false;
546
+ this.showOverlay = true;
547
547
  }
548
548
 
549
549
  get overlayEl() {
@@ -552,12 +552,12 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
552
552
 
553
553
  render() {
554
554
  return html`${cache(
555
- this.switched
556
- ? html`something else`
557
- : html`<${tag} id="myOverlay">
555
+ this.showOverlay
556
+ ? html`<${tag} id="myOverlay">
558
557
  <div slot="content">content b</div>
559
558
  <button slot="invoker">invoker button b</button>
560
- </${tag}>`,
559
+ </${tag}>`
560
+ : html`something else`,
561
561
  )}`;
562
562
  }
563
563
  }
@@ -578,7 +578,7 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
578
578
  // @ts-expect-error [allow-protected] in tests
579
579
  const setupSpy = sinon.spy(el.overlayEl, '_setupOverlayCtrl');
580
580
 
581
- el.switched = true;
581
+ el.showOverlay = false;
582
582
  // render the new content
583
583
  await el.updateComplete;
584
584
  // wait for the teardown to complete
@@ -588,7 +588,7 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
588
588
  expect(teardownSpy.callCount).to.equal(1);
589
589
  expect(setupSpy.callCount).to.equal(0);
590
590
 
591
- el.switched = false;
591
+ el.showOverlay = true;
592
592
  await el.updateComplete;
593
593
  expect(setupSpy.callCount).to.equal(1);
594
594
 
@@ -11,6 +11,15 @@ export type ViewportPlacement =
11
11
  | 'bottom-left'
12
12
  | 'left';
13
13
 
14
+ export type OverlayPhase =
15
+ | 'setup'
16
+ | 'init'
17
+ | 'teardown'
18
+ | 'before-show'
19
+ | 'show'
20
+ | 'hide'
21
+ | 'add'
22
+ | 'remove';
14
23
 
15
24
  export interface ViewportConfig {
16
25
  placement: ViewportPlacement;
@@ -87,14 +96,26 @@ export interface OverlayConfig {
87
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) */
88
97
  invokerRelation?: 'label' | 'description';
89
98
 
90
- /** render a div instead of dialog */
91
- _noDialogEl?: Boolean;
92
-
93
99
  /**
94
100
  * Determines the conditions hiding/showing should be based on. It gets the OverlayController as input and returns an object with
95
101
  * functions with Overlay phases as keys
96
102
  */
97
103
  visibilityTriggerFunction?: Function;
98
- }
99
104
 
105
+ /**
106
+ * Whether the overlay content should receive focus when shown.
107
+ */
108
+ focusContentOnOpen?: boolean;
100
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
+ isOpenable?: 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
+ }
@@ -832,6 +832,7 @@ describe('lion-select-rich', () => {
832
832
  await waitUntil(isSelectRichRendered);
833
833
  getInvoker()?.click();
834
834
  await waitUntil(isDialogVisible);
835
+ // @ts-ignore - NodeList array access
835
836
  const optionBlue = /** @type { HTMLElement | undefined } */ (
836
837
  wrapperElementShadowRoot?.querySelectorAll('lion-option')
837
838
  )?.[1];
@@ -955,6 +956,7 @@ describe('lion-select-rich', () => {
955
956
  await waitUntil(isSelectRichRendered);
956
957
  getInvoker()?.click();
957
958
  await waitUntil(isDialogVisible);
959
+ // @ts-ignore - NodeList array access
958
960
  const optionBlue = /** @type { HTMLElement | undefined } */ (
959
961
  wrapperElementShadowRoot?.querySelectorAll('lion-option')
960
962
  )?.[1];
@@ -1082,6 +1084,7 @@ describe('lion-select-rich', () => {
1082
1084
  await waitUntil(isSelectRichRendered);
1083
1085
  getInvoker()?.click();
1084
1086
  await waitUntil(isDialogVisible);
1087
+ // @ts-ignore - NodeList array access
1085
1088
  const optionBlue = /** @type { HTMLElement | undefined } */ (
1086
1089
  wrapperElementShadowRoot?.querySelectorAll('lion-option')
1087
1090
  )?.[1];