@lion/ui 0.9.1 → 0.10.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 (172) hide show
  1. package/components/button/src/LionButton.js +5 -1
  2. package/components/button/test-suites/LionButton.suite.js +1 -1
  3. package/components/calendar/src/LionCalendar.js +5 -14
  4. package/components/calendar/test/lion-calendar.test.js +88 -0
  5. package/components/calendar/translations/bg.js +5 -0
  6. package/components/calendar/translations/cs.js +5 -0
  7. package/components/calendar/translations/de.js +5 -0
  8. package/components/calendar/translations/en.js +5 -0
  9. package/components/calendar/translations/es.js +5 -0
  10. package/components/calendar/translations/fr.js +5 -0
  11. package/components/calendar/translations/hu.js +5 -0
  12. package/components/calendar/translations/it.js +5 -0
  13. package/components/calendar/translations/nl.js +5 -0
  14. package/components/calendar/translations/pl.js +5 -0
  15. package/components/calendar/translations/ro.js +5 -0
  16. package/components/calendar/translations/ru.js +5 -0
  17. package/components/calendar/translations/sk.js +5 -0
  18. package/components/calendar/translations/uk.js +3 -0
  19. package/components/calendar/translations/zh.js +3 -0
  20. package/components/combobox/test/lion-combobox.test.js +2 -0
  21. package/components/core/src/SlotMixin.js +15 -0
  22. package/components/core/test/ScopedElementsMixin.test.js +3 -3
  23. package/components/core/test/SlotMixin.test.js +50 -0
  24. package/components/dialog/src/LionDialog.js +13 -0
  25. package/components/dialog/test/lion-dialog.test.js +110 -0
  26. package/components/dialog/test-helpers/test-router.js +43 -0
  27. package/components/form-core/src/FormatMixin.js +66 -18
  28. package/components/form-core/src/choice-group/ChoiceInputMixin.js +3 -1
  29. package/components/form-core/test-suites/FormatMixin.suite.js +107 -8
  30. package/components/form-core/types/FormatMixinTypes.ts +4 -1
  31. package/components/form-integrations/test/dialog-integrations.test.js +6 -2
  32. package/components/input-amount/src/parsers.js +3 -3
  33. package/components/input-amount/test/lion-input-amount.test.js +36 -2
  34. package/components/input-amount/test/parsers.test.js +11 -0
  35. package/components/input-tel-dropdown/src/LionInputTelDropdown.js +1 -1
  36. package/components/localize/src/number/getDecimalSeparator.js +1 -1
  37. package/components/localize/src/number/parseNumber.js +9 -6
  38. package/components/localize/test/number/parseNumber.test.js +11 -0
  39. package/components/localize/types/LocalizeMixinTypes.ts +3 -3
  40. package/components/overlays/src/OverlayController.js +21 -6
  41. package/components/overlays/src/OverlayMixin.js +20 -14
  42. package/components/overlays/test/OverlayController.test.js +40 -8
  43. package/components/overlays/test-suites/OverlayMixin.suite.js +184 -9
  44. package/components/overlays/types/ArrowMixinTypes.ts +11 -2
  45. package/components/overlays/types/OverlayConfig.ts +19 -15
  46. package/components/select-rich/src/LionSelectRich.js +3 -1
  47. package/components/select-rich/test/lion-select-rich.test.js +44 -0
  48. package/custom-elements.json +1340 -89
  49. package/dist-types/components/button/src/LionButton.d.ts.map +1 -1
  50. package/dist-types/components/calendar/src/LionCalendar.d.ts.map +1 -1
  51. package/dist-types/components/calendar/translations/bg.d.ts +3 -0
  52. package/dist-types/components/calendar/translations/cs.d.ts +3 -0
  53. package/dist-types/components/calendar/translations/de.d.ts +3 -0
  54. package/dist-types/components/calendar/translations/en.d.ts +3 -0
  55. package/dist-types/components/calendar/translations/es.d.ts +3 -0
  56. package/dist-types/components/calendar/translations/fr.d.ts +3 -0
  57. package/dist-types/components/calendar/translations/hu.d.ts +3 -0
  58. package/dist-types/components/calendar/translations/it.d.ts +3 -0
  59. package/dist-types/components/calendar/translations/nl.d.ts +3 -0
  60. package/dist-types/components/calendar/translations/pl.d.ts +3 -0
  61. package/dist-types/components/calendar/translations/ro.d.ts +3 -0
  62. package/dist-types/components/calendar/translations/ru.d.ts +3 -0
  63. package/dist-types/components/calendar/translations/sk.d.ts +3 -0
  64. package/dist-types/components/calendar/translations/uk.d.ts +3 -0
  65. package/dist-types/components/calendar/translations/zh.d.ts +3 -0
  66. package/dist-types/components/core/src/SlotMixin.d.ts.map +1 -1
  67. package/dist-types/components/dialog/src/LionDialog.d.ts +32 -0
  68. package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
  69. package/dist-types/components/dialog/test/lion-dialog.test.d.ts.map +1 -1
  70. package/dist-types/components/dialog/test-helpers/test-router.d.ts +2 -0
  71. package/dist-types/components/dialog/test-helpers/test-router.d.ts.map +1 -0
  72. package/dist-types/components/form-core/src/FormatMixin.d.ts +1 -1
  73. package/dist-types/components/form-core/src/FormatMixin.d.ts.map +1 -1
  74. package/dist-types/components/form-core/src/choice-group/ChoiceInputMixin.d.ts.map +1 -1
  75. package/dist-types/components/form-core/test-suites/FormatMixin.suite.d.ts.map +1 -1
  76. package/dist-types/components/form-core/types/FormatMixinTypes.d.ts +3 -2
  77. package/dist-types/components/form-core/types/FormatMixinTypes.d.ts.map +1 -1
  78. package/dist-types/components/form-integrations/test/dialog-integrations.test.d.ts.map +1 -1
  79. package/dist-types/components/input-amount/src/parsers.d.ts +2 -2
  80. package/dist-types/components/input-amount/src/parsers.d.ts.map +1 -1
  81. package/dist-types/components/input-amount/test/lion-input-amount.test.d.ts.map +1 -1
  82. package/dist-types/components/input-tel-dropdown/src/LionInputTelDropdown.d.ts +1 -1
  83. package/dist-types/components/localize/src/number/parseNumber.d.ts.map +1 -1
  84. package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts +3 -2
  85. package/dist-types/components/localize/types/LocalizeMixinTypes.d.ts.map +1 -1
  86. package/dist-types/components/overlays/src/OverlayController.d.ts +5 -0
  87. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  88. package/dist-types/components/overlays/src/OverlayMixin.d.ts.map +1 -1
  89. package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts +4 -4
  90. package/dist-types/components/overlays/test-suites/OverlayMixin.suite.d.ts.map +1 -1
  91. package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts +2 -2
  92. package/dist-types/components/overlays/types/ArrowMixinTypes.d.ts.map +1 -1
  93. package/dist-types/components/overlays/types/OverlayConfig.d.ts +8 -6
  94. package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
  95. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +2 -1
  96. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  97. package/dist-types/components/select-rich/test/lion-select-rich.test.d.ts.map +1 -1
  98. package/dist-types/tsconfig.tsbuildinfo +1 -1
  99. package/docs/components/checkbox-group/api-table.md +2 -2
  100. package/docs/components/checkbox-group/overview.md +2 -2
  101. package/docs/components/collapsible/overview.md +1 -1
  102. package/docs/components/combobox/api-table.md +1 -0
  103. package/docs/components/combobox/overview.md +1 -1
  104. package/docs/components/combobox/use-cases.md +1 -1
  105. package/docs/components/dialog/api-table.md +6 -3
  106. package/docs/components/dialog/overview.md +1 -1
  107. package/docs/components/dialog/use-cases.md +32 -1
  108. package/docs/components/fieldset/overview.md +5 -5
  109. package/docs/components/form/overview.md +3 -3
  110. package/docs/components/form/use-cases.md +2 -2
  111. package/docs/components/icon/use-cases.md +1 -1
  112. package/docs/components/input/api-table.md +1 -1
  113. package/docs/components/input/overview.md +1 -1
  114. package/docs/components/input-amount/api-table.md +1 -1
  115. package/docs/components/input-amount/overview.md +3 -3
  116. package/docs/components/input-date/api-table.md +1 -1
  117. package/docs/components/input-date/overview.md +3 -3
  118. package/docs/components/input-datepicker/api-table.md +2 -1
  119. package/docs/components/input-datepicker/overview.md +5 -5
  120. package/docs/components/input-email/api-table.md +1 -1
  121. package/docs/components/input-email/overview.md +2 -2
  122. package/docs/components/input-file/api-table.md +1 -1
  123. package/docs/components/input-file/overview.md +1 -1
  124. package/docs/components/input-iban/api-table.md +1 -1
  125. package/docs/components/input-iban/overview.md +2 -2
  126. package/docs/components/input-range/api-table.md +1 -1
  127. package/docs/components/input-range/overview.md +2 -2
  128. package/docs/components/input-stepper/api-table.md +1 -1
  129. package/docs/components/input-stepper/overview.md +1 -1
  130. package/docs/components/input-tel/api-table.md +1 -1
  131. package/docs/components/input-tel/overview.md +1 -1
  132. package/docs/components/input-tel/use-cases.md +6 -6
  133. package/docs/components/input-tel-dropdown/api-table.md +1 -1
  134. package/docs/components/input-tel-dropdown/overview.md +1 -1
  135. package/docs/components/listbox/api-table.md +1 -1
  136. package/docs/components/listbox/use-cases.md +1 -1
  137. package/docs/components/radio-group/api-table.md +1 -1
  138. package/docs/components/radio-group/overview.md +3 -3
  139. package/docs/components/select/api-table.md +1 -1
  140. package/docs/components/select/use-cases.md +1 -1
  141. package/docs/components/select-rich/api-table.md +1 -0
  142. package/docs/components/select-rich/use-cases.md +1 -1
  143. package/docs/components/switch/api-table.md +1 -1
  144. package/docs/components/tabs/examples.md +1 -1
  145. package/docs/components/textarea/api-table.md +1 -1
  146. package/docs/components/tooltip/api-table.md +1 -0
  147. package/docs/fundamentals/index.md +4 -4
  148. package/docs/fundamentals/node-tools/providence-analytics/overview.md +5 -5
  149. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  150. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  151. package/docs/fundamentals/rationales/accessibility.md +1 -1
  152. package/docs/fundamentals/systems/core/SlotMixin.md +1 -1
  153. package/docs/fundamentals/systems/core/overview.md +1 -1
  154. package/docs/fundamentals/systems/form/api-table.md +1 -1
  155. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  156. package/docs/fundamentals/systems/form/model-value.md +1 -1
  157. package/docs/fundamentals/systems/form/overview.md +28 -28
  158. package/docs/fundamentals/systems/form/validate.md +2 -2
  159. package/docs/fundamentals/systems/localize/overview.md +4 -4
  160. package/docs/fundamentals/systems/overlays/api-table.md +35 -34
  161. package/docs/fundamentals/systems/overlays/configuration.md +32 -2
  162. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  163. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  164. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  165. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  166. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  167. package/docs/guides/how-to/create-a-form.md +10 -9
  168. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  169. package/docs/guides/index.md +8 -8
  170. package/docs/guides/principles/index.md +5 -5
  171. package/docs/guides/principles/styling.md +1 -1
  172. package/package.json +1 -1
@@ -151,6 +151,7 @@ export class OverlayController extends EventTarget {
151
151
  hidesOnOutsideEsc: false,
152
152
  hidesOnOutsideClick: false,
153
153
  isTooltip: false,
154
+ isAlertDialog: false,
154
155
  invokerRelation: 'description',
155
156
  visibilityTriggerFunction: undefined,
156
157
  handlesAccessibility: false,
@@ -381,6 +382,14 @@ export class OverlayController extends EventTarget {
381
382
  return /** @type {boolean} */ (this.config?.isTooltip);
382
383
  }
383
384
 
385
+ /**
386
+ * The alertdialog role is to be used on modal alert dialogs that interrupt a user's workflow
387
+ * to communicate an important message and require a response.
388
+ */
389
+ get isAlertDialog() {
390
+ return /** @type {boolean} */ (this.config?.isAlertDialog);
391
+ }
392
+
384
393
  /**
385
394
  * By default, the tooltip content is a 'description' for the invoker (uses aria-describedby).
386
395
  * Setting this property to 'label' makes the content function as a label (via aria-labelledby)
@@ -611,6 +620,10 @@ export class OverlayController extends EventTarget {
611
620
  });
612
621
  // @ts-ignore
613
622
  wrappingDialogElement.open = true;
623
+ if (this.isTooltip) {
624
+ // needed to prevent tooltip getting focus in Safari and Firefox
625
+ wrappingDialogElement.setAttribute('tabindex', '-1');
626
+ }
614
627
 
615
628
  this.__wrappingDialogNode.style.display = 'none';
616
629
  this.contentWrapperNode.style.zIndex = '1';
@@ -672,7 +685,10 @@ export class OverlayController extends EventTarget {
672
685
  if (this.invokerNode && !isModal) {
673
686
  this.invokerNode.setAttribute('aria-expanded', `${this.isShown}`);
674
687
  }
675
- if (!this.contentNode.getAttribute('role')) {
688
+ if (this.isAlertDialog) {
689
+ this.contentNode.setAttribute('role', 'alertdialog');
690
+ } else if (!this.contentNode.getAttribute('role')) {
691
+ // N.B. if we did not explicitly set `isTooltip` or `isAlertDialog`, a role potentially already provided by a user (like 'listbox') takes precedence
676
692
  this.contentNode.setAttribute('role', 'dialog');
677
693
  }
678
694
  }
@@ -1191,9 +1207,8 @@ export class OverlayController extends EventTarget {
1191
1207
  const hasPressedInside =
1192
1208
  event.composedPath().includes(this.contentNode) ||
1193
1209
  deepContains(this.contentNode, /** @type {HTMLElement|ShadowRoot} */ (event.target));
1194
- if (!hasPressedInside) {
1195
- this.hide();
1196
- }
1210
+ if (hasPressedInside) return;
1211
+ this.hide();
1197
1212
  };
1198
1213
 
1199
1214
  /**
@@ -1206,7 +1221,7 @@ export class OverlayController extends EventTarget {
1206
1221
  if (this.invokerNode) {
1207
1222
  this.invokerNode.addEventListener('keyup', this.__escKeyHandler);
1208
1223
  }
1209
- } else if (phase === 'hide') {
1224
+ } else if (phase === 'hide' || phase === 'teardown') {
1210
1225
  this.contentNode.removeEventListener('keyup', this.__escKeyHandler);
1211
1226
  if (this.invokerNode) {
1212
1227
  this.invokerNode.removeEventListener('keyup', this.__escKeyHandler);
@@ -1221,7 +1236,7 @@ export class OverlayController extends EventTarget {
1221
1236
  _handleHidesOnOutsideEsc({ phase }) {
1222
1237
  if (phase === 'show') {
1223
1238
  document.addEventListener('keyup', this.#outsideEscKeyHandler);
1224
- } else if (phase === 'hide') {
1239
+ } else if (phase === 'hide' || phase === 'teardown') {
1225
1240
  document.removeEventListener('keyup', this.#outsideEscKeyHandler);
1226
1241
  }
1227
1242
  }
@@ -24,6 +24,8 @@ export const OverlayMixinImplementation = superclass =>
24
24
  };
25
25
  }
26
26
 
27
+ #hasSetup = false;
28
+
27
29
  constructor() {
28
30
  super();
29
31
  /**
@@ -173,22 +175,23 @@ export const OverlayMixinImplementation = superclass =>
173
175
  }
174
176
  }
175
177
 
176
- /**
177
- * @param {import('lit-element').PropertyValues } changedProperties
178
- */
179
- firstUpdated(changedProperties) {
180
- super.firstUpdated(changedProperties);
178
+ connectedCallback() {
179
+ super.connectedCallback();
181
180
 
182
- this._setupOverlayCtrl();
181
+ this.updateComplete.then(() => {
182
+ if (this.#hasSetup) return;
183
+ this._setupOverlayCtrl();
184
+ this.#hasSetup = true;
185
+ });
183
186
  }
184
187
 
185
188
  async disconnectedCallback() {
186
189
  super.disconnectedCallback();
187
190
 
188
- if (!this._overlayCtrl) return;
189
- if (await this.#isMovingInDom()) return;
190
-
191
- this._teardownOverlayCtrl();
191
+ if (await this._isPermanentlyDisconnected()) {
192
+ this._teardownOverlayCtrl();
193
+ this.#hasSetup = false;
194
+ }
192
195
  }
193
196
 
194
197
  /**
@@ -238,6 +241,8 @@ export const OverlayMixinImplementation = superclass =>
238
241
 
239
242
  /** @protected */
240
243
  _setupOverlayCtrl() {
244
+ if (this._overlayCtrl) return;
245
+
241
246
  /** @type {OverlayController} */
242
247
  this._overlayCtrl = this._defineOverlay({
243
248
  contentNode: this._overlayContentNode,
@@ -253,11 +258,12 @@ export const OverlayMixinImplementation = superclass =>
253
258
 
254
259
  /** @protected */
255
260
  _teardownOverlayCtrl() {
261
+ if (!this._overlayCtrl) return;
262
+
256
263
  this._teardownOpenCloseListeners();
257
264
  this.__teardownSyncFromOverlayController();
258
265
 
259
- /** @type {OverlayController} */
260
- (this._overlayCtrl).teardown();
266
+ /** @type {OverlayController} */ (this._overlayCtrl).teardown();
261
267
  }
262
268
 
263
269
  /**
@@ -396,9 +402,9 @@ export const OverlayMixinImplementation = superclass =>
396
402
  * Before we decide to teardown, let's wait to see if we were not just moving nodes around.
397
403
  * @return {Promise<boolean>}
398
404
  */
399
- async #isMovingInDom() {
405
+ async _isPermanentlyDisconnected() {
400
406
  await this.updateComplete;
401
- return this.isConnected;
407
+ return !this.isConnected;
402
408
  }
403
409
  };
404
410
  export const OverlayMixin = dedupeMixin(OverlayMixinImplementation);
@@ -103,12 +103,12 @@ const withLocalTestConfig = () =>
103
103
  async function createNestedEscControllers(parentContent) {
104
104
  const childContent = /** @type {HTMLDivElement} */ (parentContent.querySelector('div[id]'));
105
105
  // Assert valid fixure
106
- const isValid =
107
- (parentContent.id === 'parent-overlay--hidesOnEsc' ||
108
- parentContent.id === 'parent-overlay--hidesOnOutsideEsc') &&
109
- (childContent.id === 'child-overlay--hidesOnEsc' ||
110
- childContent.id === 'child-overlay--hidesOnOutsideEsc');
111
- if (!isValid) {
106
+ const isValidFixture =
107
+ (parentContent.id.startsWith('parent-overlay--hidesOnEsc') ||
108
+ parentContent.id.startsWith('parent-overlay--hidesOnOutsideEsc')) &&
109
+ (childContent.id.startsWith('child-overlay--hidesOnEsc') ||
110
+ childContent.id.startsWith('child-overlay--hidesOnOutsideEsc'));
111
+ if (!isValidFixture) {
112
112
  throw new Error('Provide a valid fixture');
113
113
  }
114
114
 
@@ -117,8 +117,8 @@ async function createNestedEscControllers(parentContent) {
117
117
  shadowRootParent.appendChild(childContent);
118
118
  }
119
119
 
120
- const parentHasOutsideOnEsc = parentContent.id === 'parent-overlay--hidesOnOutsideEsc';
121
- const childHasOutsideOnEsc = childContent.id === 'child-overlay--hidesOnOutsideEsc';
120
+ const parentHasOutsideOnEsc = parentContent.id.startsWith('parent-overlay--hidesOnOutsideEsc');
121
+ const childHasOutsideOnEsc = childContent.id.startsWith('child-overlay--hidesOnOutsideEsc');
122
122
 
123
123
  const parentConfig = parentHasOutsideOnEsc ? { hidesOnOutsideEsc: true } : { hidesOnEsc: true };
124
124
  const childConfig = childHasOutsideOnEsc ? { hidesOnOutsideEsc: true } : { hidesOnEsc: true };
@@ -787,6 +787,9 @@ describe('OverlayController', () => {
787
787
 
788
788
  expect(parentOverlay.isShown).to.be.false;
789
789
  expect(childOverlay.isShown).to.be.true;
790
+
791
+ await childOverlay.teardown();
792
+ await parentOverlay.teardown();
790
793
  });
791
794
 
792
795
  it('on [Escape] press outside overlays: parent stays shown, child hides', async () => {
@@ -1873,6 +1876,22 @@ describe('OverlayController', () => {
1873
1876
  it.skip('adds attributes inert and aria-hidden="true" on all siblings of rootNode if an overlay is shown', async () => {});
1874
1877
  it.skip('disables pointer events and selection on inert elements', async () => {});
1875
1878
 
1879
+ describe('Alert dialog', () => {
1880
+ it('sets role="alertdialog" when isAlertDialog is set', async () => {
1881
+ const invokerNode = /** @type {HTMLElement} */ (
1882
+ await fixture('<div role="button">invoker</div>')
1883
+ );
1884
+ const ctrl = new OverlayController({
1885
+ ...withLocalTestConfig(),
1886
+ handlesAccessibility: true,
1887
+ isAlertDialog: true,
1888
+ invokerNode,
1889
+ });
1890
+
1891
+ expect(ctrl.contentNode?.getAttribute('role')).to.equal('alertdialog');
1892
+ });
1893
+ });
1894
+
1876
1895
  describe('Tooltip', () => {
1877
1896
  it('adds [aria-describedby] on invoker', async () => {
1878
1897
  const invokerNode = /** @type {HTMLElement} */ (
@@ -1919,6 +1938,19 @@ describe('OverlayController', () => {
1919
1938
  expect(ctrl.contentNode.getAttribute('role')).to.equal('tooltip');
1920
1939
  });
1921
1940
 
1941
+ it('adds tabindex="-1" to the wrappingDialog element', async () => {
1942
+ const invokerNode = /** @type {HTMLElement} */ (
1943
+ await fixture('<div role="button">invoker</div>')
1944
+ );
1945
+ const ctrl = new OverlayController({
1946
+ ...withLocalTestConfig(),
1947
+ handlesAccessibility: true,
1948
+ isTooltip: true,
1949
+ invokerNode,
1950
+ });
1951
+ expect(ctrl.__wrappingDialogNode?.getAttribute('tabindex')).to.equal('-1');
1952
+ });
1953
+
1922
1954
  describe('Teardown', () => {
1923
1955
  it('restores [role] on dialog content', async () => {
1924
1956
  const invokerNode = /** @type {HTMLElement} */ (
@@ -1,18 +1,25 @@
1
- import { expect, fixture, html, nextFrame, aTimeout } from '@open-wc/testing';
2
-
3
- import sinon from 'sinon';
1
+ import {
2
+ unsafeStatic,
3
+ nextFrame,
4
+ aTimeout,
5
+ defineCE,
6
+ fixture,
7
+ expect,
8
+ html,
9
+ } from '@open-wc/testing';
4
10
  import { overlays as overlaysManager, OverlayController } from '@lion/ui/overlays.js';
5
-
6
- import '@lion/ui/define/lion-dialog.js';
7
11
  import { browserDetection } from '@lion/ui/core.js';
12
+ import { cache } from 'lit/directives/cache.js';
13
+ import '@lion/ui/define/lion-dialog.js';
14
+ import { LitElement } from 'lit';
15
+ import sinon from 'sinon';
8
16
 
9
17
  /**
10
- * @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig
11
18
  * @typedef {import('../types/OverlayMixinTypes.js').DefineOverlayConfig} DefineOverlayConfig
12
- * @typedef {import('../types/OverlayMixinTypes.js').OverlayHost} OverlayHost
13
- * @typedef {import('../types/OverlayMixinTypes.js').OverlayMixin} OverlayMixin
14
- * @typedef {import('lit').LitElement} LitElement
15
19
  * @typedef {LitElement & OverlayHost & {_overlayCtrl:OverlayController}} OverlayEl
20
+ * @typedef {import('../types/OverlayMixinTypes.js').OverlayMixin} OverlayMixin
21
+ * @typedef {import('../types/OverlayMixinTypes.js').OverlayHost} OverlayHost
22
+ * @typedef {import('../types/OverlayConfig.js').OverlayConfig} OverlayConfig
16
23
  */
17
24
 
18
25
  function getGlobalOverlayCtrls() {
@@ -331,6 +338,173 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
331
338
  }).to.not.throw;
332
339
  stub.restore();
333
340
  });
341
+
342
+ describe('Teardown', () => {
343
+ it('does not teardown when moving dom nodes', async () => {
344
+ const el = /** @type {OverlayEl} */ (
345
+ await fixture(html`
346
+ <${tag}>
347
+ <div slot="content">content</div>
348
+ <button slot="invoker">invoker button</button>
349
+ </${tag}>
350
+ `)
351
+ );
352
+ // @ts-expect-error [allow-protected] in tests
353
+ const teardownSpy = sinon.spy(el, '_teardownOverlayCtrl');
354
+ // @ts-expect-error [allow-protected] in tests
355
+ const spyDisconnected = sinon.spy(el, '_isPermanentlyDisconnected');
356
+
357
+ // move around in dom
358
+ const newParent = document.createElement('div');
359
+ document.body.appendChild(newParent);
360
+ newParent.appendChild(el);
361
+ expect(spyDisconnected.callCount).to.equal(1);
362
+
363
+ const otherParent = document.createElement('div');
364
+ document.body.appendChild(otherParent);
365
+ otherParent.appendChild(el);
366
+ await aTimeout(0);
367
+
368
+ expect(spyDisconnected.callCount).to.equal(2);
369
+ expect(teardownSpy.callCount).to.equal(0);
370
+
371
+ // simulate a permanent disconnect
372
+ otherParent.removeChild(el);
373
+ await aTimeout(0);
374
+
375
+ expect(teardownSpy.callCount).to.equal(1);
376
+ });
377
+
378
+ it('allows to disconnect and reconnect later', async () => {
379
+ const el = /** @type {OverlayEl} */ (
380
+ await fixture(html`
381
+ <${tag}>
382
+ <div slot="content">content</div>
383
+ <button slot="invoker">invoker button</button>
384
+ </${tag}>
385
+ `)
386
+ );
387
+ // @ts-expect-error [allow-protected] in tests
388
+ const teardownSpy = sinon.spy(el, '_teardownOverlayCtrl');
389
+ // @ts-expect-error [allow-protected] in tests
390
+ const spyDisconnected = sinon.spy(el, '_isPermanentlyDisconnected');
391
+ // @ts-expect-error [allow-protected] in tests
392
+ const setupSpy = sinon.spy(el, '_setupOverlayCtrl');
393
+
394
+ // 'permanently' disconnect (although we will reconnect later)
395
+ const offlineParent = document.createElement('div');
396
+ offlineParent.appendChild(el);
397
+ await aTimeout(0);
398
+
399
+ expect(spyDisconnected.callCount).to.equal(1);
400
+ expect(teardownSpy.callCount).to.equal(1);
401
+
402
+ // reconnect
403
+ document.body.appendChild(offlineParent);
404
+ await aTimeout(0);
405
+
406
+ expect(setupSpy.callCount).to.equal(1);
407
+ });
408
+
409
+ it('works with cache directive when reconnected', async () => {
410
+ class CachingContext extends LitElement {
411
+ static properties = {
412
+ switched: { type: Boolean },
413
+ };
414
+
415
+ constructor() {
416
+ super();
417
+
418
+ this.switched = false;
419
+ }
420
+
421
+ get overlayEl() {
422
+ return this.shadowRoot?.querySelector('#myOverlay');
423
+ }
424
+
425
+ render() {
426
+ return html`${cache(
427
+ this.switched
428
+ ? html`something else`
429
+ : html`<${tag} id="myOverlay">
430
+ <div slot="content">content b</div>
431
+ <button slot="invoker">invoker button b</button>
432
+ </${tag}>`,
433
+ )}`;
434
+ }
435
+ }
436
+
437
+ const cachingTagName = defineCE(CachingContext);
438
+ const cachingTag = unsafeStatic(cachingTagName);
439
+
440
+ const el = /** @type {CachingContext} */ (
441
+ await fixture(html`
442
+ <${cachingTag}></${cachingTag}>
443
+ `)
444
+ );
445
+
446
+ // @ts-expect-error [allow-protected] in tests
447
+ const teardownSpy = sinon.spy(el.overlayEl, '_teardownOverlayCtrl');
448
+ // @ts-expect-error [allow-protected] in tests
449
+ const spyDisconnected = sinon.spy(el.overlayEl, '_isPermanentlyDisconnected');
450
+ // @ts-expect-error [allow-protected] in tests
451
+ const setupSpy = sinon.spy(el.overlayEl, '_setupOverlayCtrl');
452
+
453
+ el.switched = true;
454
+ // render the new content
455
+ await el.updateComplete;
456
+ // wait for the teardown to complete
457
+ await el.updateComplete;
458
+
459
+ expect(spyDisconnected.callCount).to.equal(1);
460
+ expect(teardownSpy.callCount).to.equal(1);
461
+ expect(setupSpy.callCount).to.equal(0);
462
+
463
+ el.switched = false;
464
+ await el.updateComplete;
465
+
466
+ expect(setupSpy.callCount).to.equal(1);
467
+ });
468
+
469
+ it('correctly removes event listeners when disconnected from dom', async () => {
470
+ const el = /** @type {OverlayEl} */ (
471
+ await fixture(html`
472
+ <${tag}>
473
+ <div slot="content">content</div>
474
+ <button slot="invoker">invoker button</button>
475
+ </${tag}>
476
+ `)
477
+ );
478
+
479
+ const eventRemoveSpy = sinon.spy(el._overlayCtrl, 'removeEventListener');
480
+ el.parentNode?.removeChild(el);
481
+ await el.updateComplete;
482
+
483
+ expect(eventRemoveSpy.callCount).to.equal(0);
484
+
485
+ await el.updateComplete;
486
+ const hasOneInstanceFor = (
487
+ /** @type {string} */ eventName,
488
+ /** @type {any[][]} */ eventsToSearch,
489
+ /** @type {string} */ methodToRemove,
490
+ ) =>
491
+ Boolean(
492
+ eventsToSearch.filter(
493
+ ([eventToSearch, methodToSearch]) =>
494
+ eventToSearch === eventName && methodToSearch === methodToRemove,
495
+ ).length,
496
+ );
497
+
498
+ // @ts-expect-error [allow-private] in tests
499
+ expect(hasOneInstanceFor('show', eventRemoveSpy.args, el.__onOverlayCtrlShow)).to.be.true;
500
+ // @ts-expect-error [allow-private] in tests
501
+ expect(hasOneInstanceFor('hide', eventRemoveSpy.args, el.__onOverlayCtrlHide)).to.be.true;
502
+ // @ts-expect-error [allow-private] in tests
503
+ expect(hasOneInstanceFor('before-show', eventRemoveSpy.args, el.__onBeforeShow)).to.be.true;
504
+ // @ts-expect-error [allow-private] in tests
505
+ expect(hasOneInstanceFor('before-hide', eventRemoveSpy.args, el.__onBeforeHide)).to.be.true;
506
+ });
507
+ });
334
508
  });
335
509
 
336
510
  describe(`OverlayMixin${suffix} nested`, () => {
@@ -388,6 +562,7 @@ export function runOverlayMixinSuite({ tagString, tag, suffix = '' }) {
388
562
  const moveTarget = /** @type {OverlayEl} */ (await fixture('<div id="target"></div>'));
389
563
  moveTarget.appendChild(el);
390
564
  await el.updateComplete;
565
+
391
566
  expect(getGlobalOverlayCtrls().length).to.equal(1);
392
567
  }
393
568
  });
@@ -4,25 +4,34 @@ import { Options as PopperOptions, State } from '@popperjs/core/lib/popper.js';
4
4
  import { OverlayConfig } from './OverlayConfig.js';
5
5
 
6
6
  export declare class ArrowHost {
7
- static get properties(): {
7
+ static properties: {
8
8
  hasArrow: {
9
9
  type: BooleanConstructor;
10
10
  reflect: boolean;
11
11
  attribute: string;
12
12
  };
13
13
  };
14
+
14
15
  hasArrow: boolean;
16
+
15
17
  repositionComplete: Promise<void>;
16
18
 
17
- static get styles(): CSSResultArray;
19
+ static styles: CSSResultArray;
18
20
 
19
21
  render(): TemplateResult;
22
+
20
23
  protected _arrowTemplate(): TemplateResult;
24
+
21
25
  protected _arrowNodeTemplate(): TemplateResult;
26
+
22
27
  protected _defineOverlayConfig(): OverlayConfig;
28
+
23
29
  protected _getPopperArrowConfig(popperConfigToExtendFrom: Partial<PopperOptions>): Partial<PopperOptions>;
30
+
24
31
  private __setupRepositionCompletePromise(): void;
32
+
25
33
  get _arrowNode(): Element | null;
34
+
26
35
  private __syncFromPopperState(data: Partial<State>): void;
27
36
  }
28
37
 
@@ -1,5 +1,20 @@
1
1
  import { Options } from '@popperjs/core';
2
2
 
3
+ export type ViewportPlacement =
4
+ | 'center'
5
+ | 'top-left'
6
+ | 'top'
7
+ | 'top-right'
8
+ | 'right'
9
+ | 'bottom-right'
10
+ | 'bottom'
11
+ | 'bottom-left'
12
+ | 'left';
13
+
14
+
15
+ export interface ViewportConfig {
16
+ placement: ViewportPlacement;
17
+ }
3
18
  export interface OverlayConfig {
4
19
  // Positioning
5
20
 
@@ -26,7 +41,7 @@ export interface OverlayConfig {
26
41
  contentNode?: HTMLElement;
27
42
  /** The wrapper element of contentNode, used to supply inline positioning styles. When a Popper arrow is needed, it acts as parent of the arrow node. Will be automatically created for global and non projected contentNodes. Required when used in shadow dom mode or when Popper arrow is supplied. Essential for allowing webcomponents to style their projected contentNodes */
28
43
  contentWrapperNode?: HTMLElement;
29
- /** The element that is placed behin the contentNode. When not provided and `hasBackdrop` is true, a backdropNode will be automatically created */
44
+ /** The element that is placed behind the contentNode. When not provided and `hasBackdrop` is true, a backdropNode will be automatically created */
30
45
  backdropNode?: HTMLElement;
31
46
  /** The element that should be called `.focus()` on after dialog closes */
32
47
  elementToFocusAfterHide?: HTMLElement;
@@ -44,7 +59,7 @@ export interface OverlayConfig {
44
59
  trapsKeyboardFocus?: boolean;
45
60
  /** Hides the overlay when pressing [ esc ] */
46
61
  hidesOnEsc?: boolean;
47
- /** Hides the overlay when clicking next to it, exluding invoker */
62
+ /** Hides the overlay when clicking next to it, excluding invoker */
48
63
  hidesOnOutsideClick?: boolean;
49
64
  /** Hides the overlay when pressing esc, even when contentNode has no focus */
50
65
  hidesOnOutsideEsc?: boolean;
@@ -67,6 +82,8 @@ export interface OverlayConfig {
67
82
 
68
83
  /** Has a totally different interaction- and accessibility pattern from all other overlays. Will behave as role="tooltip" element instead of a role="dialog" element */
69
84
  isTooltip?: boolean;
85
+ /** The alertdialog role is to be used on modal alert dialogs that interrupt a user's workflow to communicate an important message and require a response. */
86
+ isAlertDialog?: boolean;
70
87
  /** 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) */
71
88
  invokerRelation?: 'label' | 'description';
72
89
 
@@ -80,17 +97,4 @@ export interface OverlayConfig {
80
97
  visibilityTriggerFunction?: Function;
81
98
  }
82
99
 
83
- export type ViewportPlacement =
84
- | 'center'
85
- | 'top-left'
86
- | 'top'
87
- | 'top-right'
88
- | 'right'
89
- | 'bottom-right'
90
- | 'bottom'
91
- | 'bottom-left'
92
- | 'left';
93
100
 
94
- export interface ViewportConfig {
95
- placement: ViewportPlacement;
96
- }
@@ -423,6 +423,7 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
423
423
  */
424
424
  _teardownOverlayCtrl() {
425
425
  super._teardownOverlayCtrl();
426
+
426
427
  this._overlayCtrl.removeEventListener('show', this.__overlayOnShow);
427
428
  this._overlayCtrl.removeEventListener('before-show', this.__overlayBeforeShow);
428
429
  this._overlayCtrl.removeEventListener('hide', this.__overlayOnHide);
@@ -434,11 +435,12 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
434
435
  * @protected
435
436
  */
436
437
  async _alignInvokerWidth() {
438
+ await this.updateComplete;
439
+
437
440
  if (!this._overlayCtrl?.content) {
438
441
  return;
439
442
  }
440
443
 
441
- await this.updateComplete;
442
444
  const initContentDisplay = this._overlayCtrl.content.style.display;
443
445
  const initContentMinWidth = this._overlayCtrl.contentWrapperNode.style.minWidth;
444
446
  const initContentWidth = this._overlayCtrl.contentWrapperNode.style.width;
@@ -7,6 +7,7 @@ import '@lion/ui/define/lion-select-rich.js';
7
7
  import '@lion/ui/define/lion-listbox.js';
8
8
  import '@lion/ui/define/lion-option.js';
9
9
  import { LitElement } from 'lit';
10
+ import sinon from 'sinon';
10
11
  import {
11
12
  fixture as _fixture,
12
13
  unsafeStatic,
@@ -711,6 +712,49 @@ describe('lion-select-rich', () => {
711
712
  });
712
713
  });
713
714
 
715
+ describe('Teardown', () => {
716
+ it('correctly removes event listeners when disconnected from dom', async () => {
717
+ const el = await fixture(html`
718
+ <lion-select-rich label="age">
719
+ <lion-option .choiceValue=${10}>Item 1</lion-option>
720
+ <lion-option .choiceValue=${20}>Item 2</lion-option>
721
+ </lion-select-rich>
722
+ `);
723
+
724
+ const { _overlayCtrl } = getSelectRichMembers(el);
725
+ const eventRemoveSpy = sinon.spy(_overlayCtrl, 'removeEventListener');
726
+
727
+ el.parentNode?.removeChild(el);
728
+
729
+ expect(eventRemoveSpy.callCount).to.equal(0);
730
+
731
+ await el.updateComplete;
732
+ expect(eventRemoveSpy.callCount).to.equal(0);
733
+
734
+ await el.updateComplete;
735
+
736
+ const hasOneInstanceFor = (
737
+ /** @type {string} */ eventName,
738
+ /** @type {any[][]} */ eventsToSearch,
739
+ /** @type {string} */ methodToRemove,
740
+ ) =>
741
+ Boolean(
742
+ eventsToSearch.filter(
743
+ ([eventToSearch, methodToSearch]) =>
744
+ eventToSearch === eventName && methodToSearch === methodToRemove,
745
+ ).length,
746
+ );
747
+
748
+ // @ts-expect-error [allow-private] in tests
749
+ expect(hasOneInstanceFor('show', eventRemoveSpy.args, el.__overlayOnShow)).to.be.true;
750
+ // @ts-expect-error [allow-private] in tests
751
+ expect(hasOneInstanceFor('before-show', eventRemoveSpy.args, el.__overlayBeforeShow)).to.be
752
+ .true;
753
+ // @ts-expect-error [allow-private] in tests
754
+ expect(hasOneInstanceFor('hide', eventRemoveSpy.args, el.__overlayOnHide)).to.be.true;
755
+ });
756
+ });
757
+
714
758
  describe('Subclassers', () => {
715
759
  it('allows to override the type of overlay', async () => {
716
760
  const mySelectTagString = defineCE(