@lion/ui 0.0.10 → 0.0.11

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 (86) 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 +65 -11
  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 +72 -7
  14. package/components/overlays/types/OverlayConfig.ts +6 -0
  15. package/components/tooltip/src/LionTooltip.js +3 -90
  16. package/components/tooltip/test/lion-tooltip.test.js +52 -20
  17. package/custom-elements.json +296 -153
  18. package/dist-types/components/dialog/src/LionDialog.d.ts +0 -2
  19. package/dist-types/components/dialog/src/LionDialog.d.ts.map +1 -1
  20. package/dist-types/components/input-datepicker/src/LionInputDatepicker.d.ts.map +1 -1
  21. package/dist-types/components/overlays/src/OverlayController.d.ts +10 -0
  22. package/dist-types/components/overlays/src/OverlayController.d.ts.map +1 -1
  23. package/dist-types/components/overlays/src/configurations/visibility-trigger-partials/withClickInteraction.d.ts +12 -0
  24. package/dist-types/components/overlays/src/configurations/visibility-trigger-partials/withClickInteraction.d.ts.map +1 -0
  25. package/dist-types/components/overlays/src/configurations/visibility-trigger-partials/withHoverInteraction.d.ts +18 -0
  26. package/dist-types/components/overlays/src/configurations/visibility-trigger-partials/withHoverInteraction.d.ts.map +1 -0
  27. package/dist-types/components/overlays/src/configurations/withBottomSheetConfig.d.ts.map +1 -1
  28. package/dist-types/components/overlays/src/configurations/withDropdownConfig.d.ts.map +1 -1
  29. package/dist-types/components/overlays/src/configurations/withModalDialogConfig.d.ts.map +1 -1
  30. package/dist-types/components/overlays/src/configurations/withTooltipConfig.d.ts +8 -0
  31. package/dist-types/components/overlays/src/configurations/withTooltipConfig.d.ts.map +1 -0
  32. package/dist-types/components/overlays/types/OverlayConfig.d.ts +5 -0
  33. package/dist-types/components/overlays/types/OverlayConfig.d.ts.map +1 -1
  34. package/dist-types/components/tooltip/src/LionTooltip.d.ts +4 -20
  35. package/dist-types/components/tooltip/src/LionTooltip.d.ts.map +1 -1
  36. package/dist-types/components/tooltip/test/lion-tooltip.test.d.ts.map +1 -1
  37. package/dist-types/exports/overlays.d.ts +1 -0
  38. package/dist-types/tsconfig.tsbuildinfo +1 -1
  39. package/docs/components/checkbox-group/overview.md +2 -2
  40. package/docs/components/collapsible/overview.md +1 -1
  41. package/docs/components/combobox/overview.md +1 -1
  42. package/docs/components/combobox/use-cases.md +1 -1
  43. package/docs/components/dialog/overview.md +1 -1
  44. package/docs/components/fieldset/overview.md +5 -5
  45. package/docs/components/form/overview.md +3 -3
  46. package/docs/components/icon/use-cases.md +1 -1
  47. package/docs/components/input/overview.md +1 -1
  48. package/docs/components/input-amount/overview.md +3 -3
  49. package/docs/components/input-date/overview.md +3 -3
  50. package/docs/components/input-datepicker/overview.md +5 -5
  51. package/docs/components/input-email/overview.md +2 -2
  52. package/docs/components/input-iban/overview.md +2 -2
  53. package/docs/components/input-range/overview.md +2 -2
  54. package/docs/components/input-stepper/overview.md +1 -1
  55. package/docs/components/input-tel/overview.md +1 -1
  56. package/docs/components/input-tel/use-cases.md +6 -6
  57. package/docs/components/input-tel-dropdown/overview.md +1 -1
  58. package/docs/components/radio-group/overview.md +3 -3
  59. package/docs/components/select-rich/use-cases.md +1 -1
  60. package/docs/components/tabs/examples.md +1 -1
  61. package/docs/fundamentals/index.md +4 -4
  62. package/docs/fundamentals/node-tools/providence-analytics/LocalConfiguration.md +1 -1
  63. package/docs/fundamentals/node-tools/providence-analytics/QueryResult.md +1 -1
  64. package/docs/fundamentals/node-tools/providence-analytics/analyzer.md +1 -1
  65. package/docs/fundamentals/node-tools/providence-analytics/dashboard.md +1 -1
  66. package/docs/fundamentals/node-tools/providence-analytics/overview.md +3 -3
  67. package/docs/fundamentals/node-tools/publish-docs/overview.md +3 -3
  68. package/docs/fundamentals/node-tools/rocket-preset-extend-lion-docs/overview.md +2 -2
  69. package/docs/fundamentals/systems/core/overview.md +1 -1
  70. package/docs/fundamentals/systems/form/formatting-and-parsing.md +4 -4
  71. package/docs/fundamentals/systems/form/model-value.md +1 -1
  72. package/docs/fundamentals/systems/form/overview.md +28 -28
  73. package/docs/fundamentals/systems/form/validate.md +1 -1
  74. package/docs/fundamentals/systems/localize/overview.md +4 -4
  75. package/docs/fundamentals/systems/overlays/configuration.md +2 -2
  76. package/docs/fundamentals/systems/overlays/overview.md +5 -5
  77. package/docs/fundamentals/systems/overlays/use-cases.md +2 -2
  78. package/docs/fundamentals/tools/helpers/overview.md +1 -1
  79. package/docs/fundamentals/tools/singleton-manager/overview.md +4 -4
  80. package/docs/guides/how-to/create-a-custom-field.md +5 -5
  81. package/docs/guides/how-to/extend-a-native-input.md +2 -2
  82. package/docs/guides/index.md +8 -8
  83. package/docs/guides/principles/index.md +5 -5
  84. package/docs/guides/principles/styling.md +1 -1
  85. package/exports/overlays.js +1 -0
  86. 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',
@@ -14,6 +14,22 @@ import { overlayShadowDomStyle } from './overlayShadowDomStyle.js';
14
14
  * @typedef {'setup'|'init'|'teardown'|'before-show'|'show'|'hide'|'add'|'remove'} OverlayPhase
15
15
  */
16
16
 
17
+ const rootNodeStylesMap = new WeakSet();
18
+
19
+ /**
20
+ * Returns element that adopts stylesheet
21
+ * @param {Element} shadowOrBodyEl
22
+ * @returns {ShadowRoot}
23
+ */
24
+ function getRootNodeOrBodyElThatAdoptsStylesheet(shadowOrBodyEl) {
25
+ const rootNode = /** @type {* & DocumentOrShadowRoot} */ (shadowOrBodyEl.getRootNode());
26
+ if (rootNode === document) {
27
+ // @ts-ignore
28
+ return document.body;
29
+ }
30
+ return rootNode;
31
+ }
32
+
17
33
  /**
18
34
  * From:
19
35
  * - wrappingDialogNodeL1: `<dialog role="none"/>`
@@ -42,7 +58,7 @@ function rearrangeNodes({ wrappingDialogNodeL1, contentWrapperNodeL2, contentNod
42
58
  }
43
59
 
44
60
  let parentElement;
45
- const tempMarker = document.createComment('temp-marker');
61
+ const tempMarker = document.createComment('tempMarker');
46
62
 
47
63
  if (contentWrapperNodeL2.isConnected) {
48
64
  // This is the case when contentWrapperNode (living in shadow dom, wrapping <slot name="my-content-outlet">) is already provided via controller.
@@ -150,7 +166,7 @@ export class OverlayController extends EventTargetShim {
150
166
  hidesOnOutsideClick: false,
151
167
  isTooltip: false,
152
168
  invokerRelation: 'description',
153
- // handlesUserInteraction: false,
169
+ visibilityTriggerFunction: undefined,
154
170
  handlesAccessibility: false,
155
171
  popperConfig: {
156
172
  placement: 'top',
@@ -403,6 +419,10 @@ export class OverlayController extends EventTargetShim {
403
419
  return /** @type {ViewportConfig} */ (this.config?.viewportConfig);
404
420
  }
405
421
 
422
+ get visibilityTriggerFunction() {
423
+ return /** @type {function} */ (this.config?.visibilityTriggerFunction);
424
+ }
425
+
406
426
  /**
407
427
  * @desc The element our local overlay will be positioned relative to.
408
428
  * @type {HTMLElement | undefined}
@@ -454,11 +474,9 @@ export class OverlayController extends EventTargetShim {
454
474
  ...(this.__sharedConfig.popperConfig || {}),
455
475
  ...(cfgToAdd.popperConfig || {}),
456
476
  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) || []),
477
+ ...(this._defaultConfig.popperConfig?.modifiers || []),
478
+ ...(this.__sharedConfig.popperConfig?.modifiers || []),
479
+ ...(cfgToAdd.popperConfig?.modifiers || []),
462
480
  ],
463
481
  },
464
482
  };
@@ -514,16 +532,24 @@ export class OverlayController extends EventTargetShim {
514
532
  this.contentWrapperNode.removeAttribute('class');
515
533
 
516
534
  if (this.placementMode === 'local') {
517
- // Lazily load Popper if not done yet
535
+ // Lazily load Popper as soon as the first local overlay is used...
518
536
  if (!OverlayController.popperModule) {
519
537
  OverlayController.popperModule = preloadPopper();
520
538
  }
521
539
  }
522
- const rootNode = /** @type {ShadowRoot} */ (this.contentWrapperNode.getRootNode());
523
- adoptStyles(rootNode, [...(rootNode.adoptedStyleSheets || []), overlayShadowDomStyle]);
540
+ this.__initOverlayStyles();
524
541
  this._handleFeatures({ phase: 'init' });
525
542
  }
526
543
 
544
+ __initOverlayStyles() {
545
+ const rootNode = getRootNodeOrBodyElThatAdoptsStylesheet(this.contentWrapperNode);
546
+ if (!rootNodeStylesMap.has(rootNode)) {
547
+ // TODO: ideally we should also support a teardown
548
+ adoptStyles(rootNode, [...(rootNode.adoptedStyleSheets || []), overlayShadowDomStyle]);
549
+ rootNodeStylesMap.add(rootNode);
550
+ }
551
+ }
552
+
527
553
  /**
528
554
  * Here we arrange our content node via:
529
555
  * 1. HTMLDialogElement: the content will always be painted to the browser's top layer
@@ -710,7 +736,7 @@ export class OverlayController extends EventTargetShim {
710
736
  this.dispatchEvent(event);
711
737
  if (!event.defaultPrevented) {
712
738
  if (this.__wrappingDialogNode instanceof HTMLDialogElement) {
713
- this.__wrappingDialogNode.show();
739
+ this.__wrappingDialogNode.open = true;
714
740
  }
715
741
  // @ts-ignore
716
742
  this.__wrappingDialogNode.style.display = '';
@@ -947,6 +973,24 @@ export class OverlayController extends EventTargetShim {
947
973
  if (this.inheritsReferenceWidth) {
948
974
  this._handleInheritsReferenceWidth();
949
975
  }
976
+
977
+ if (this.visibilityTriggerFunction) {
978
+ this._handleUserInteraction({ phase });
979
+ }
980
+ }
981
+
982
+ /**
983
+ * @param {{ phase: OverlayPhase }} config
984
+ */
985
+ _handleUserInteraction({ phase }) {
986
+ if (typeof this.visibilityTriggerFunction === 'function') {
987
+ if (phase === 'init') {
988
+ this.__userInteractionHandler = this.visibilityTriggerFunction({ phase, controller: this });
989
+ }
990
+ if (this.__userInteractionHandler[phase]) {
991
+ this.__userInteractionHandler[phase]();
992
+ }
993
+ }
950
994
  }
951
995
 
952
996
  /**
@@ -1252,6 +1296,16 @@ export class OverlayController extends EventTargetShim {
1252
1296
  });
1253
1297
  }
1254
1298
  }
1299
+
1300
+ _hasDisabledInvoker() {
1301
+ if (this.invokerNode) {
1302
+ return (
1303
+ /** @type {HTMLElement & { disabled: boolean }} */ (this.invokerNode).disabled ||
1304
+ this.invokerNode.getAttribute('aria-disabled') === 'true'
1305
+ );
1306
+ }
1307
+ return false;
1308
+ }
1255
1309
  }
1256
1310
  /** @type {Promise<PopperModule> | undefined} */
1257
1311
  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 () => {
@@ -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 =
@@ -1,8 +1,8 @@
1
1
  import { css, LitElement } from 'lit';
2
- import { ArrowMixin, OverlayMixin } from '@lion/ui/overlays.js';
2
+ import { ArrowMixin, OverlayMixin, withTooltipConfig } from '@lion/ui/overlays.js';
3
3
 
4
4
  /**
5
- * @typedef {import('../../overlays/types/OverlayConfig.js').OverlayConfig} OverlayConfig
5
+ * @typedef {import('@lion/ui/types/overlays.js').OverlayConfig} OverlayConfig
6
6
  * @typedef {import('lit').CSSResult} CSSResult
7
7
  * @typedef {import('lit').CSSResultArray} CSSResultArray
8
8
  */
@@ -49,10 +49,6 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
49
49
  * @type {'label'|'description'}
50
50
  */
51
51
  this.invokerRelation = 'description';
52
- /** @protected */
53
- this._mouseActive = false;
54
- /** @protected */
55
- this._keyActive = false;
56
52
  }
57
53
 
58
54
  /** @protected */
@@ -60,90 +56,7 @@ export class LionTooltip extends ArrowMixin(OverlayMixin(LitElement)) {
60
56
  _defineOverlayConfig() {
61
57
  return /** @type {OverlayConfig} */ ({
62
58
  ...super._defineOverlayConfig(),
63
- placementMode: 'local',
64
- elementToFocusAfterHide: undefined,
65
- hidesOnEsc: true,
66
- hidesOnOutsideEsc: true,
67
- handlesAccessibility: true,
68
- isTooltip: true,
69
- invokerRelation: this.invokerRelation,
59
+ ...withTooltipConfig({ invokerRelation: this.invokerRelation }),
70
60
  });
71
61
  }
72
-
73
- /** @protected */
74
- _hasDisabledInvoker() {
75
- if (this._overlayCtrl && this._overlayCtrl.invoker) {
76
- return (
77
- /** @type {HTMLElement & { disabled: boolean }} */ (this._overlayCtrl.invoker).disabled ||
78
- this._overlayCtrl.invoker.getAttribute('aria-disabled') === 'true'
79
- );
80
- }
81
- return false;
82
- }
83
-
84
- /** @protected */
85
- _setupOpenCloseListeners() {
86
- super._setupOpenCloseListeners();
87
- this.__resetActive = this.__resetActive.bind(this);
88
- this._overlayCtrl.addEventListener('hide', this.__resetActive);
89
-
90
- this.addEventListener('mouseenter', this._showMouse);
91
- this.addEventListener('mouseleave', this._hideMouse);
92
-
93
- this._showKey = this._showKey.bind(this);
94
- this._overlayInvokerNode.addEventListener('focusin', this._showKey);
95
-
96
- this._hideKey = this._hideKey.bind(this);
97
- this._overlayInvokerNode.addEventListener('focusout', this._hideKey);
98
- }
99
-
100
- /** @protected */
101
- _teardownOpenCloseListeners() {
102
- super._teardownOpenCloseListeners();
103
- this._overlayCtrl.removeEventListener('hide', this.__resetActive);
104
- this.removeEventListener('mouseenter', this._showMouse);
105
- this.removeEventListener('mouseleave', this._hideMouse);
106
- this._overlayInvokerNode.removeEventListener('focusin', this._showKey);
107
- this._overlayInvokerNode.removeEventListener('focusout', this._hideKey);
108
- }
109
-
110
- /** @private */
111
- __resetActive() {
112
- this._mouseActive = false;
113
- this._keyActive = false;
114
- }
115
-
116
- /** @protected */
117
- _showMouse() {
118
- if (!this._keyActive) {
119
- this._mouseActive = true;
120
- if (!this._hasDisabledInvoker()) {
121
- this.opened = true;
122
- }
123
- }
124
- }
125
-
126
- /** @protected */
127
- _hideMouse() {
128
- if (!this._keyActive) {
129
- this.opened = false;
130
- }
131
- }
132
-
133
- /** @protected */
134
- _showKey() {
135
- if (!this._mouseActive) {
136
- this._keyActive = true;
137
- if (!this._hasDisabledInvoker()) {
138
- this.opened = true;
139
- }
140
- }
141
- }
142
-
143
- /** @protected */
144
- _hideKey() {
145
- if (!this._mouseActive) {
146
- this.opened = false;
147
- }
148
- }
149
62
  }