@lion/ui 0.0.10 → 0.0.12

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 (89) hide show
  1. package/components/accordion/src/LionAccordion.js +2 -2
  2. package/components/dialog/src/LionDialog.js +0 -28
  3. package/components/dialog/test/lion-dialog.test.js +10 -5
  4. package/components/input-datepicker/src/LionInputDatepicker.js +2 -4
  5. package/components/localize/src/singleton.js +1 -1
  6. package/components/overlays/src/OverlayController.js +73 -14
  7. package/components/overlays/src/configurations/visibility-trigger-partials/withClickInteraction.js +32 -0
  8. package/components/overlays/src/configurations/visibility-trigger-partials/withHoverInteraction.js +76 -0
  9. package/components/overlays/src/configurations/withBottomSheetConfig.js +3 -0
  10. package/components/overlays/src/configurations/withDropdownConfig.js +3 -0
  11. package/components/overlays/src/configurations/withModalDialogConfig.js +3 -0
  12. package/components/overlays/src/configurations/withTooltipConfig.js +29 -0
  13. package/components/overlays/test/OverlayController.test.js +88 -7
  14. package/components/overlays/types/OverlayConfig.ts +6 -0
  15. package/components/select-rich/src/LionSelectRich.js +2 -0
  16. package/components/tooltip/src/LionTooltip.js +18 -91
  17. package/components/tooltip/test/lion-tooltip.test.js +50 -19
  18. package/custom-elements.json +307 -220
  19. package/dist-types/components/dialog/src/LionDialog.d.ts +0 -2
  20. package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
  21. package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts.map +1 -1
  22. package/dist-types/components/overlays/src/OverlayController.d.ts +11 -2
  23. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  24. package/dist-types/components/overlays/src/configurations/visibility-trigger-partials/withClickInteraction.d.ts +12 -0
  25. package/dist-types/components/overlays/src/configurations/visibility-trigger-partials/withClickInteraction.d.ts.map +1 -0
  26. package/dist-types/components/overlays/src/configurations/visibility-trigger-partials/withHoverInteraction.d.ts +18 -0
  27. package/dist-types/components/overlays/src/configurations/visibility-trigger-partials/withHoverInteraction.d.ts.map +1 -0
  28. package/dist-types/components/overlays/src/configurations/withBottomSheetConfig.d.ts.map +1 -1
  29. package/dist-types/components/overlays/src/configurations/withDropdownConfig.d.ts.map +1 -1
  30. package/dist-types/components/overlays/src/configurations/withModalDialogConfig.d.ts.map +1 -1
  31. package/dist-types/components/overlays/src/configurations/withTooltipConfig.d.ts +8 -0
  32. package/dist-types/components/overlays/src/configurations/withTooltipConfig.d.ts.map +1 -0
  33. package/dist-types/components/overlays/types/OverlayConfig.d.ts +5 -0
  34. package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
  35. package/dist-types/components/select-rich/src/LionSelectRich.d.ts +32 -0
  36. package/dist-types/components/select-rich/src/LionSelectRich.d.ts.map +1 -1
  37. package/dist-types/components/tooltip/src/LionTooltip.d.ts +4 -20
  38. package/dist-types/components/tooltip/src/LionTooltip.d.ts.map +1 -1
  39. package/dist-types/components/tooltip/test/lion-tooltip.test.d.ts.map +1 -1
  40. package/dist-types/exports/overlays.d.ts +1 -0
  41. package/dist-types/tsconfig.tsbuildinfo +1 -1
  42. package/docs/components/checkbox-group/overview.md +2 -2
  43. package/docs/components/collapsible/overview.md +1 -1
  44. package/docs/components/combobox/overview.md +1 -1
  45. package/docs/components/combobox/use-cases.md +1 -1
  46. package/docs/components/dialog/overview.md +1 -1
  47. package/docs/components/fieldset/overview.md +5 -5
  48. package/docs/components/form/overview.md +3 -3
  49. package/docs/components/icon/use-cases.md +1 -1
  50. package/docs/components/input/overview.md +1 -1
  51. package/docs/components/input-amount/overview.md +3 -3
  52. package/docs/components/input-date/overview.md +3 -3
  53. package/docs/components/input-datepicker/overview.md +5 -5
  54. package/docs/components/input-email/overview.md +2 -2
  55. package/docs/components/input-iban/overview.md +2 -2
  56. package/docs/components/input-range/overview.md +2 -2
  57. package/docs/components/input-stepper/overview.md +1 -1
  58. package/docs/components/input-tel/overview.md +1 -1
  59. package/docs/components/input-tel/use-cases.md +6 -6
  60. package/docs/components/input-tel-dropdown/overview.md +1 -1
  61. package/docs/components/radio-group/overview.md +3 -3
  62. package/docs/components/select-rich/use-cases.md +1 -1
  63. package/docs/components/tabs/examples.md +1 -1
  64. package/docs/fundamentals/index.md +4 -4
  65. package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
  66. package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
  67. package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
  68. package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
  69. package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
  70. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  71. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  72. package/docs/fundamentals/systems/core/overview.md +1 -1
  73. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  74. package/docs/fundamentals/systems/form/model-value.md +1 -1
  75. package/docs/fundamentals/systems/form/overview.md +28 -28
  76. package/docs/fundamentals/systems/form/validate.md +1 -1
  77. package/docs/fundamentals/systems/localize/overview.md +4 -4
  78. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  79. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  80. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  81. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  82. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  83. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  84. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  85. package/docs/guides/index.md +8 -8
  86. package/docs/guides/principles/index.md +5 -5
  87. package/docs/guides/principles/styling.md +1 -1
  88. package/exports/overlays.js +1 -0
  89. package/package.json +1 -1
@@ -210,10 +210,10 @@ export class LionAccordion extends LitElement {
210
210
  */
211
211
  __rearrangeInvokersAndContent() {
212
212
  const invokers = /** @type {HTMLElement[]} */ (
213
- Array.from(this.querySelectorAll('[slot="invoker"]'))
213
+ Array.from(this.children).filter(child => child.slot === 'invoker')
214
214
  );
215
215
  const contents = /** @type {HTMLElement[]} */ (
216
- Array.from(this.querySelectorAll('[slot="content"]'))
216
+ Array.from(this.children).filter(child => child.slot === 'content')
217
217
  );
218
218
  const accordion = this.shadowRoot?.querySelector('slot[name=_accordion]');
219
219
  if (accordion) {
@@ -2,14 +2,6 @@ import { html, LitElement } from 'lit';
2
2
  import { OverlayMixin, withModalDialogConfig } from '@lion/ui/overlays.js';
3
3
 
4
4
  export class LionDialog extends OverlayMixin(LitElement) {
5
- constructor() {
6
- super();
7
- /** @private */
8
- this.__toggle = () => {
9
- this.opened = !this.opened;
10
- };
11
- }
12
-
13
5
  /**
14
6
  * @protected
15
7
  */
@@ -20,26 +12,6 @@ export class LionDialog extends OverlayMixin(LitElement) {
20
12
  };
21
13
  }
22
14
 
23
- /**
24
- * @protected
25
- */
26
- _setupOpenCloseListeners() {
27
- super._setupOpenCloseListeners();
28
- if (this._overlayInvokerNode) {
29
- this._overlayInvokerNode.addEventListener('click', this.__toggle);
30
- }
31
- }
32
-
33
- /**
34
- * @protected
35
- */
36
- _teardownOpenCloseListeners() {
37
- super._teardownOpenCloseListeners();
38
- if (this._overlayInvokerNode) {
39
- this._overlayInvokerNode.removeEventListener('click', this.__toggle);
40
- }
41
- }
42
-
43
15
  render() {
44
16
  return html`
45
17
  <slot name="invoker"></slot>
@@ -39,7 +39,8 @@ describe('lion-dialog', () => {
39
39
  `);
40
40
  const invoker = /** @type {HTMLElement} */ (el.querySelector('[slot="invoker"]'));
41
41
  invoker.click();
42
-
42
+ // @ts-expect-error [allow-protected-in-tests]
43
+ await el._overlayCtrl._showComplete;
43
44
  expect(el.opened).to.be.true;
44
45
  });
45
46
 
@@ -57,14 +58,18 @@ describe('lion-dialog', () => {
57
58
  </lion-dialog>
58
59
  `);
59
60
 
60
- // @ts-expect-error [allow-protected] in tests
61
+ // @ts-expect-error [allow-protected-in-tests]
61
62
  el._overlayInvokerNode.click();
63
+ // @ts-expect-error [allow-protected-in-tests]
64
+ await el._overlayCtrl._showComplete;
62
65
  expect(el.opened).to.be.true;
63
66
 
64
- const nestedDialog = /** @type {LionDialog} */ (el.querySelector('lion-dialog'));
67
+ const nestedDialogEl = /** @type {LionDialog} */ (el.querySelector('lion-dialog'));
65
68
  // @ts-expect-error you're not allowed to call protected _overlayInvokerNode in public context, but for testing it's okay
66
- nestedDialog?._overlayInvokerNode.click();
67
- expect(nestedDialog.opened).to.be.true;
69
+ nestedDialogEl?._overlayInvokerNode.click();
70
+ // @ts-expect-error [allow-protected-in-tests]
71
+ await nestedDialogEl._overlayCtrl._showComplete;
72
+ expect(nestedDialogEl.opened).to.be.true;
68
73
  });
69
74
  });
70
75
  });
@@ -305,6 +305,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(
305
305
  return {
306
306
  ...withModalDialogConfig(),
307
307
  hidesOnOutsideClick: true,
308
+ visibilityTriggerFunction: undefined,
308
309
  ...super._defineOverlayConfig(),
309
310
  popperConfig: {
310
311
  ...super._defineOverlayConfig().popperConfig,
@@ -379,10 +380,7 @@ export class LionInputDatepicker extends ScopedElementsMixin(
379
380
  // On every validator change, synchronize disabled dates: this means
380
381
  // we need to extract minDate, maxDate, minMaxDate and disabledDates validators
381
382
  validators.forEach(v => {
382
- const vctor =
383
- /** @type {typeof import('../../form-core/src/validate/Validator.js').Validator} */ (
384
- v.constructor
385
- );
383
+ const vctor = /** @type {typeof import('@lion/ui/form-core.js').Validator} */ (v.constructor);
386
384
  if (vctor.validatorName === 'MinDate') {
387
385
  this.__calendarMinDate = v.param;
388
386
  } else if (vctor.validatorName === 'MaxDate') {
@@ -4,7 +4,7 @@ import { LocalizeManager } from './LocalizeManager.js';
4
4
  /** @type {LocalizeManager} */
5
5
  // eslint-disable-next-line import/no-mutable-exports
6
6
  export const localize =
7
- singletonManager.get('@lion/ui::localize::0.x') ||
7
+ /** @type {LocalizeManager} */ (singletonManager.get('@lion/ui::localize::0.x')) ||
8
8
  new LocalizeManager({
9
9
  autoLoadOnLocaleChange: true,
10
10
  fallbackLocale: 'en-GB',
@@ -1,4 +1,3 @@
1
- import { EventTargetShim } from '@lion/ui/core.js';
2
1
  import { adoptStyles } from 'lit';
3
2
  import { overlays } from './singleton.js';
4
3
  import { containFocus } from './utils/contain-focus.js';
@@ -14,6 +13,22 @@ import { overlayShadowDomStyle } from './overlayShadowDomStyle.js';
14
13
  * @typedef {'setup'|'init'|'teardown'|'before-show'|'show'|'hide'|'add'|'remove'} OverlayPhase
15
14
  */
16
15
 
16
+ const rootNodeStylesMap = new WeakSet();
17
+
18
+ /**
19
+ * Returns element that adopts stylesheet
20
+ * @param {Element} shadowOrBodyEl
21
+ * @returns {ShadowRoot}
22
+ */
23
+ function getRootNodeOrBodyElThatAdoptsStylesheet(shadowOrBodyEl) {
24
+ const rootNode = /** @type {* & DocumentOrShadowRoot} */ (shadowOrBodyEl.getRootNode());
25
+ if (rootNode === document) {
26
+ // @ts-ignore
27
+ return document.body;
28
+ }
29
+ return rootNode;
30
+ }
31
+
17
32
  /**
18
33
  * From:
19
34
  * - wrappingDialogNodeL1: `<dialog role="none"/>`
@@ -42,7 +57,7 @@ function rearrangeNodes({ wrappingDialogNodeL1, contentWrapperNodeL2, contentNod
42
57
  }
43
58
 
44
59
  let parentElement;
45
- const tempMarker = document.createComment('temp-marker');
60
+ const tempMarker = document.createComment('tempMarker');
46
61
 
47
62
  if (contentWrapperNodeL2.isConnected) {
48
63
  // This is the case when contentWrapperNode (living in shadow dom, wrapping <slot name="my-content-outlet">) is already provided via controller.
@@ -113,7 +128,7 @@ const supportsCSSTypedObject = window.CSS?.number && document.body.attributeStyl
113
128
  * bottom/top/left/right sheets etc.
114
129
  *
115
130
  */
116
- export class OverlayController extends EventTargetShim {
131
+ export class OverlayController extends EventTarget {
117
132
  /**
118
133
  * @constructor
119
134
  * @param {OverlayConfig} config initial config. Will be remembered as shared config
@@ -150,7 +165,7 @@ export class OverlayController extends EventTargetShim {
150
165
  hidesOnOutsideClick: false,
151
166
  isTooltip: false,
152
167
  invokerRelation: 'description',
153
- // handlesUserInteraction: false,
168
+ visibilityTriggerFunction: undefined,
154
169
  handlesAccessibility: false,
155
170
  popperConfig: {
156
171
  placement: 'top',
@@ -403,6 +418,10 @@ export class OverlayController extends EventTargetShim {
403
418
  return /** @type {ViewportConfig} */ (this.config?.viewportConfig);
404
419
  }
405
420
 
421
+ get visibilityTriggerFunction() {
422
+ return /** @type {function} */ (this.config?.visibilityTriggerFunction);
423
+ }
424
+
406
425
  /**
407
426
  * @desc The element our local overlay will be positioned relative to.
408
427
  * @type {HTMLElement | undefined}
@@ -454,11 +473,9 @@ export class OverlayController extends EventTargetShim {
454
473
  ...(this.__sharedConfig.popperConfig || {}),
455
474
  ...(cfgToAdd.popperConfig || {}),
456
475
  modifiers: [
457
- ...((this._defaultConfig.popperConfig && this._defaultConfig.popperConfig.modifiers) ||
458
- []),
459
- ...((this.__sharedConfig.popperConfig && this.__sharedConfig.popperConfig.modifiers) ||
460
- []),
461
- ...((cfgToAdd.popperConfig && cfgToAdd.popperConfig.modifiers) || []),
476
+ ...(this._defaultConfig.popperConfig?.modifiers || []),
477
+ ...(this.__sharedConfig.popperConfig?.modifiers || []),
478
+ ...(cfgToAdd.popperConfig?.modifiers || []),
462
479
  ],
463
480
  },
464
481
  };
@@ -514,16 +531,24 @@ export class OverlayController extends EventTargetShim {
514
531
  this.contentWrapperNode.removeAttribute('class');
515
532
 
516
533
  if (this.placementMode === 'local') {
517
- // Lazily load Popper if not done yet
534
+ // Lazily load Popper as soon as the first local overlay is used...
518
535
  if (!OverlayController.popperModule) {
519
536
  OverlayController.popperModule = preloadPopper();
520
537
  }
521
538
  }
522
- const rootNode = /** @type {ShadowRoot} */ (this.contentWrapperNode.getRootNode());
523
- adoptStyles(rootNode, [...(rootNode.adoptedStyleSheets || []), overlayShadowDomStyle]);
539
+ this.__initOverlayStyles();
524
540
  this._handleFeatures({ phase: 'init' });
525
541
  }
526
542
 
543
+ __initOverlayStyles() {
544
+ const rootNode = getRootNodeOrBodyElThatAdoptsStylesheet(this.contentWrapperNode);
545
+ if (!rootNodeStylesMap.has(rootNode)) {
546
+ // TODO: ideally we should also support a teardown
547
+ adoptStyles(rootNode, [...(rootNode.adoptedStyleSheets || []), overlayShadowDomStyle]);
548
+ rootNodeStylesMap.add(rootNode);
549
+ }
550
+ }
551
+
527
552
  /**
528
553
  * Here we arrange our content node via:
529
554
  * 1. HTMLDialogElement: the content will always be painted to the browser's top layer
@@ -710,7 +735,7 @@ export class OverlayController extends EventTargetShim {
710
735
  this.dispatchEvent(event);
711
736
  if (!event.defaultPrevented) {
712
737
  if (this.__wrappingDialogNode instanceof HTMLDialogElement) {
713
- this.__wrappingDialogNode.show();
738
+ this.__wrappingDialogNode.open = true;
714
739
  }
715
740
  // @ts-ignore
716
741
  this.__wrappingDialogNode.style.display = '';
@@ -947,6 +972,26 @@ export class OverlayController extends EventTargetShim {
947
972
  if (this.inheritsReferenceWidth) {
948
973
  this._handleInheritsReferenceWidth();
949
974
  }
975
+ if (this.visibilityTriggerFunction) {
976
+ this._handleVisibilityTriggers({ phase });
977
+ }
978
+ }
979
+
980
+ /**
981
+ * @param {{ phase: OverlayPhase }} config
982
+ */
983
+ _handleVisibilityTriggers({ phase }) {
984
+ if (typeof this.visibilityTriggerFunction === 'function') {
985
+ if (phase === 'init') {
986
+ this.__visibilityTriggerHandler = this.visibilityTriggerFunction({
987
+ phase,
988
+ controller: this,
989
+ });
990
+ }
991
+ if (this.__visibilityTriggerHandler[phase]) {
992
+ this.__visibilityTriggerHandler[phase]();
993
+ }
994
+ }
950
995
  }
951
996
 
952
997
  /**
@@ -1009,10 +1054,14 @@ export class OverlayController extends EventTargetShim {
1009
1054
  break;
1010
1055
  }
1011
1056
  case 'show':
1012
- this.backdropNode.classList.add(`overlays__backdrop--visible`);
1057
+ if (this.config.hasBackdrop) {
1058
+ this.backdropNode.classList.add(`overlays__backdrop--visible`);
1059
+ }
1013
1060
  this.__hasActiveBackdrop = true;
1014
1061
  break;
1015
1062
  case 'hide':
1063
+ case 'teardown':
1064
+ this.backdropNode.classList.remove(`overlays__backdrop--visible`);
1016
1065
  this.__hasActiveBackdrop = false;
1017
1066
  break;
1018
1067
  }
@@ -1252,6 +1301,16 @@ export class OverlayController extends EventTargetShim {
1252
1301
  });
1253
1302
  }
1254
1303
  }
1304
+
1305
+ _hasDisabledInvoker() {
1306
+ if (this.invokerNode) {
1307
+ return (
1308
+ /** @type {HTMLElement & { disabled: boolean }} */ (this.invokerNode).disabled ||
1309
+ this.invokerNode.getAttribute('aria-disabled') === 'true'
1310
+ );
1311
+ }
1312
+ return false;
1313
+ }
1255
1314
  }
1256
1315
  /** @type {Promise<PopperModule> | undefined} */
1257
1316
  OverlayController.popperModule = undefined;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
3
+ * @typedef {import('@lion/ui/overlays.js').OverlayController} OverlayController
4
+ */
5
+
6
+ /**
7
+ * Use for popovers/dropdowns, (modal) dialogs etc...
8
+ * @returns {Partial<OverlayConfig>}
9
+ */
10
+ export function withClickInteraction() {
11
+ return {
12
+ visibilityTriggerFunction: (
13
+ /** @type {{ controller: OverlayController }} */ { controller },
14
+ ) => {
15
+ function handleOpenClosed() {
16
+ if (controller._hasDisabledInvoker()) {
17
+ return;
18
+ }
19
+ controller.toggle();
20
+ }
21
+
22
+ return {
23
+ init: () => {
24
+ controller.invokerNode?.addEventListener('click', handleOpenClosed);
25
+ },
26
+ teardown: () => {
27
+ controller.invokerNode?.removeEventListener('click', handleOpenClosed);
28
+ },
29
+ };
30
+ },
31
+ };
32
+ }
@@ -0,0 +1,76 @@
1
+ /**
2
+ * @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
3
+ * @typedef {import('@lion/ui/overlays.js').OverlayController} OverlayController
4
+ */
5
+
6
+ // N.B. Below logic is tested in LionTooltip
7
+
8
+ /**
9
+ * Use for tooltips and [flyout menus](https://www.w3.org/WAI/tutorials/menus/flyout/).
10
+ * Note that it handles both mouse hover and focus interaction.
11
+ * Provide delayIn and delayOut when the content needs a small delay before being showed
12
+ * @param {{ delayIn?: number, delayOut?: number }} options
13
+ * @returns {Partial<OverlayConfig>}
14
+ */
15
+ export function withHoverInteraction({ delayIn = 0, delayOut = 300 }) {
16
+ return {
17
+ visibilityTriggerFunction: (
18
+ /** @type {{ controller: OverlayController }} */ { controller },
19
+ ) => {
20
+ let isFocused = false;
21
+ let isHovered = false;
22
+ /** @type {NodeJS.Timeout} */
23
+ let delayTimeout;
24
+
25
+ function resetActive() {
26
+ isFocused = false;
27
+ isHovered = false;
28
+ }
29
+
30
+ /**
31
+ * @param {Event} event
32
+ */
33
+ function handleOpenClosed(event) {
34
+ const { type } = event;
35
+ if (controller._hasDisabledInvoker()) {
36
+ return;
37
+ }
38
+ clearTimeout(delayTimeout);
39
+ isFocused = type === 'focusout' ? false : isFocused || type === 'focusin';
40
+ isHovered = type === 'mouseleave' ? false : isHovered || type === 'mouseenter';
41
+ const shouldOpen = isFocused || isHovered;
42
+
43
+ if (shouldOpen) {
44
+ delayTimeout = setTimeout(() => {
45
+ controller.show();
46
+ }, delayIn);
47
+ } else {
48
+ delayTimeout = setTimeout(() => {
49
+ controller.hide();
50
+ }, delayOut);
51
+ }
52
+ }
53
+
54
+ return {
55
+ init: () => {
56
+ controller.addEventListener('hide', resetActive);
57
+ controller.contentNode?.addEventListener('mouseenter', handleOpenClosed);
58
+ controller.contentNode?.addEventListener('mouseleave', handleOpenClosed);
59
+ controller.invokerNode?.addEventListener('mouseenter', handleOpenClosed);
60
+ controller.invokerNode?.addEventListener('mouseleave', handleOpenClosed);
61
+ controller.invokerNode?.addEventListener('focusin', handleOpenClosed);
62
+ controller.invokerNode?.addEventListener('focusout', handleOpenClosed);
63
+ },
64
+ teardown: () => {
65
+ controller.removeEventListener('hide', resetActive);
66
+ controller.contentNode?.removeEventListener('mouseenter', handleOpenClosed);
67
+ controller.contentNode?.removeEventListener('mouseleave', handleOpenClosed);
68
+ controller.invokerNode?.removeEventListener('mouseenter', handleOpenClosed);
69
+ controller.invokerNode?.removeEventListener('mouseleave', handleOpenClosed);
70
+ controller.invokerNode?.removeEventListener('focusin', handleOpenClosed);
71
+ controller.invokerNode?.removeEventListener('focusout', handleOpenClosed);
72
+ },
73
+ };
74
+ },
75
+ };
76
+ }
@@ -1,3 +1,5 @@
1
+ import { withClickInteraction } from './visibility-trigger-partials/withClickInteraction.js';
2
+
1
3
  /**
2
4
  * @typedef {import('../../types/OverlayConfig.js').OverlayConfig} OverlayConfig
3
5
  */
@@ -13,4 +15,5 @@ export const withBottomSheetConfig = () =>
13
15
  placement: 'bottom',
14
16
  },
15
17
  handlesAccessibility: true,
18
+ ...withClickInteraction(),
16
19
  });
@@ -1,3 +1,5 @@
1
+ import { withClickInteraction } from './visibility-trigger-partials/withClickInteraction.js';
2
+
1
3
  /**
2
4
  * @typedef {import('../../types/OverlayConfig.js').OverlayConfig} OverlayConfig
3
5
  */
@@ -19,4 +21,5 @@ export const withDropdownConfig = () =>
19
21
  ],
20
22
  },
21
23
  handlesAccessibility: true,
24
+ ...withClickInteraction(),
22
25
  });
@@ -1,3 +1,5 @@
1
+ import { withClickInteraction } from './visibility-trigger-partials/withClickInteraction.js';
2
+
1
3
  /**
2
4
  * @typedef {import('../../types/OverlayConfig.js').OverlayConfig} OverlayConfig
3
5
  */
@@ -13,4 +15,5 @@ export const withModalDialogConfig = () =>
13
15
  trapsKeyboardFocus: true,
14
16
  hidesOnEsc: true,
15
17
  handlesAccessibility: true,
18
+ ...withClickInteraction(),
16
19
  });
@@ -0,0 +1,29 @@
1
+ import { withHoverInteraction } from './visibility-trigger-partials/withHoverInteraction.js';
2
+
3
+ /**
4
+ * @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
5
+ * @typedef {import('@lion/ui/overlays.js').OverlayController} OverlayController
6
+ */
7
+
8
+ /**
9
+ *
10
+ * @param {{invokerRelation?: 'description'| 'label', delayIn?: number, delayOut?: number }} options
11
+ */
12
+ export const withTooltipConfig = ({
13
+ invokerRelation = 'description',
14
+ delayIn = 300,
15
+ delayOut = 300,
16
+ } = {}) =>
17
+ /** @type {OverlayConfig} */ ({
18
+ placementMode: 'local',
19
+ elementToFocusAfterHide: undefined,
20
+ hidesOnEsc: true,
21
+ hidesOnOutsideEsc: true,
22
+ handlesAccessibility: true,
23
+ isTooltip: true,
24
+ invokerRelation,
25
+ popperConfig: {
26
+ strategy: 'absolute',
27
+ },
28
+ ...withHoverInteraction({ delayIn, delayOut }),
29
+ });
@@ -11,7 +11,7 @@ import {
11
11
  import sinon from 'sinon';
12
12
  import { OverlayController, overlays } from '@lion/ui/overlays.js';
13
13
  import { mimicClick } from '@lion/ui/overlays-test-helpers.js';
14
-
14
+ import { overlayShadowDomStyle } from '../src/overlayShadowDomStyle.js';
15
15
  import { keyCodes } from '../src/utils/key-codes.js';
16
16
  import { simulateTab } from '../src/utils/simulate-tab.js';
17
17
 
@@ -23,6 +23,22 @@ import { simulateTab } from '../src/utils/simulate-tab.js';
23
23
  const wrappingDialogNodeStyle = 'display: none; z-index: 9999;';
24
24
 
25
25
  /**
26
+ * Returns element that adopts stylesheet
27
+ * @param {Element} shadowOrBodyEl
28
+ * @returns {ShadowRoot}
29
+ */
30
+ function getRootNodeOrBodyElThatAdoptsStylesheet(shadowOrBodyEl) {
31
+ const rootNode = /** @type {* & DocumentOrShadowRoot} */ (shadowOrBodyEl.getRootNode());
32
+ if (rootNode === document) {
33
+ // @ts-ignore
34
+ return document.body;
35
+ }
36
+ return rootNode;
37
+ }
38
+
39
+ /**
40
+ * Make sure that all browsers serialize html in a similar way
41
+ * (Firefox tends to output empty style attrs)
26
42
  * @param {HTMLElement} node
27
43
  */
28
44
  function normalizeOverlayContentWapper(node) {
@@ -80,6 +96,55 @@ describe('OverlayController', () => {
80
96
  expect(ctrl.contentNode.parentElement).to.equal(ctrl.contentWrapperNode);
81
97
  });
82
98
 
99
+ describe('Stylesheets', () => {
100
+ it('adds a stylesheet to the body when contentWrapper is located there', async () => {
101
+ new OverlayController({
102
+ ...withLocalTestConfig(),
103
+ });
104
+ // @ts-ignore
105
+ if (document.body.adoptedStyleSheets) {
106
+ // @ts-ignore
107
+ expect(document.body.adoptedStyleSheets).to.include(overlayShadowDomStyle.styleSheet);
108
+ }
109
+ });
110
+
111
+ it('adds a stylesheet to the shadowRoot when contentWrappeNode is located there', async () => {
112
+ const contentNode = /** @type {HTMLElement} */ (await fixture('<div>contentful</div>'));
113
+ const shadowHost = document.createElement('div');
114
+ shadowHost.attachShadow({ mode: 'open' });
115
+ /** @type {ShadowRoot} */ (shadowHost.shadowRoot).innerHTML = `<slot></slot>`;
116
+ shadowHost.appendChild(contentNode);
117
+ document.body.appendChild(shadowHost);
118
+ const ctrl = new OverlayController({
119
+ ...withLocalTestConfig(),
120
+ contentNode,
121
+ });
122
+
123
+ const rootNodeOrBody = getRootNodeOrBodyElThatAdoptsStylesheet(ctrl.contentWrapperNode);
124
+ expect(rootNodeOrBody).to.not.equal(document.body);
125
+
126
+ if (rootNodeOrBody.adoptedStyleSheets) {
127
+ expect(rootNodeOrBody.adoptedStyleSheets).to.include(overlayShadowDomStyle.styleSheet);
128
+ }
129
+ document.body.removeChild(shadowHost);
130
+ });
131
+
132
+ it('does not add same stylesheet twice', async () => {
133
+ // @ts-ignore
134
+ if (!document.body.adoptedStyleSheets) {
135
+ return;
136
+ }
137
+
138
+ new OverlayController({ ...withLocalTestConfig() });
139
+ // @ts-ignore
140
+ const amountOfStylesheetsAfterOneInit = document.body.adoptedStyleSheets.length;
141
+
142
+ new OverlayController({ ...withLocalTestConfig() });
143
+ // @ts-ignore
144
+ expect(document.body.adoptedStyleSheets.length).to.equal(amountOfStylesheetsAfterOneInit);
145
+ });
146
+ });
147
+
83
148
  describe('Z-index on local overlays', () => {
84
149
  /** @type {HTMLElement} */
85
150
  let contentNode;
@@ -895,11 +960,11 @@ describe('OverlayController', () => {
895
960
  await fixture('<div><textarea></textarea></div>')
896
961
  );
897
962
 
898
- const shadowEl = document.createElement('div');
899
- shadowEl.attachShadow({ mode: 'open' });
900
- /** @type {ShadowRoot} */ (shadowEl.shadowRoot).innerHTML = `<slot></slot>`;
901
- shadowEl.appendChild(contentNode);
902
- document.body.appendChild(shadowEl);
963
+ const shadowHost = document.createElement('div');
964
+ shadowHost.attachShadow({ mode: 'open' });
965
+ /** @type {ShadowRoot} */ (shadowHost.shadowRoot).innerHTML = `<slot></slot>`;
966
+ shadowHost.appendChild(contentNode);
967
+ document.body.appendChild(shadowHost);
903
968
 
904
969
  const ctrl = new OverlayController({
905
970
  ...withGlobalTestConfig(),
@@ -915,7 +980,7 @@ describe('OverlayController', () => {
915
980
  await ctrl.hide();
916
981
  expect(document.activeElement).to.equal(input);
917
982
 
918
- document.body.removeChild(shadowEl);
983
+ document.body.removeChild(shadowHost);
919
984
  });
920
985
 
921
986
  it(`only sets focus when outside world didn't take over already`, async () => {
@@ -1450,6 +1515,22 @@ describe('OverlayController', () => {
1450
1515
  expect(ctrl.contentWrapperNode.classList.contains('overlays__overlay-container--top-right'));
1451
1516
  expect(ctrl.isShown).to.be.true;
1452
1517
  });
1518
+
1519
+ it('disables backdrop when switching to hasBackrop "false"', async () => {
1520
+ const ctrl = new OverlayController({
1521
+ ...withLocalTestConfig(),
1522
+ contentNode: /** @type {HTMLElement} */ (await fixture(html`<div>content1</div>`)),
1523
+ hasBackdrop: true,
1524
+ });
1525
+ await ctrl.show(); // Popper adds inline styles
1526
+ expect(ctrl.backdropNode).not.to.be.undefined;
1527
+ expect(Array.from(ctrl.backdropNode.classList)).to.include('overlays__backdrop--visible');
1528
+
1529
+ ctrl.updateConfig({
1530
+ hasBackdrop: false,
1531
+ });
1532
+ expect(Array.from(ctrl.backdropNode.classList)).to.not.include('overlays__backdrop--visible');
1533
+ });
1453
1534
  });
1454
1535
 
1455
1536
  describe('Accessibility', () => {
@@ -72,6 +72,12 @@ export interface OverlayConfig {
72
72
 
73
73
  /** render a div instead of dialog */
74
74
  _noDialogEl?: Boolean;
75
+
76
+ /**
77
+ * Determines the conditions hiding/showing should be based on. It gets the OverlayController as input and returns an object with
78
+ * functions with Overlay phases as keys
79
+ */
80
+ visibilityTriggerFunction?: Function;
75
81
  }
76
82
 
77
83
  export type ViewportPlacement =
@@ -336,6 +336,8 @@ export class LionSelectRich extends SlotMixin(ScopedElementsMixin(OverlayMixin(L
336
336
  _defineOverlayConfig() {
337
337
  return {
338
338
  ...withDropdownConfig(),
339
+ // Needs more advanced behavior (see `__invokerOnClick`)
340
+ visibilityTriggerFunction: undefined,
339
341
  };
340
342
  }
341
343