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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (218) hide show
  1. package/collapsible.js +1 -0
  2. package/components/button/src/LionButton.js +4 -0
  3. package/components/button/test-suites/LionButton.suite.js +1 -0
  4. package/components/calendar/src/LionCalendar.js +2 -0
  5. package/components/collapsible/src/DisclosureMixin.js +313 -0
  6. package/components/collapsible/types/DisclosureMixinTypes.ts +48 -0
  7. package/components/combobox/test/lion-combobox.test.js +1 -0
  8. package/components/form-core/src/form-group/FormGroupMixin.js +7 -0
  9. package/components/form-core/src/registration/FormRegistrarMixin.js +11 -0
  10. package/components/form-core/test/FormControlMixin.test.js +1 -0
  11. package/components/form-core/test/validate/Validator.test.js +2 -0
  12. package/components/form-core/test-suites/ValidateMixin.suite.js +2 -0
  13. package/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.js +1 -0
  14. package/components/input-datepicker/src/localizeNamespaceLoader.js +1 -0
  15. package/components/input-datepicker/test/lion-input-datepicker.test.js +5 -6
  16. package/components/localize/test/date/formatDate.test.js +1 -0
  17. package/components/menu/CHANGELOG.md +37 -0
  18. package/components/menu/README.md +335 -0
  19. package/components/menu/index.js +2 -0
  20. package/components/menu/lion-menu-overlay.js +3 -0
  21. package/components/menu/lion-menu.js +3 -0
  22. package/components/menu/lion-spinbutton.js +3 -0
  23. package/components/menu/lion-toolbar.js +3 -0
  24. package/components/menu/lion-tree.js +3 -0
  25. package/components/menu/package.json +46 -0
  26. package/components/menu/requirements.temp.md +12 -0
  27. package/components/menu/src/InteractiveListMixin.js +805 -0
  28. package/components/menu/src/LionMenu.js +250 -0
  29. package/components/menu/src/LionMenuHybrid.js +46 -0
  30. package/components/menu/src/LionMenuOverlay.js +155 -0
  31. package/components/menu/src/LionSpinbutton.js +213 -0
  32. package/components/menu/src/LionToolbar.js +94 -0
  33. package/components/menu/src/LionTree.js +52 -0
  34. package/components/menu/src/MoreButtonMenuMixin.js +297 -0
  35. package/components/menu/src/MultiLevelListMixin.js +274 -0
  36. package/components/menu/src/utils/isInView.js +26 -0
  37. package/components/menu/src/utils/listItemInteractions.js +104 -0
  38. package/components/menu/src/utils/uuid.js +3 -0
  39. package/components/menu/test/lion-menu.test.js +3 -0
  40. package/components/menu/test-suites/InteractiveListMixin.suite.js +142 -0
  41. package/components/menu/types/InteractiveListMixinTypes.d.ts +82 -0
  42. package/components/navigation-bar/README.md +1 -0
  43. package/components/navigation-bar/src/LionNavigationBar.js +439 -0
  44. package/components/navigation-bar/src/types.js +41 -0
  45. package/components/overlays/NEXT.md +49 -0
  46. package/components/overlays/src/OverlayController.js +244 -111
  47. package/components/overlays/src/OverlayMixin.js +58 -11
  48. package/components/overlays/src/OverlaysManager.js +2 -2
  49. package/components/overlays/src/utils/adopt-styles.js +3 -2
  50. package/components/overlays/src/utils/get-focusable-elements.js +4 -0
  51. package/components/overlays/src/utils/is-equal-config.js +10 -6
  52. package/components/overlays/test/OverlayController.test.js +5 -3
  53. package/components/overlays/test/utils-tests/is-equal-config.test.js +1 -1
  54. package/components/overlays/test-suites/OverlayMixin.suite.js +8 -8
  55. package/components/overlays/types/OverlayConfig.ts +16 -5
  56. package/components/select-rich/test/lion-select-rich.test.js +3 -0
  57. package/custom-elements.json +32144 -19531
  58. package/dist-types/components/button/src/LionButton.d.ts +1 -0
  59. package/dist-types/components/button/src/LionButton.d.ts.map +1 -1
  60. package/dist-types/components/button/test-suites/LionButton.suite.d.ts.map +1 -1
  61. package/dist-types/components/calendar/src/LionCalendar.d.ts.map +1 -1
  62. package/dist-types/components/collapsible/src/DisclosureMixin.d.ts +3 -0
  63. package/dist-types/components/collapsible/src/DisclosureMixin.d.ts.map +1 -0
  64. package/dist-types/components/collapsible/types/DisclosureMixinTypes.d.ts +24 -0
  65. package/dist-types/components/collapsible/types/DisclosureMixinTypes.d.ts.map +1 -0
  66. package/dist-types/components/dialog/src/LionDialog.d.ts +3 -1
  67. package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
  68. package/dist-types/components/form-core/src/form-group/FormGroupMixin.d.ts.map +1 -1
  69. package/dist-types/components/form-core/src/registration/FormRegistrarMixin.d.ts.map +1 -1
  70. package/dist-types/components/form-core/test-suites/ValidateMixin.suite.d.ts.map +1 -1
  71. package/dist-types/components/form-core/test-suites/ValidateMixinFeedbackPart.suite.d.ts.map +1 -1
  72. package/dist-types/components/input-datepicker/src/localizeNamespaceLoader.d.ts.map +1 -1
  73. package/dist-types/components/menu/index.d.ts +3 -0
  74. package/dist-types/components/menu/index.d.ts.map +1 -0
  75. package/dist-types/components/menu/lion-menu-overlay.d.ts +2 -0
  76. package/dist-types/components/menu/lion-menu-overlay.d.ts.map +1 -0
  77. package/dist-types/components/menu/lion-menu.d.ts +2 -0
  78. package/dist-types/components/menu/lion-menu.d.ts.map +1 -0
  79. package/dist-types/components/menu/lion-spinbutton.d.ts +2 -0
  80. package/dist-types/components/menu/lion-spinbutton.d.ts.map +1 -0
  81. package/dist-types/components/menu/lion-toolbar.d.ts +2 -0
  82. package/dist-types/components/menu/lion-toolbar.d.ts.map +1 -0
  83. package/dist-types/components/menu/lion-tree.d.ts +2 -0
  84. package/dist-types/components/menu/lion-tree.d.ts.map +1 -0
  85. package/dist-types/components/menu/src/InteractiveListMixin.d.ts +611 -0
  86. package/dist-types/components/menu/src/InteractiveListMixin.d.ts.map +1 -0
  87. package/dist-types/components/menu/src/LionMenu.d.ts +563 -0
  88. package/dist-types/components/menu/src/LionMenu.d.ts.map +1 -0
  89. package/dist-types/components/menu/src/LionMenuHybrid.d.ts +19 -0
  90. package/dist-types/components/menu/src/LionMenuHybrid.d.ts.map +1 -0
  91. package/dist-types/components/menu/src/LionMenuOverlay.d.ts +1007 -0
  92. package/dist-types/components/menu/src/LionMenuOverlay.d.ts.map +1 -0
  93. package/dist-types/components/menu/src/LionSpinbutton.d.ts +58 -0
  94. package/dist-types/components/menu/src/LionSpinbutton.d.ts.map +1 -0
  95. package/dist-types/components/menu/src/LionToolbar.d.ts +8 -0
  96. package/dist-types/components/menu/src/LionToolbar.d.ts.map +1 -0
  97. package/dist-types/components/menu/src/LionTree.d.ts +445 -0
  98. package/dist-types/components/menu/src/LionTree.d.ts.map +1 -0
  99. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts +31 -0
  100. package/dist-types/components/menu/src/MoreButtonMenuMixin.d.ts.map +1 -0
  101. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts +493 -0
  102. package/dist-types/components/menu/src/MultiLevelListMixin.d.ts.map +1 -0
  103. package/dist-types/components/menu/src/utils/isInView.d.ts +9 -0
  104. package/dist-types/components/menu/src/utils/isInView.d.ts.map +1 -0
  105. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts +42 -0
  106. package/dist-types/components/menu/src/utils/listItemInteractions.d.ts.map +1 -0
  107. package/dist-types/components/menu/src/utils/uuid.d.ts +2 -0
  108. package/dist-types/components/menu/src/utils/uuid.d.ts.map +1 -0
  109. package/dist-types/components/menu/test/lion-menu.test.d.ts +2 -0
  110. package/dist-types/components/menu/test/lion-menu.test.d.ts.map +1 -0
  111. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts +13 -0
  112. package/dist-types/components/menu/test-suites/InteractiveListMixin.suite.d.ts.map +1 -0
  113. package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts +238 -0
  114. package/dist-types/components/navigation-bar/src/LionNavigationBar.d.ts.map +1 -0
  115. package/dist-types/components/navigation-bar/src/types.d.ts +85 -0
  116. package/dist-types/components/navigation-bar/src/types.d.ts.map +1 -0
  117. package/dist-types/components/overlays/src/OverlayController.d.ts +9 -13
  118. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  119. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  120. package/dist-types/components/overlays/src/utils/get-focusable-elements.d.ts.map +1 -1
  121. package/dist-types/components/overlays/src/utils/is-equal-config.d.ts.map +1 -1
  122. package/dist-types/components/overlays/types/OverlayConfig.d.ts +10 -2
  123. package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
  124. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +8 -2
  125. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  126. package/dist-types/exports/define/lion-item.d.ts +2 -0
  127. package/dist-types/exports/define/lion-item.d.ts.map +1 -0
  128. package/dist-types/exports/define/lion-menu-overlay.d.ts +2 -0
  129. package/dist-types/exports/define/lion-menu-overlay.d.ts.map +1 -0
  130. package/dist-types/exports/define/lion-menu.d.ts +2 -0
  131. package/dist-types/exports/define/lion-menu.d.ts.map +1 -0
  132. package/dist-types/exports/define/lion-navigation-bar.d.ts +2 -0
  133. package/dist-types/exports/define/lion-navigation-bar.d.ts.map +1 -0
  134. package/dist-types/exports/define/lion-toolbar.d.ts +2 -0
  135. package/dist-types/exports/define/lion-toolbar.d.ts.map +1 -0
  136. package/dist-types/exports/define/lion-tree.d.ts +2 -0
  137. package/dist-types/exports/define/lion-tree.d.ts.map +1 -0
  138. package/dist-types/exports/menu.d.ts +7 -0
  139. package/dist-types/exports/menu.d.ts.map +1 -0
  140. package/dist-types/exports/navigation-bar.d.ts +2 -0
  141. package/dist-types/exports/navigation-bar.d.ts.map +1 -0
  142. package/dist-types/exports/overlays.d.ts +3 -0
  143. package/dist-types/exports/overlays.d.ts.map +1 -1
  144. package/dist-types/tsconfig.tsbuildinfo +1 -1
  145. package/docs/components/checkbox-group/overview.md +2 -2
  146. package/docs/components/collapsible/overview.md +1 -1
  147. package/docs/components/combobox/api-table.md +21 -8
  148. package/docs/components/combobox/overview.md +1 -1
  149. package/docs/components/combobox/use-cases.md +1 -1
  150. package/docs/components/dialog/api-table.md +31 -6
  151. package/docs/components/dialog/overview.md +1 -1
  152. package/docs/components/fieldset/overview.md +5 -5
  153. package/docs/components/form/overview.md +3 -3
  154. package/docs/components/form/use-cases.md +2 -2
  155. package/docs/components/icon/use-cases.md +1 -1
  156. package/docs/components/input/overview.md +1 -1
  157. package/docs/components/input-amount/overview.md +3 -3
  158. package/docs/components/input-date/overview.md +3 -3
  159. package/docs/components/input-datepicker/api-table.md +21 -4
  160. package/docs/components/input-datepicker/overview.md +5 -5
  161. package/docs/components/input-email/overview.md +2 -2
  162. package/docs/components/input-file/overview.md +1 -1
  163. package/docs/components/input-iban/overview.md +2 -2
  164. package/docs/components/input-range/overview.md +2 -2
  165. package/docs/components/input-stepper/overview.md +1 -1
  166. package/docs/components/input-tel/overview.md +1 -1
  167. package/docs/components/input-tel/use-cases.md +6 -6
  168. package/docs/components/input-tel-dropdown/overview.md +1 -1
  169. package/docs/components/listbox/use-cases.md +1 -1
  170. package/docs/components/menu/api-table.md +450 -0
  171. package/docs/components/menu/applyMenuDemoStyles.js +173 -0
  172. package/docs/components/menu/extensions.md +235 -0
  173. package/docs/components/menu/index.md +13 -0
  174. package/docs/components/menu/overview.md +422 -0
  175. package/docs/components/radio-group/overview.md +3 -3
  176. package/docs/components/select/use-cases.md +1 -1
  177. package/docs/components/select-rich/api-table.md +21 -5
  178. package/docs/components/select-rich/use-cases.md +2 -2
  179. package/docs/components/tabs/examples.md +1 -1
  180. package/docs/components/tooltip/api-table.md +36 -11
  181. package/docs/fundamentals/index.md +4 -4
  182. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  183. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  184. package/docs/fundamentals/rationales/accessibility.md +1 -1
  185. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  186. package/docs/fundamentals/systems/core/overview.md +1 -1
  187. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  188. package/docs/fundamentals/systems/form/model-value.md +1 -1
  189. package/docs/fundamentals/systems/form/overview.md +28 -28
  190. package/docs/fundamentals/systems/form/validate.md +2 -2
  191. package/docs/fundamentals/systems/localize/overview.md +4 -4
  192. package/docs/fundamentals/systems/overlays/api-table.md +34 -34
  193. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  194. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  195. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  196. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  197. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  198. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  199. package/docs/guides/how-to/create-a-form.md +4 -4
  200. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  201. package/docs/guides/how-to/index.md +1 -1
  202. package/docs/guides/index.md +8 -8
  203. package/docs/guides/principles/index.md +5 -5
  204. package/docs/guides/principles/styling.md +1 -1
  205. package/exports/collapsible.js +1 -0
  206. package/exports/define/lion-item.js +3 -0
  207. package/exports/define/lion-menu-overlay.js +3 -0
  208. package/exports/define/lion-menu.js +3 -0
  209. package/exports/define/lion-navigation-bar.js +3 -0
  210. package/exports/define/lion-toolbar.js +3 -0
  211. package/exports/define/lion-tree.js +3 -0
  212. package/exports/menu.js +7 -0
  213. package/exports/navigation-bar.js +1 -0
  214. package/exports/overlays.js +6 -0
  215. package/menu.js +7 -0
  216. package/navigation-bar.js +1 -0
  217. package/overlays.js +6 -0
  218. package/package.json +1 -1
@@ -3,6 +3,7 @@ import { deepContains } from './utils/deep-contains.js';
3
3
  import { overlayShadowDomStyle } from './overlayShadowDomStyle.js';
4
4
  import { _adoptStyleUtils } from './utils/adopt-styles.js';
5
5
  import { getFocusableElements } from './utils/get-focusable-elements.js';
6
+ import { isEqualConfig } from './utils/is-equal-config.js';
6
7
 
7
8
  /**
8
9
  * @typedef {import('@lion/ui/types/overlays.js').OverlayPhase} OverlayPhase
@@ -32,27 +33,37 @@ import { getFocusableElements } from './utils/get-focusable-elements.js';
32
33
  * `<slot name="my-content">` belonging to `<div slot="content"/>` will be wrapped with wrappingDialogNodeL1 and contentWrapperNodeL2
33
34
  * inside shadow dom. With the help of temp markers, `<slot name="my-content">`'s original position will be respected.
34
35
  *
35
- * @param {{ wrappingDialogNodeL1:HTMLDialogElement|HTMLDivElement; contentWrapperNodeL2:Element; contentNodeL3: Element }} opts
36
+ * @param {{
37
+ * wrappingDialogNodeL1: HTMLDialogElement|HTMLDivElement;
38
+ * contentWrapperNodeL2:Element;
39
+ * contentNodeL3: Element;
40
+ * }} opts
36
41
  */
37
42
  function rearrangeNodes({ wrappingDialogNodeL1, contentWrapperNodeL2, contentNodeL3 }) {
38
- if (!(contentWrapperNodeL2.isConnected || contentNodeL3.isConnected)) {
43
+ // if contentWrapperNode is provided by the user,
44
+ // we assume it lives in shadow dom around a slot.
45
+ const hasLegacyMethodOfProvidingWrapperNode = Boolean(contentWrapperNodeL2.isConnected);
46
+ // We could be initialized via a directive (in offline dom). It's important that we know about the parents,
47
+ // as we cannot deal with single content nodes
48
+ const hasContentNodeAttachmentPoints = Boolean(contentNodeL3.parentNode);
49
+
50
+ if (!hasLegacyMethodOfProvidingWrapperNode && !hasContentNodeAttachmentPoints) {
39
51
  throw new Error(
40
- '[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.',
52
+ '[OverlayController] Could not find a render target, makes sure contentNode has a parent element (or contentWrapperNode is connected)',
41
53
  );
42
54
  }
43
55
 
56
+ /** @type {Node} */
44
57
  let parentElement;
45
- const tempMarker = document.createComment('tempMarker');
58
+ const tempMarker = document.createComment('overlay-insertion-marker');
46
59
 
47
- if (contentWrapperNodeL2.isConnected) {
60
+ if (hasLegacyMethodOfProvidingWrapperNode) {
48
61
  // This is the case when contentWrapperNode (living in shadow dom, wrapping <slot name="my-content-outlet">) is already provided via controller.
49
62
  parentElement = contentWrapperNodeL2.parentElement || contentWrapperNodeL2.getRootNode();
50
63
  parentElement.insertBefore(tempMarker, contentWrapperNodeL2);
51
64
  // Wrap...
52
65
  wrappingDialogNodeL1.appendChild(contentWrapperNodeL2);
53
- }
54
- // if contentNodeL3.isConnected
55
- else {
66
+ } else {
56
67
  const contentIsProjected = contentNodeL3.assignedSlot;
57
68
  if (contentIsProjected) {
58
69
  parentElement =
@@ -93,7 +104,22 @@ function rearrangeNodes({ wrappingDialogNodeL1, contentWrapperNodeL2, contentNod
93
104
  * ```
94
105
  */
95
106
  parentElement.insertBefore(wrappingDialogNodeL1, tempMarker);
96
- parentElement?.removeChild(tempMarker);
107
+
108
+ return function cleanup() {
109
+ if (hasLegacyMethodOfProvidingWrapperNode) {
110
+ parentElement?.insertBefore(contentWrapperNodeL2, tempMarker);
111
+ } else {
112
+ parentElement?.insertBefore(contentNodeL3, tempMarker);
113
+ if (parentElement.contains(contentWrapperNodeL2)) {
114
+ contentWrapperNodeL2.remove();
115
+ }
116
+ }
117
+ parentElement?.removeChild(tempMarker);
118
+
119
+ if (parentElement.contains(wrappingDialogNodeL1)) {
120
+ parentElement?.removeChild(wrappingDialogNodeL1);
121
+ }
122
+ };
97
123
  }
98
124
 
99
125
  /**
@@ -106,6 +132,21 @@ async function preloadPopper() {
106
132
 
107
133
  const childDialogsClosedInEventLoopWeakmap = new WeakMap();
108
134
 
135
+ /**
136
+ * @param {HTMLElement | null} el
137
+ * @param {string} selector
138
+ * @returns {HTMLElement | null}
139
+ */
140
+ function deepClosest(el, selector) {
141
+ // @ts-ignore - comparing HTMLElement to document/window for safety
142
+ return (
143
+ // @ts-ignore - type comparison check
144
+ (el && el !== document && el !== window && el.closest(selector)) ||
145
+ // @ts-ignore - host property on ShadowRoot
146
+ deepClosest(el.getRootNode().host, selector)
147
+ );
148
+ }
149
+
109
150
  /**
110
151
  * OverlayController is the fundament for every single type of overlay. With the right
111
152
  * configuration, it can be used to build (modal) dialogs, tooltips, dropdowns, popovers,
@@ -196,10 +237,21 @@ export class OverlayController extends EventTarget {
196
237
  placement: 'center',
197
238
  },
198
239
  zIndex: 9999,
240
+ // TODO: rename to isActivated
241
+ isOpenable: true,
242
+ focusContentOnOpen: false,
243
+ // This means that content will have visually-hidden / sr-only styles. This is handy for:
244
+ // - hidden menus that should be "indexable" by screen readers (like the Links list in VO: https://support.apple.com/en-gb/guide/voiceover/mchlp2719/mac)
245
+ // - "more" menus that put content in a dropdown that do not fit in the current row
246
+ // - menus that should generally open on tab.
247
+ // @ts-ignore - hideVisually is an extension property
248
+ hideVisually: false,
249
+ requireConnectedNodes: true,
199
250
  };
200
251
 
201
252
  /** @protected */
202
253
  this._contentId = `overlay-content--${Math.random().toString(36).slice(2, 10)}`;
254
+
203
255
  /** @private */
204
256
  this.__originalAttrs = new Map();
205
257
  /** @private */
@@ -220,6 +272,8 @@ export class OverlayController extends EventTarget {
220
272
  this.__escKeyHandlerCalled = false;
221
273
  }
222
274
 
275
+ #hasSetup = false;
276
+
223
277
  /**
224
278
  * The invokerNode
225
279
  * @type {HTMLElement | undefined}
@@ -238,7 +292,7 @@ export class OverlayController extends EventTarget {
238
292
 
239
293
  /**
240
294
  * Determines the connection point in DOM (body vs next to invoker).
241
- * @type {'global' | 'local' | undefined}
295
+ * @type {'global' | 'local' | 'custom' | undefined}
242
296
  */
243
297
  get placementMode() {
244
298
  return this.config?.placementMode;
@@ -463,17 +517,14 @@ export class OverlayController extends EventTarget {
463
517
  * @param { OverlayConfig } cfgToAdd
464
518
  */
465
519
  updateConfig(cfgToAdd) {
466
- // Teardown all previous configs
467
- this.teardown();
468
-
469
520
  /**
470
521
  * @type {OverlayConfig}
471
522
  * @private
472
523
  */
473
- this.__prevConfig = this.config;
524
+ const prevConfig = this.config;
474
525
 
475
526
  /** @type {OverlayConfig} */
476
- this.config = {
527
+ const newConfig = {
477
528
  ...this._defaultConfig, // our basic ingredients
478
529
  ...this.__sharedConfig, // the initial configured overlayController
479
530
  ...cfgToAdd, // the added config
@@ -489,12 +540,18 @@ export class OverlayController extends EventTarget {
489
540
  },
490
541
  };
491
542
 
543
+ const shouldUpdate = !this.#hasSetup || !isEqualConfig(prevConfig, newConfig);
544
+ if (!shouldUpdate) return;
545
+ // Teardown all previous configs
546
+
547
+ this.teardown();
548
+
549
+ this.config = newConfig;
550
+
492
551
  /** @private */
493
552
  this.__validateConfiguration(this.config);
494
553
  /** @protected */
495
554
  this._init();
496
- /** @private */
497
- this.__elementToFocusAfterHide = undefined;
498
555
 
499
556
  if (!this.#isRegisteredOnManager()) {
500
557
  this.manager.add(this);
@@ -516,7 +573,7 @@ export class OverlayController extends EventTarget {
516
573
  '[OverlayController] You need to provide a .placementMode ("global"|"local")',
517
574
  );
518
575
  }
519
- if (!['global', 'local'].includes(newConfig.placementMode)) {
576
+ if (!['global', 'local', 'custom'].includes(newConfig.placementMode)) {
520
577
  throw new Error(
521
578
  `[OverlayController] "${newConfig.placementMode}" is not a valid .placementMode, use ("global"|"local")`,
522
579
  );
@@ -535,14 +592,23 @@ export class OverlayController extends EventTarget {
535
592
  * @protected
536
593
  */
537
594
  _init() {
538
- if (!this.__contentHasBeenInitialized) {
539
- this.__initContentDomStructure();
540
- this.__contentHasBeenInitialized = true;
541
- }
595
+ if (!this.config.isOpenable) return;
596
+
597
+ // TODO: should not be behind a flag when we are fully tearing down...
598
+ // TODO 2: When we move away from dialog (use popover to paint to top layer)
599
+ // and popper.js, (use popover and anchor positioning and popperjs as fallback, but we can put styles on contentNode),
600
+ // we dont need to create complex wrappers
601
+ // if (!this.__contentHasBeenInitialized) {
602
+ this.__initContentDomStructure();
603
+ // this.__contentHasBeenInitialized = true;
604
+ // }
605
+ this.__initVisibility();
542
606
 
543
607
  // Reset all positioning styles (local, c.q. Popper) and classes (global)
544
- this.contentWrapperNode.removeAttribute('style');
545
- this.contentWrapperNode.removeAttribute('class');
608
+ if (this.contentWrapperNode !== this.contentNode) {
609
+ this.contentWrapperNode.removeAttribute('style');
610
+ this.contentWrapperNode.removeAttribute('class');
611
+ }
546
612
 
547
613
  if (this.placementMode === 'local') {
548
614
  // Lazily load Popper as soon as the first local overlay is used...
@@ -552,8 +618,27 @@ export class OverlayController extends EventTarget {
552
618
  }
553
619
  this.__handleOverlayStyles({ phase: 'init' });
554
620
  this._handleFeatures({ phase: 'init' });
621
+
622
+ /** @private */
623
+ this.__elementToFocusAfterHide = undefined;
624
+
625
+ this.contentNode.dataset.content = '';
626
+
627
+ // Use event delegation to listen for clicks on close buttons...
628
+ this.contentNode.addEventListener('click', this.#hideOnCloseButtonClick);
629
+
630
+ this.#hasSetup = true;
555
631
  }
556
632
 
633
+ #hideOnCloseButtonClick = (/** @type {{ target: any; }} */ ev) => {
634
+ const isOurCloseButton =
635
+ ev.target.hasAttribute('data-close') &&
636
+ deepClosest(ev.target, '[data-content]') === this.contentNode;
637
+ if (!isOurCloseButton) return;
638
+
639
+ this.hide();
640
+ };
641
+
557
642
  /**
558
643
  * @param {{ phase: OverlayPhase }} config
559
644
  * @private
@@ -594,84 +679,108 @@ export class OverlayController extends EventTarget {
594
679
  * @private
595
680
  */
596
681
  __initContentDomStructure() {
597
- const wrappingDialogElement = document.createElement('dialog');
598
- // We use a dialog for its visual capabilities: it renders to the top layer.
599
- // A11y will depend on the type of overlay and is arranged on contentNode level.
600
- // Also see: https://www.scottohara.me/blog/2019/03/05/open-dialog.html
601
- //
602
- // The role="dialog" is set on the contentNode (or another role), so role="none"
603
- // is valid here, although AXE complains about this setup.
604
- // For now we need to add `ignoredRules: ['aria-allowed-role']` in your AXE tests.
605
- // see: https://lion.js.org/fundamentals/systems/overlays/rationale/#considerations
606
- wrappingDialogElement.setAttribute('role', 'none');
607
- wrappingDialogElement.setAttribute('data-overlay-outer-wrapper', '');
608
- // N.B. position: fixed is needed to escape out of 'overflow: hidden'
609
- // We give a high z-index for non-modal dialogs, so that we at least win from all siblings of our
610
- // parent stacking context
611
- // padding reset so we don't get a weird dialog visual square showing up
612
- wrappingDialogElement.style.cssText = `display:none; z-index: ${this.config.zIndex}; padding: 0;`;
613
- this.__wrappingDialogNode = wrappingDialogElement;
682
+ if (this.placementMode !== 'custom') {
683
+ const wrappingDialogElement = document.createElement('dialog');
684
+ // We use a dialog for its visual capabilities: it renders to the top layer.
685
+ // A11y will depend on the type of overlay and is arranged on contentNode level.
686
+ // Also see: https://www.scottohara.me/blog/2019/03/05/open-dialog.html
687
+ //
688
+ // The role="dialog" is set on the contentNode (or another role), so role="none"
689
+ // is valid here, although AXE complains about this setup.
690
+ // For now we need to add `ignoredRules: ['aria-allowed-role']` in your AXE tests.
691
+ // see: https://lion.js.org/fundamentals/systems/overlays/rationale/#considerations
692
+ wrappingDialogElement.setAttribute('role', 'none');
693
+ wrappingDialogElement.setAttribute('data-overlay-outer-wrapper', '');
694
+ // N.B. position: fixed is needed to escape out of 'overflow: hidden'
695
+ // We give a high z-index for non-modal dialogs, so that we at least win from all siblings of our
696
+ // parent stacking context
697
+ // padding reset so we don't get a weird dialog visual square showing up
698
+ wrappingDialogElement.style.cssText = `display:none; z-index: ${this.config.zIndex}; padding: 0;`;
699
+ // @ts-ignore - 'custom' is a valid extension of placementMode
700
+ if (this.config.placementMode === 'custom') {
701
+ // The user should have full freedom to control the content node, so its wrapper nodes should remain neutral.
702
+ wrappingDialogElement.style.cssText += `position: static;`;
703
+ }
704
+ this.__wrappingDialogNode = wrappingDialogElement;
614
705
 
615
- /**
616
- * Based on the configuration of the developer, multiple scenarios are accounted for
617
- * A. We already have a contentWrapperNode ()
618
- */
619
- if (!this.config?.contentWrapperNode) {
620
- this.__contentWrapperNode = document.createElement('div');
621
- }
622
- this.contentWrapperNode.setAttribute('data-id', 'content-wrapper');
623
- // 'hack' that makes sure popperjs (that is applied one level lower) works correctly in deeply nested shadow roots
624
- this.contentWrapperNode.style.transform = 'translateZ(0px)';
706
+ /**
707
+ * Based on the configuration of the developer, multiple scenarios are accounted for
708
+ * A. We already have a contentWrapperNode ()
709
+ */
710
+ if (!this.config?.contentWrapperNode) {
711
+ this.__contentWrapperNode = document.createElement('div');
712
+ }
713
+ this.contentWrapperNode.setAttribute('data-id', 'content-wrapper');
714
+ // 'hack' that makes sure popperjs (that is applied one level lower) works correctly in deeply nested shadow roots
715
+ this.contentWrapperNode.style.transform = 'translateZ(0px)';
716
+
717
+ this.__rearrangeNodesCleanup = rearrangeNodes({
718
+ wrappingDialogNodeL1: wrappingDialogElement,
719
+ contentWrapperNodeL2: this.contentWrapperNode,
720
+ contentNodeL3: this.contentNode,
721
+ // requireConnectedNodes: Boolean(this.config.requireConnectedNodes),
722
+ });
723
+ wrappingDialogElement.open = true;
625
724
 
626
- rearrangeNodes({
627
- wrappingDialogNodeL1: wrappingDialogElement,
628
- contentWrapperNodeL2: this.contentWrapperNode,
629
- contentNodeL3: this.contentNode,
630
- });
631
- wrappingDialogElement.open = true;
725
+ if (this.isTooltip) {
726
+ // needed to prevent tooltip getting focus in Safari and Firefox
727
+ wrappingDialogElement.setAttribute('tabindex', '-1');
728
+ }
729
+ this.contentWrapperNode.style.zIndex = '1';
730
+ if (getComputedStyle(this.contentNode).position === 'absolute') {
731
+ // Having a _contWrapperNode and a contentNode with 'position:absolute' results in
732
+ // computed height of 0...
733
+ this.contentNode.style.position = 'static';
734
+ }
632
735
 
633
- if (this.isTooltip) {
634
- // needed to prevent tooltip getting focus in Safari and Firefox
635
- wrappingDialogElement.setAttribute('tabindex', '-1');
736
+ // Here we prevent any interference of the native <dialog> element with the keyboard behavior
737
+ // as defined by the OverlayController. This is needed until we can configure `closedby="none"`
738
+ // on the native dialog for all browsers: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#closedby
739
+ const hasClosedBySupport = HTMLDialogElement && 'closedBy' in HTMLDialogElement.prototype;
740
+ if (hasClosedBySupport) {
741
+ // @ts-ignore - closedBy is a newer property not in all TypeScript versions
742
+ wrappingDialogElement.closedBy = 'none';
743
+ } else {
744
+ wrappingDialogElement.addEventListener(
745
+ 'keydown',
746
+ (/** @type {* & KeyboardEvent} */ event) => {
747
+ if (event.key === 'Escape') {
748
+ event.preventDefault();
749
+ }
750
+ },
751
+ );
752
+ wrappingDialogElement.addEventListener(
753
+ 'keyup',
754
+ (/** @type {* & KeyboardEvent} */ event) => {
755
+ if (event.key === 'Escape') {
756
+ event.preventDefault();
757
+ }
758
+ },
759
+ );
760
+ wrappingDialogElement.addEventListener('cancel', event => {
761
+ event.stopPropagation();
762
+ });
763
+ wrappingDialogElement.addEventListener('close', event => {
764
+ event.stopPropagation();
765
+ });
766
+ }
767
+ } else {
768
+ // quick hack to make none-popper/global flows work...
769
+ this.__contentWrapperNode = this.contentNode;
770
+ this.__wrappingDialogNode = this.contentNode;
636
771
  }
772
+ }
637
773
 
638
- this.__wrappingDialogNode.style.display = 'none';
639
- this.contentWrapperNode.style.zIndex = '1';
640
-
641
- if (getComputedStyle(this.contentNode).position === 'absolute') {
642
- // Having a _contWrapperNode and a contentNode with 'position:absolute' results in
643
- // computed height of 0...
644
- this.contentNode.style.position = 'static';
645
- }
774
+ __initVisibility() {
775
+ // TODO: allow defaultOpen?
776
+ /** @type {HTMLDialogElement} */ (this.__wrappingDialogNode).style.display = 'none';
777
+ }
646
778
 
647
- // Here we prevent any interference of the native <dialog> element with the keyboard behavior
648
- // as defined by the OverlayController. This is needed until we can configure `closedby="none"`
649
- // on the native dialog for all browsers: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#closedby
650
- const hasClosedBySupport = HTMLDialogElement && 'closedBy' in HTMLDialogElement.prototype;
651
- if (hasClosedBySupport) {
652
- // @ts-expect-error
653
- wrappingDialogElement.closedBy = 'none';
654
- } else {
655
- wrappingDialogElement.addEventListener(
656
- 'keydown',
657
- (/** @type {* & KeyboardEvent} */ event) => {
658
- if (event.key === 'Escape') {
659
- event.preventDefault();
660
- }
661
- },
662
- );
663
- wrappingDialogElement.addEventListener('keyup', (/** @type {* & KeyboardEvent} */ event) => {
664
- if (event.key === 'Escape') {
665
- event.preventDefault();
666
- }
667
- });
668
- wrappingDialogElement.addEventListener('cancel', event => {
669
- event.stopPropagation();
670
- });
671
- wrappingDialogElement.addEventListener('close', event => {
672
- event.stopPropagation();
673
- });
674
- }
779
+ // Clean up the DOM structure, leaving it as we found it.
780
+ // TODO: __teardownContentDomStructure method... use Resettable (rename ro Restorable) found in VisibilitToggleCtrl
781
+ __teardownVisibility() {
782
+ // TODO: full structure. For now we just reset display prop...
783
+ /** @type {HTMLDialogElement} */ (this.__wrappingDialogNode).style.display = '';
675
784
  }
676
785
 
677
786
  /**
@@ -742,6 +851,7 @@ export class OverlayController extends EventTarget {
742
851
  * @private
743
852
  */
744
853
  __storeOriginalAttrs(node, attrs) {
854
+ /** @type {Record<string, string | null>} */
745
855
  const attrMap = {};
746
856
  attrs.forEach(attrName => {
747
857
  attrMap[attrName] = node.getAttribute(attrName);
@@ -798,6 +908,7 @@ export class OverlayController extends EventTarget {
798
908
  if ('HTMLDialogElement' in window && this.__wrappingDialogNode instanceof HTMLDialogElement) {
799
909
  this.__wrappingDialogNode.open = true;
800
910
  }
911
+
801
912
  // @ts-ignore
802
913
  this.__wrappingDialogNode.style.display = '';
803
914
  this._keepBodySize({ phase: 'before-show' });
@@ -811,6 +922,14 @@ export class OverlayController extends EventTarget {
811
922
  contentNode: this.contentNode,
812
923
  });
813
924
  }
925
+
926
+ // a styling/debug hook for open state
927
+ this.contentNode.setAttribute('data-open', '');
928
+
929
+ if (this.config.focusContentOnOpen) {
930
+ this.contentNode.focus();
931
+ }
932
+
814
933
  /** @type {function} */
815
934
  (this._showResolve)();
816
935
  }
@@ -843,6 +962,7 @@ export class OverlayController extends EventTarget {
843
962
  await this.__createPopperInstance();
844
963
  this._popper.forceUpdate();
845
964
  }
965
+ // N.B. leave it up to the user if placementMode === 'custom'
846
966
  }
847
967
 
848
968
  /**
@@ -850,9 +970,7 @@ export class OverlayController extends EventTarget {
850
970
  * @protected
851
971
  */
852
972
  _keepBodySize({ phase }) {
853
- if (!this.preventsScroll) {
854
- return;
855
- }
973
+ if (!this.preventsScroll) return;
856
974
 
857
975
  this.manager.requestToKeepBodySize({ phase });
858
976
  }
@@ -862,6 +980,9 @@ export class OverlayController extends EventTarget {
862
980
  * @event hide right after the overlay is hidden
863
981
  */
864
982
  async hide() {
983
+ // Function like a no-op for dynamic edge cases...
984
+ if (!this.config.isOpenable) return;
985
+
865
986
  this._hideComplete = new Promise(resolve => {
866
987
  this._hideResolve = resolve;
867
988
  });
@@ -894,7 +1015,6 @@ export class OverlayController extends EventTarget {
894
1015
  if ('HTMLDialogElement' in window && this.__wrappingDialogNode instanceof HTMLDialogElement) {
895
1016
  this.__wrappingDialogNode.close();
896
1017
  }
897
-
898
1018
  // @ts-ignore
899
1019
  this.__wrappingDialogNode.style.display = 'none';
900
1020
  this._handleFeatures({ phase: 'hide' });
@@ -903,6 +1023,8 @@ export class OverlayController extends EventTarget {
903
1023
  this._restoreFocus();
904
1024
  }
905
1025
  /** @type {function} */ (this._hideResolve)();
1026
+
1027
+ this.contentNode.removeAttribute('data-open');
906
1028
  }
907
1029
 
908
1030
  /**
@@ -1013,23 +1135,24 @@ export class OverlayController extends EventTarget {
1013
1135
  if (this.visibilityTriggerFunction) {
1014
1136
  this._handleVisibilityTriggers({ phase });
1015
1137
  }
1138
+
1139
+ if (phase === 'init' && this.config.focusContentOnOpen) {
1140
+ this.contentNode?.setAttribute('tabindex', '-1');
1141
+ }
1142
+
1143
+ if (phase === 'teardown' && this.config.focusContentOnOpen) {
1144
+ // TODO: capture initial attr, use Resettable mechanism of VisibilityToggleCtrl in whole Controller
1145
+ this.contentNode?.removeAttribute('tabindex');
1146
+ }
1016
1147
  }
1017
1148
 
1018
1149
  /**
1019
1150
  * @param {{ phase: OverlayPhase }} config
1020
1151
  */
1021
1152
  _handleVisibilityTriggers({ phase }) {
1022
- if (typeof this.visibilityTriggerFunction === 'function') {
1023
- if (phase === 'init') {
1024
- this.__visibilityTriggerHandler = this.visibilityTriggerFunction({
1025
- phase,
1026
- controller: this,
1027
- });
1028
- }
1029
- if (this.__visibilityTriggerHandler[phase]) {
1030
- this.__visibilityTriggerHandler[phase]();
1031
- }
1032
- }
1153
+ if (typeof this.visibilityTriggerFunction !== 'function') return;
1154
+
1155
+ this.visibilityTriggerFunction({ controller: this })?.[phase]?.();
1033
1156
  }
1034
1157
 
1035
1158
  /**
@@ -1186,7 +1309,9 @@ export class OverlayController extends EventTarget {
1186
1309
  if (phase === 'show') {
1187
1310
  this.#handleShiftKeyPress();
1188
1311
  this.#handleFocusInsideDialog();
1312
+ // @ts-ignore - HTMLDialogElement methods
1189
1313
  this.__wrappingDialogNode?.close();
1314
+ // @ts-ignore - HTMLDialogElement methods
1190
1315
  this.__wrappingDialogNode?.showModal();
1191
1316
  /**
1192
1317
  * At this moment `#handleFocusInsideDialog` should handle the focus.
@@ -1437,6 +1562,7 @@ export class OverlayController extends EventTarget {
1437
1562
  }
1438
1563
 
1439
1564
  teardown() {
1565
+ if (!this.#hasSetup) return;
1440
1566
  this.__handleOverlayStyles({ phase: 'teardown' });
1441
1567
  if (this.isShown) {
1442
1568
  this._keepBodySize({ phase: 'teardown' });
@@ -1445,6 +1571,13 @@ export class OverlayController extends EventTarget {
1445
1571
  if (this.#isRegisteredOnManager()) {
1446
1572
  this.manager.remove(this);
1447
1573
  }
1574
+
1575
+ this.contentNode?.removeEventListener('click', this.#hideOnCloseButtonClick);
1576
+
1577
+ this.__rearrangeNodesCleanup?.();
1578
+ this.__teardownVisibility();
1579
+
1580
+ this.#hasSetup = false;
1448
1581
  }
1449
1582
 
1450
1583
  /** @private */