@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
@@ -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,41 +33,45 @@ 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.parentNode);
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');
59
+ // Moving the wrapper can change slot assignment, so capture the node we relocate up front
60
+ // and restore that same node during cleanup.
61
+ const contentNodeOrSlot = contentNodeL3.assignedSlot || contentNodeL3;
46
62
 
47
- if (contentWrapperNodeL2.isConnected) {
63
+ if (hasLegacyMethodOfProvidingWrapperNode) {
48
64
  // This is the case when contentWrapperNode (living in shadow dom, wrapping <slot name="my-content-outlet">) is already provided via controller.
49
65
  parentElement = contentWrapperNodeL2.parentElement || contentWrapperNodeL2.getRootNode();
50
66
  parentElement.insertBefore(tempMarker, contentWrapperNodeL2);
51
67
  // Wrap...
52
68
  wrappingDialogNodeL1.appendChild(contentWrapperNodeL2);
53
- }
54
- // if contentNodeL3.isConnected
55
- else {
56
- const contentIsProjected = contentNodeL3.assignedSlot;
57
- if (contentIsProjected) {
58
- parentElement =
59
- contentNodeL3.assignedSlot.parentElement || contentNodeL3.assignedSlot.getRootNode();
60
- parentElement.insertBefore(tempMarker, contentNodeL3.assignedSlot);
61
- wrappingDialogNodeL1.appendChild(contentWrapperNodeL2);
62
- // Important: we do not move around contentNodeL3, but the assigned slot
63
- contentWrapperNodeL2.appendChild(contentNodeL3.assignedSlot);
64
- } else {
65
- parentElement = contentNodeL3.parentElement || contentNodeL3.getRootNode();
66
- parentElement.insertBefore(tempMarker, contentNodeL3);
67
- wrappingDialogNodeL1.appendChild(contentWrapperNodeL2);
68
- contentWrapperNodeL2.appendChild(contentNodeL3);
69
- }
69
+ } else {
70
+ parentElement = contentNodeOrSlot.parentElement || contentNodeOrSlot.getRootNode();
71
+ parentElement.insertBefore(tempMarker, contentNodeOrSlot);
72
+ wrappingDialogNodeL1.appendChild(contentWrapperNodeL2);
73
+ // Important: for projected content we do not move contentNodeL3, but its assigned slot
74
+ contentWrapperNodeL2.appendChild(contentNodeOrSlot);
70
75
  }
71
76
 
72
77
  /**
@@ -93,7 +98,22 @@ function rearrangeNodes({ wrappingDialogNodeL1, contentWrapperNodeL2, contentNod
93
98
  * ```
94
99
  */
95
100
  parentElement.insertBefore(wrappingDialogNodeL1, tempMarker);
96
- parentElement?.removeChild(tempMarker);
101
+
102
+ return function cleanup() {
103
+ if (hasLegacyMethodOfProvidingWrapperNode) {
104
+ parentElement?.insertBefore(contentWrapperNodeL2, tempMarker);
105
+ } else {
106
+ parentElement?.insertBefore(contentNodeOrSlot, tempMarker);
107
+ if (parentElement.contains(contentWrapperNodeL2)) {
108
+ contentWrapperNodeL2.remove();
109
+ }
110
+ }
111
+ parentElement?.removeChild(tempMarker);
112
+
113
+ if (parentElement.contains(wrappingDialogNodeL1)) {
114
+ parentElement?.removeChild(wrappingDialogNodeL1);
115
+ }
116
+ };
97
117
  }
98
118
 
99
119
  /**
@@ -106,6 +126,21 @@ async function preloadPopper() {
106
126
 
107
127
  const childDialogsClosedInEventLoopWeakmap = new WeakMap();
108
128
 
129
+ /**
130
+ * @param {HTMLElement | null} el
131
+ * @param {string} selector
132
+ * @returns {HTMLElement | null}
133
+ */
134
+ function deepClosest(el, selector) {
135
+ // @ts-ignore - comparing HTMLElement to document/window for safety
136
+ return (
137
+ // @ts-ignore - type comparison check
138
+ (el && el !== document && el !== window && el.closest(selector)) ||
139
+ // @ts-ignore - host property on ShadowRoot
140
+ deepClosest(el.getRootNode().host, selector)
141
+ );
142
+ }
143
+
109
144
  /**
110
145
  * OverlayController is the fundament for every single type of overlay. With the right
111
146
  * configuration, it can be used to build (modal) dialogs, tooltips, dropdowns, popovers,
@@ -236,6 +271,20 @@ export class OverlayController extends EventTarget {
236
271
  placement: 'center',
237
272
  },
238
273
  zIndex: 9999,
274
+ isActivated: true,
275
+ elementToFocusOnShow: false,
276
+ // This means that content will have visually-hidden / sr-only styles. This is handy for:
277
+ // - 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)
278
+ // - "more" menus that put content in a dropdown that do not fit in the current row
279
+ // - menus that should generally open on tab.
280
+ // @ts-ignore - hideVisually is an extension property
281
+ hideVisually: false,
282
+ requireConnectedNodes: true,
283
+
284
+ // hides children overlays when parent is closed...
285
+ syncChildrenCloseState: false,
286
+ // // In next major, we remove this prop. Now we disable it for backwards compatibility and enable it in the places we need it internally
287
+ // _shouldTeardownDomStructure: false,
239
288
  };
240
289
 
241
290
  this._contentId = `overlay-content--${Math.random().toString(36).slice(2, 10)}`;
@@ -249,6 +298,8 @@ export class OverlayController extends EventTarget {
249
298
  this.__escKeyHandlerCalled = false;
250
299
  }
251
300
 
301
+ __hasSetup = false;
302
+
252
303
  /**
253
304
  * The invokerNode
254
305
  * @type {HTMLElement | undefined}
@@ -267,7 +318,7 @@ export class OverlayController extends EventTarget {
267
318
 
268
319
  /**
269
320
  * Determines the connection point in DOM (body vs next to invoker).
270
- * @type {'global' | 'local' | undefined}
321
+ * @type {'global' | 'local' | 'custom' | undefined}
271
322
  */
272
323
  get placementMode() {
273
324
  return this.config?.placementMode;
@@ -492,17 +543,14 @@ export class OverlayController extends EventTarget {
492
543
  * @param { OverlayConfig } cfgToAdd
493
544
  */
494
545
  updateConfig(cfgToAdd) {
495
- // Teardown all previous configs
496
- this.teardown();
497
-
498
546
  /**
499
547
  * @type {OverlayConfig}
500
548
  * @private
501
549
  */
502
- this.__prevConfig = this.config;
550
+ const prevConfig = this.config;
503
551
 
504
552
  /** @type {OverlayConfig} */
505
- this.config = {
553
+ const newConfig = {
506
554
  ...this._defaultConfig, // our basic ingredients
507
555
  ...this.__sharedConfig, // the initial configured overlayController
508
556
  ...cfgToAdd, // the added config
@@ -518,12 +566,18 @@ export class OverlayController extends EventTarget {
518
566
  },
519
567
  };
520
568
 
569
+ const shouldUpdate = !this.__hasSetup || !isEqualConfig(prevConfig, newConfig);
570
+ if (!shouldUpdate) return;
571
+ // Teardown all previous configs
572
+
573
+ this.teardown();
574
+
575
+ this.config = newConfig;
576
+
521
577
  /** @private */
522
578
  this.__validateConfiguration(this.config);
523
579
  /** @protected */
524
580
  this._init();
525
- /** @private */
526
- this.__elementToFocusAfterHide = undefined;
527
581
 
528
582
  if (!this.#isRegisteredOnManager()) {
529
583
  this.manager.add(this);
@@ -545,7 +599,7 @@ export class OverlayController extends EventTarget {
545
599
  '[OverlayController] You need to provide a .placementMode ("global"|"local")',
546
600
  );
547
601
  }
548
- if (!['global', 'local'].includes(newConfig.placementMode)) {
602
+ if (!['global', 'local', 'custom'].includes(newConfig.placementMode)) {
549
603
  throw new Error(
550
604
  `[OverlayController] "${newConfig.placementMode}" is not a valid .placementMode, use ("global"|"local")`,
551
605
  );
@@ -564,14 +618,23 @@ export class OverlayController extends EventTarget {
564
618
  * @protected
565
619
  */
566
620
  _init() {
567
- if (!this.__contentHasBeenInitialized) {
568
- this.__initContentDomStructure();
569
- this.__contentHasBeenInitialized = true;
570
- }
621
+ if (!this.config.isActivated) return;
622
+
623
+ // TODO: should not be behind a flag when we are fully tearing down...
624
+ // TODO 2: When we move away from dialog (use popover to paint to top layer)
625
+ // and popper.js, (use popover and anchor positioning and popperjs as fallback, but we can put styles on contentNode),
626
+ // we dont need to create complex wrappers
627
+ // if (!this.__contentHasBeenInitialized) {
628
+ this.__initContentDomStructure();
629
+ // this.__contentHasBeenInitialized = true;
630
+ // }
631
+ this.__initVisibility();
571
632
 
572
633
  // Reset all positioning styles (local, c.q. Popper) and classes (global)
573
- this.contentWrapperNode.removeAttribute('style');
574
- this.contentWrapperNode.removeAttribute('class');
634
+ if (this.contentWrapperNode !== this.contentNode) {
635
+ this.contentWrapperNode.removeAttribute('style');
636
+ this.contentWrapperNode.removeAttribute('class');
637
+ }
575
638
 
576
639
  if (this.placementMode === 'local') {
577
640
  // Lazily load Popper as soon as the first local overlay is used...
@@ -581,8 +644,27 @@ export class OverlayController extends EventTarget {
581
644
  }
582
645
  this.__handleOverlayStyles({ phase: 'init' });
583
646
  this._handleFeatures({ phase: 'init' });
647
+
648
+ /** @private */
649
+ this.__elementToFocusAfterHide = undefined;
650
+
651
+ this.contentNode.dataset.content = '';
652
+
653
+ // Use event delegation to listen for clicks on close buttons...
654
+ this.contentNode.addEventListener('click', this.#hideOnCloseButtonClick);
655
+
656
+ this.__hasSetup = true;
584
657
  }
585
658
 
659
+ #hideOnCloseButtonClick = (/** @type {{ target: any; }} */ ev) => {
660
+ const isOurCloseButton =
661
+ ev.target.hasAttribute('data-close') &&
662
+ deepClosest(ev.target, '[data-content]') === this.contentNode;
663
+ if (!isOurCloseButton) return;
664
+
665
+ this.hide();
666
+ };
667
+
586
668
  /**
587
669
  * @param {{ phase: OverlayPhase }} config
588
670
  * @private
@@ -623,84 +705,108 @@ export class OverlayController extends EventTarget {
623
705
  * @private
624
706
  */
625
707
  __initContentDomStructure() {
626
- const wrappingDialogElement = document.createElement('dialog');
627
- // We use a dialog for its visual capabilities: it renders to the top layer.
628
- // A11y will depend on the type of overlay and is arranged on contentNode level.
629
- // Also see: https://www.scottohara.me/blog/2019/03/05/open-dialog.html
630
- //
631
- // The role="dialog" is set on the contentNode (or another role), so role="none"
632
- // is valid here, although AXE complains about this setup.
633
- // For now we need to add `ignoredRules: ['aria-allowed-role']` in your AXE tests.
634
- // see: https://lion.js.org/fundamentals/systems/overlays/rationale/#considerations
635
- wrappingDialogElement.setAttribute('role', 'none');
636
- wrappingDialogElement.setAttribute('data-overlay-outer-wrapper', '');
637
- // N.B. position: fixed is needed to escape out of 'overflow: hidden'
638
- // We give a high z-index for non-modal dialogs, so that we at least win from all siblings of our
639
- // parent stacking context
640
- // padding reset so we don't get a weird dialog visual square showing up
641
- wrappingDialogElement.style.cssText = `display:none; z-index: ${this.config.zIndex}; padding: 0;`;
642
- this.__wrappingDialogNode = wrappingDialogElement;
708
+ if (this.placementMode !== 'custom') {
709
+ const wrappingDialogElement = document.createElement('dialog');
710
+ // We use a dialog for its visual capabilities: it renders to the top layer.
711
+ // A11y will depend on the type of overlay and is arranged on contentNode level.
712
+ // Also see: https://www.scottohara.me/blog/2019/03/05/open-dialog.html
713
+ //
714
+ // The role="dialog" is set on the contentNode (or another role), so role="none"
715
+ // is valid here, although AXE complains about this setup.
716
+ // For now we need to add `ignoredRules: ['aria-allowed-role']` in your AXE tests.
717
+ // see: https://lion.js.org/fundamentals/systems/overlays/rationale/#considerations
718
+ wrappingDialogElement.setAttribute('role', 'none');
719
+ wrappingDialogElement.setAttribute('data-overlay-outer-wrapper', '');
720
+ // N.B. position: fixed is needed to escape out of 'overflow: hidden'
721
+ // We give a high z-index for non-modal dialogs, so that we at least win from all siblings of our
722
+ // parent stacking context
723
+ // padding reset so we don't get a weird dialog visual square showing up
724
+ wrappingDialogElement.style.cssText = `display:none; z-index: ${this.config.zIndex}; padding: 0;`;
725
+ // @ts-ignore - 'custom' is a valid extension of placementMode
726
+ if (this.config.placementMode === 'custom') {
727
+ // The user should have full freedom to control the content node, so its wrapper nodes should remain neutral.
728
+ wrappingDialogElement.style.cssText += `position: static;`;
729
+ }
730
+ this.__wrappingDialogNode = wrappingDialogElement;
643
731
 
644
- /**
645
- * Based on the configuration of the developer, multiple scenarios are accounted for
646
- * A. We already have a contentWrapperNode ()
647
- */
648
- if (!this.config?.contentWrapperNode) {
649
- this.__contentWrapperNode = document.createElement('div');
650
- }
651
- this.contentWrapperNode.setAttribute('data-id', 'content-wrapper');
652
- // 'hack' that makes sure popperjs (that is applied one level lower) works correctly in deeply nested shadow roots
653
- this.contentWrapperNode.style.transform = 'translateZ(0px)';
732
+ /**
733
+ * Based on the configuration of the developer, multiple scenarios are accounted for
734
+ * A. We already have a contentWrapperNode ()
735
+ */
736
+ if (!this.config?.contentWrapperNode) {
737
+ this.__contentWrapperNode = document.createElement('div');
738
+ }
739
+ this.contentWrapperNode.setAttribute('data-id', 'content-wrapper');
740
+ // 'hack' that makes sure popperjs (that is applied one level lower) works correctly in deeply nested shadow roots
741
+ this.contentWrapperNode.style.transform = 'translateZ(0px)';
742
+
743
+ this.__rearrangeNodesCleanup = rearrangeNodes({
744
+ wrappingDialogNodeL1: wrappingDialogElement,
745
+ contentWrapperNodeL2: this.contentWrapperNode,
746
+ contentNodeL3: this.contentNode,
747
+ // requireConnectedNodes: Boolean(this.config.requireConnectedNodes),
748
+ });
749
+ wrappingDialogElement.open = true;
654
750
 
655
- rearrangeNodes({
656
- wrappingDialogNodeL1: wrappingDialogElement,
657
- contentWrapperNodeL2: this.contentWrapperNode,
658
- contentNodeL3: this.contentNode,
659
- });
660
- wrappingDialogElement.open = true;
751
+ if (this.isTooltip) {
752
+ // needed to prevent tooltip getting focus in Safari and Firefox
753
+ wrappingDialogElement.setAttribute('tabindex', '-1');
754
+ }
755
+ this.contentWrapperNode.style.zIndex = '1';
756
+ if (getComputedStyle(this.contentNode).position === 'absolute') {
757
+ // Having a _contWrapperNode and a contentNode with 'position:absolute' results in
758
+ // computed height of 0...
759
+ this.contentNode.style.position = 'static';
760
+ }
661
761
 
662
- if (this.isTooltip) {
663
- // needed to prevent tooltip getting focus in Safari and Firefox
664
- wrappingDialogElement.setAttribute('tabindex', '-1');
762
+ // Here we prevent any interference of the native <dialog> element with the keyboard behavior
763
+ // as defined by the OverlayController. This is needed until we can configure `closedby="none"`
764
+ // on the native dialog for all browsers: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#closedby
765
+ const hasClosedBySupport = HTMLDialogElement && 'closedBy' in HTMLDialogElement.prototype;
766
+ if (hasClosedBySupport) {
767
+ // @ts-ignore - closedBy is a newer property not in all TypeScript versions
768
+ wrappingDialogElement.closedBy = 'none';
769
+ } else {
770
+ wrappingDialogElement.addEventListener(
771
+ 'keydown',
772
+ (/** @type {* & KeyboardEvent} */ event) => {
773
+ if (event.key === 'Escape') {
774
+ event.preventDefault();
775
+ }
776
+ },
777
+ );
778
+ wrappingDialogElement.addEventListener(
779
+ 'keyup',
780
+ (/** @type {* & KeyboardEvent} */ event) => {
781
+ if (event.key === 'Escape') {
782
+ event.preventDefault();
783
+ }
784
+ },
785
+ );
786
+ wrappingDialogElement.addEventListener('cancel', event => {
787
+ event.stopPropagation();
788
+ });
789
+ wrappingDialogElement.addEventListener('close', event => {
790
+ event.stopPropagation();
791
+ });
792
+ }
793
+ } else {
794
+ // quick hack to make none-popper/global flows work...
795
+ this.__contentWrapperNode = this.contentNode;
796
+ this.__wrappingDialogNode = this.contentNode;
665
797
  }
798
+ }
666
799
 
667
- this.__wrappingDialogNode.style.display = 'none';
668
- this.contentWrapperNode.style.zIndex = '1';
669
-
670
- if (getComputedStyle(this.contentNode).position === 'absolute') {
671
- // Having a _contWrapperNode and a contentNode with 'position:absolute' results in
672
- // computed height of 0...
673
- this.contentNode.style.position = 'static';
674
- }
800
+ __initVisibility() {
801
+ // TODO: allow defaultOpen?
802
+ /** @type {HTMLDialogElement} */ (this.__wrappingDialogNode).style.display = 'none';
803
+ }
675
804
 
676
- // Here we prevent any interference of the native <dialog> element with the keyboard behavior
677
- // as defined by the OverlayController. This is needed until we can configure `closedby="none"`
678
- // on the native dialog for all browsers: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#closedby
679
- const hasClosedBySupport = HTMLDialogElement && 'closedBy' in HTMLDialogElement.prototype;
680
- if (hasClosedBySupport) {
681
- // @ts-ignore [closedBy is feature-detected above]
682
- wrappingDialogElement.closedBy = 'none';
683
- } else {
684
- wrappingDialogElement.addEventListener(
685
- 'keydown',
686
- (/** @type {* & KeyboardEvent} */ event) => {
687
- if (event.key === 'Escape') {
688
- event.preventDefault();
689
- }
690
- },
691
- );
692
- wrappingDialogElement.addEventListener('keyup', (/** @type {* & KeyboardEvent} */ event) => {
693
- if (event.key === 'Escape') {
694
- event.preventDefault();
695
- }
696
- });
697
- wrappingDialogElement.addEventListener('cancel', event => {
698
- event.stopPropagation();
699
- });
700
- wrappingDialogElement.addEventListener('close', event => {
701
- event.stopPropagation();
702
- });
703
- }
805
+ // Clean up the DOM structure, leaving it as we found it.
806
+ // TODO: __teardownContentDomStructure method... use Resettable (rename ro Restorable) found in VisibilitToggleCtrl
807
+ __teardownVisibility() {
808
+ // TODO: full structure. For now we just reset display prop...
809
+ /** @type {HTMLDialogElement} */ (this.__wrappingDialogNode).style.display = '';
704
810
  }
705
811
 
706
812
  /**
@@ -794,7 +900,7 @@ export class OverlayController extends EventTarget {
794
900
  }
795
901
 
796
902
  get isShown() {
797
- return Boolean(this.__wrappingDialogNode?.style.display !== 'none');
903
+ return Boolean(this.__wrappingDialogNode && this.__wrappingDialogNode.style.display !== 'none');
798
904
  }
799
905
 
800
906
  /**
@@ -812,6 +918,13 @@ export class OverlayController extends EventTarget {
812
918
  this._showResolve = resolve;
813
919
  });
814
920
 
921
+ // Teardown() removes the content dom structure we created (contentWrapperNode +
922
+ // wrapping <dialog>). Make sure it exists again before the manager (which sets
923
+ // our elevation on the wrapping node) and the show logic below touch it.
924
+ if (!this.__wrappingDialogNode) {
925
+ this._init();
926
+ }
927
+
815
928
  if (this.manager) {
816
929
  this.manager.show(this);
817
930
  }
@@ -828,6 +941,7 @@ export class OverlayController extends EventTarget {
828
941
  if ('HTMLDialogElement' in window && this.__wrappingDialogNode instanceof HTMLDialogElement) {
829
942
  this.__wrappingDialogNode.open = true;
830
943
  }
944
+
831
945
  // @ts-ignore
832
946
  this.__wrappingDialogNode.style.display = '';
833
947
  this._keepBodySize({ phase: 'before-show' });
@@ -841,6 +955,14 @@ export class OverlayController extends EventTarget {
841
955
  contentNode: this.contentNode,
842
956
  });
843
957
  }
958
+
959
+ // a styling/debug hook for open state
960
+ this.contentNode.setAttribute('data-open', '');
961
+
962
+ if (this.config.elementToFocusOnShow) {
963
+ this.contentNode.focus({ preventScroll: true });
964
+ }
965
+
844
966
  /** @type {function} */
845
967
  (this._showResolve)();
846
968
  }
@@ -873,6 +995,7 @@ export class OverlayController extends EventTarget {
873
995
  await this.__createPopperInstance();
874
996
  /** @type {import('@popperjs/core').Instance} */ (this._popper).forceUpdate();
875
997
  }
998
+ // N.B. leave it up to the user if placementMode === 'custom'
876
999
  }
877
1000
 
878
1001
  /**
@@ -880,9 +1003,7 @@ export class OverlayController extends EventTarget {
880
1003
  * @protected
881
1004
  */
882
1005
  _keepBodySize({ phase }) {
883
- if (!this.preventsScroll) {
884
- return;
885
- }
1006
+ if (!this.preventsScroll) return;
886
1007
 
887
1008
  this.manager.requestToKeepBodySize({ phase });
888
1009
  }
@@ -892,6 +1013,9 @@ export class OverlayController extends EventTarget {
892
1013
  * @event hide right after the overlay is hidden
893
1014
  */
894
1015
  async hide() {
1016
+ // Function like a no-op for dynamic edge cases...
1017
+ if (!this.config.isActivated) return;
1018
+
895
1019
  this._hideComplete = new Promise(resolve => {
896
1020
  this._hideResolve = resolve;
897
1021
  });
@@ -924,7 +1048,6 @@ export class OverlayController extends EventTarget {
924
1048
  if ('HTMLDialogElement' in window && this.__wrappingDialogNode instanceof HTMLDialogElement) {
925
1049
  this.__wrappingDialogNode.close();
926
1050
  }
927
-
928
1051
  // @ts-ignore
929
1052
  this.__wrappingDialogNode.style.display = 'none';
930
1053
  this._handleFeatures({ phase: 'hide' });
@@ -933,6 +1056,8 @@ export class OverlayController extends EventTarget {
933
1056
  this._restoreFocus();
934
1057
  }
935
1058
  /** @type {function} */ (this._hideResolve)();
1059
+
1060
+ this.contentNode.removeAttribute('data-open');
936
1061
  }
937
1062
 
938
1063
  /**
@@ -1043,23 +1168,53 @@ export class OverlayController extends EventTarget {
1043
1168
  if (this.visibilityTriggerFunction) {
1044
1169
  this._handleVisibilityTriggers({ phase });
1045
1170
  }
1171
+ if (this.config.syncChildrenCloseState) {
1172
+ this._handleSyncChildrenCloseState({ phase });
1173
+ }
1174
+ if (this.config.elementToFocusOnShow) {
1175
+ this._handleElementToFocusOnShow({ phase });
1176
+ }
1177
+ }
1178
+
1179
+ /**
1180
+ * @param {{phase: OverlayPhase}} opts
1181
+ * @returns {void}
1182
+ */
1183
+ _handleElementToFocusOnShow({ phase }) {
1184
+ if (phase === 'init') {
1185
+ this.contentNode?.setAttribute('tabindex', '-1');
1186
+ } else if (phase === 'teardown') {
1187
+ // TODO: capture initial attr, use Resettable mechanism of VisibilityToggleCtrl in whole Controller
1188
+ this.contentNode?.removeAttribute('tabindex');
1189
+ }
1190
+ }
1191
+
1192
+ /**
1193
+ * @param {{phase: OverlayPhase}} opts
1194
+ * @returns {void}
1195
+ */
1196
+ _handleSyncChildrenCloseState({ phase }) {
1197
+ if (phase !== 'hide') return;
1198
+
1199
+ const visibleChildren = this.manager.shownList.filter(
1200
+ ctrl => ctrl !== this && deepContains(this.contentNode, ctrl.contentNode),
1201
+ );
1202
+ visibleChildren.forEach(ctrl => ctrl.hide());
1046
1203
  }
1047
1204
 
1048
1205
  /**
1049
1206
  * @param {{ phase: OverlayPhase }} config
1050
1207
  */
1051
1208
  _handleVisibilityTriggers({ phase }) {
1052
- if (typeof this.visibilityTriggerFunction === 'function') {
1053
- if (phase === 'init') {
1054
- this.__visibilityTriggerHandler = this.visibilityTriggerFunction({
1055
- phase,
1056
- controller: this,
1057
- });
1058
- }
1059
- if (this.__visibilityTriggerHandler[phase]) {
1060
- this.__visibilityTriggerHandler[phase]();
1061
- }
1209
+ if (typeof this.visibilityTriggerFunction !== 'function') return;
1210
+
1211
+ // Here we initialize the __visibilityTriggerHandler of our invokerNode. It's important that we ONLY do this on init,
1212
+ // otherwise we risk not being able to properly clean up listeners...
1213
+ if (phase === 'init') {
1214
+ this.__visibilityTriggerHandler = this.visibilityTriggerFunction({ controller: this });
1062
1215
  }
1216
+ // Here we run the appropriate lifecycle, if defined in our handler
1217
+ this.__visibilityTriggerHandler[phase]?.();
1063
1218
  }
1064
1219
 
1065
1220
  /**
@@ -1216,7 +1371,9 @@ export class OverlayController extends EventTarget {
1216
1371
  if (phase === 'show') {
1217
1372
  this.#handleShiftKeyPress();
1218
1373
  this.#handleFocusInsideDialog();
1374
+ // @ts-ignore - HTMLDialogElement methods
1219
1375
  this.__wrappingDialogNode?.close();
1376
+ // @ts-ignore - HTMLDialogElement methods
1220
1377
  this.__wrappingDialogNode?.showModal();
1221
1378
  /**
1222
1379
  * At this moment `#handleFocusInsideDialog` should handle the focus.
@@ -1471,6 +1628,7 @@ export class OverlayController extends EventTarget {
1471
1628
  }
1472
1629
 
1473
1630
  teardown() {
1631
+ if (!this.__hasSetup) return;
1474
1632
  this.__handleOverlayStyles({ phase: 'teardown' });
1475
1633
  if (this.isShown) {
1476
1634
  this._keepBodySize({ phase: 'teardown' });
@@ -1479,6 +1637,27 @@ export class OverlayController extends EventTarget {
1479
1637
  if (this.#isRegisteredOnManager()) {
1480
1638
  this.manager.remove(this);
1481
1639
  }
1640
+
1641
+ this.contentNode?.removeEventListener('click', this.#hideOnCloseButtonClick);
1642
+
1643
+ // if (this.config._shouldTeardownDomStructure) {
1644
+ this.__rearrangeNodesCleanup?.();
1645
+ this.__rearrangeNodesCleanup = undefined;
1646
+ // }
1647
+ this.__teardownVisibility();
1648
+
1649
+ // When we created the contentWrapperNode/wrapping <dialog> ourselves, restore the
1650
+ // original dom structure and drop the (now detached) references, so that a
1651
+ // subsequent setup recreates them and `contentWrapperNode`/`content` no longer
1652
+ // expose stale nodes.
1653
+ if (this.contentWrapperNode !== this.contentNode) {
1654
+ this.contentWrapperNode?.removeAttribute('style');
1655
+ this.contentWrapperNode?.removeAttribute('class');
1656
+ this.__contentWrapperNode = undefined;
1657
+ this.__wrappingDialogNode = undefined;
1658
+ }
1659
+
1660
+ this.__hasSetup = false;
1482
1661
  }
1483
1662
 
1484
1663
  /** @private */