@momentum-design/components 0.118.5 → 0.119.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,7 @@
1
1
  import Popover from './popover.component';
2
+ import { PopoverPortal, TAG_NAME as POPOVER_PORTAL_TAG_NAME } from './popover.portal.component';
2
3
  import { TAG_NAME } from './popover.constants';
3
4
  import '../button';
4
5
  Popover.register(TAG_NAME);
6
+ PopoverPortal.register(POPOVER_PORTAL_TAG_NAME);
5
7
  export default Popover;
@@ -3,20 +3,56 @@ import { Component } from '../../models';
3
3
  import type { PopoverColor, PopoverPlacement, PopoverStrategy, PopoverTrigger } from './popover.types';
4
4
  declare const Popover_base: import("../../utils/mixins/index.types").Constructor<Component & import("../../utils/mixins/BackdropMixin").BackdropMixinInterface> & import("../../utils/mixins/index.types").Constructor<Component & import("../../utils/mixins/PreventScrollMixin").PreventScrollMixinInterface> & import("../../utils/mixins/index.types").Constructor<Component & import("../../utils/mixins/FocusTrapMixin").FocusTrapClassInterface> & typeof Component;
5
5
  /**
6
- * Popover component is a lightweight floating UI element that displays additional content when triggered.
7
- * It can be used for tooltips, dropdowns, or contextual menus.
6
+ * Popover is genric overlay which can be trigered by any actinable element.
7
+ *
8
+ * It can be used for tooltips, dropdowns, menus or any showing any other contextual content.
9
+ *
8
10
  * The popover automatically positions itself based on available space and
9
- * supports dynamic height adjustments with scrollable content when needed
11
+ * supports dynamic height adjustments with scrollable content when needed.
12
+ * It uses [Floating UI](https://floating-ui.com/) for maintaining the position of the popover.
13
+ *
14
+ * ## Limitations
15
+ *
16
+ * ### On trigger for multiple popovers
17
+ *
18
+ * A component (button, etc.) can trigger more than one popover, but only one of them should change the
19
+ * aria-expanded and aria-haspopup attributes on the trigger.
20
+ *
21
+ * To prevent unexpected attribute changes on the trigger `disable-aria-expanded` attribute must be set on all linked
22
+ * Popoers except one.
10
23
  *
11
- * Note:
12
- * - A component (button) can trigger more than one popover, but only one of them should change the
13
- * aria-expanded and aria-haspopup, the rest of the popovers must have `disable-aria-expanded` attribute.
24
+ * ### React Popover with append-to attribute
25
+ *
26
+ * React mounts the popover based on the virtual DOM, but when the append-to attribute is set, the popover removes itself
27
+ * and mounts to the specified element. React will not know about the move and will not know about the
28
+ * newly created mdc-popoverportal element either. This throws a `NotFoundError` error when the Popover is directly
29
+ * added/removed by React, for example:
30
+ *
31
+ * ```tsx
32
+ * const SomeComponent = () => {
33
+ * const [isOpen, setIsOpen] = useState(false);
34
+ * return (<div>
35
+ * {isOpen && <Popover append-to="some-element-id">...</mdc-popover>}
36
+ * </div>);
37
+ * }
38
+ * ```
39
+ * As a workaround Popover need to wrap with any other element/component, for example:
40
+ * ```tsx
41
+ * const SomeComponent = () => {
42
+ * const [isOpen, setIsOpen] = useState(false);
43
+ * return (<div>
44
+ * {isOpen && <div>
45
+ * <Popover append-to="some-element-id">...</mdc-popover>
46
+ * <div>}
47
+ * </div>);
48
+ * }
49
+ * ```
50
+ * Note the wrapper <div> around the Popover component (React.Fragment does not work).
14
51
  *
15
52
  * @dependency mdc-button
16
53
  *
17
54
  * @tagname mdc-popover
18
55
  *
19
- *
20
56
  * @event shown - (React: onShown) This event is dispatched when the popover is shown
21
57
  * @event hidden - (React: onHidden) This event is dispatched when the popover is hidden
22
58
  * @event created - (React: onCreated) This event is dispatched when the popover is created (added to the DOM)
@@ -312,9 +348,9 @@ declare class Popover extends Popover_base {
312
348
  */
313
349
  get triggerElement(): HTMLElement | null;
314
350
  constructor();
351
+ private parseTrigger;
315
352
  protected firstUpdated(changedProperties: PropertyValues): Promise<void>;
316
353
  connectedCallback(): void;
317
- private parseTrigger;
318
354
  disconnectedCallback(): Promise<void>;
319
355
  /**
320
356
  * Sets up the trigger related event listeners, based on the trigger type.
@@ -22,20 +22,56 @@ import { popoverStack } from './popover.stack';
22
22
  import styles from './popover.styles';
23
23
  import { PopoverUtils } from './popover.utils';
24
24
  /**
25
- * Popover component is a lightweight floating UI element that displays additional content when triggered.
26
- * It can be used for tooltips, dropdowns, or contextual menus.
25
+ * Popover is genric overlay which can be trigered by any actinable element.
26
+ *
27
+ * It can be used for tooltips, dropdowns, menus or any showing any other contextual content.
28
+ *
27
29
  * The popover automatically positions itself based on available space and
28
- * supports dynamic height adjustments with scrollable content when needed
30
+ * supports dynamic height adjustments with scrollable content when needed.
31
+ * It uses [Floating UI](https://floating-ui.com/) for maintaining the position of the popover.
32
+ *
33
+ * ## Limitations
34
+ *
35
+ * ### On trigger for multiple popovers
36
+ *
37
+ * A component (button, etc.) can trigger more than one popover, but only one of them should change the
38
+ * aria-expanded and aria-haspopup attributes on the trigger.
39
+ *
40
+ * To prevent unexpected attribute changes on the trigger `disable-aria-expanded` attribute must be set on all linked
41
+ * Popoers except one.
42
+ *
43
+ * ### React Popover with append-to attribute
44
+ *
45
+ * React mounts the popover based on the virtual DOM, but when the append-to attribute is set, the popover removes itself
46
+ * and mounts to the specified element. React will not know about the move and will not know about the
47
+ * newly created mdc-popoverportal element either. This throws a `NotFoundError` error when the Popover is directly
48
+ * added/removed by React, for example:
29
49
  *
30
- * Note:
31
- * - A component (button) can trigger more than one popover, but only one of them should change the
32
- * aria-expanded and aria-haspopup, the rest of the popovers must have `disable-aria-expanded` attribute.
50
+ * ```tsx
51
+ * const SomeComponent = () => {
52
+ * const [isOpen, setIsOpen] = useState(false);
53
+ * return (<div>
54
+ * {isOpen && <Popover append-to="some-element-id">...</mdc-popover>}
55
+ * </div>);
56
+ * }
57
+ * ```
58
+ * As a workaround Popover need to wrap with any other element/component, for example:
59
+ * ```tsx
60
+ * const SomeComponent = () => {
61
+ * const [isOpen, setIsOpen] = useState(false);
62
+ * return (<div>
63
+ * {isOpen && <div>
64
+ * <Popover append-to="some-element-id">...</mdc-popover>
65
+ * <div>}
66
+ * </div>);
67
+ * }
68
+ * ```
69
+ * Note the wrapper <div> around the Popover component (React.Fragment does not work).
33
70
  *
34
71
  * @dependency mdc-button
35
72
  *
36
73
  * @tagname mdc-popover
37
74
  *
38
- *
39
75
  * @event shown - (React: onShown) This event is dispatched when the popover is shown
40
76
  * @event hidden - (React: onHidden) This event is dispatched when the popover is hidden
41
77
  * @event created - (React: onCreated) This event is dispatched when the popover is created (added to the DOM)
@@ -632,12 +668,12 @@ class Popover extends BackdropMixin(PreventScrollMixin(FocusTrapMixin(Component)
632
668
  }
633
669
  async firstUpdated(changedProperties) {
634
670
  super.firstUpdated(changedProperties);
635
- this.style.zIndex = `${this.zIndex}`;
636
- this.utils.setupAppendTo();
637
671
  PopoverEventManager.onCreatedPopover(this);
638
672
  }
639
673
  connectedCallback() {
640
674
  super.connectedCallback();
675
+ this.style.zIndex = `${this.zIndex}`;
676
+ this.utils.setupAppendTo();
641
677
  this.setupTriggerListeners();
642
678
  }
643
679
  async disconnectedCallback() {
@@ -656,6 +692,7 @@ class Popover extends BackdropMixin(PreventScrollMixin(FocusTrapMixin(Component)
656
692
  this.connectedTooltip.shouldSuppressOpening = false;
657
693
  }
658
694
  }
695
+ this.utils.cleanupAppendTo();
659
696
  PopoverEventManager.onDestroyedPopover(this);
660
697
  popoverStack.remove(this);
661
698
  }
@@ -683,8 +720,13 @@ class Popover extends BackdropMixin(PreventScrollMixin(FocusTrapMixin(Component)
683
720
  if (changedProperties.has('zIndex')) {
684
721
  this.setAttribute('z-index', `${this.zIndex}`);
685
722
  }
686
- if (changedProperties.has('append-to')) {
687
- this.utils.setupAppendTo();
723
+ if (changedProperties.has('appendTo')) {
724
+ if (this.appendTo) {
725
+ this.utils.setupAppendTo();
726
+ }
727
+ else {
728
+ this.utils.cleanupAppendTo();
729
+ }
688
730
  }
689
731
  if (changedProperties.has('interactive') ||
690
732
  changedProperties.has('aria-label') ||
@@ -0,0 +1,29 @@
1
+ import { Component } from '../../models';
2
+ export declare const TAG_NAME: "mdc-popoverportal";
3
+ /**
4
+ * PopoverPortal in a placeholder component
5
+ *
6
+ * When the popover appended to another container, this component is used to mark the original place of the
7
+ * popover in the DOM. When the portal removed from the DOM, we remove the popover from the container as well.
8
+ *
9
+ * We need this behavior to support on hover menus. Without the portal:
10
+ * - Each time when then consumer renders the menu we append a new instance of the popover to the container which
11
+ * cause memory leak.
12
+ * - Trigger component will open all popovers at once, because all of them has the same triggerID and all
13
+ * listeners attached to the document.
14
+ *
15
+ * Portal component make sure the popover clean up when it was normally (without append-to) removed from the DOM.
16
+ * This is especially important when the popover is used in a framework like React or Angular, where virtual does not
17
+ * know about the popover moved to another place in the DOM.
18
+ *
19
+ * @internal
20
+ */
21
+ export declare class PopoverPortal extends Component {
22
+ onDisconnect: Function | undefined;
23
+ connectedCallback(): void;
24
+ /**
25
+ * When the portal removed from the DOM, we remove the popover from the container as well.
26
+ * @internal
27
+ */
28
+ disconnectedCallback(): void;
29
+ }
@@ -0,0 +1,37 @@
1
+ import { Component } from '../../models';
2
+ import utils from '../../utils/tag-name';
3
+ export const TAG_NAME = utils.constructTagName('popoverportal');
4
+ /**
5
+ * PopoverPortal in a placeholder component
6
+ *
7
+ * When the popover appended to another container, this component is used to mark the original place of the
8
+ * popover in the DOM. When the portal removed from the DOM, we remove the popover from the container as well.
9
+ *
10
+ * We need this behavior to support on hover menus. Without the portal:
11
+ * - Each time when then consumer renders the menu we append a new instance of the popover to the container which
12
+ * cause memory leak.
13
+ * - Trigger component will open all popovers at once, because all of them has the same triggerID and all
14
+ * listeners attached to the document.
15
+ *
16
+ * Portal component make sure the popover clean up when it was normally (without append-to) removed from the DOM.
17
+ * This is especially important when the popover is used in a framework like React or Angular, where virtual does not
18
+ * know about the popover moved to another place in the DOM.
19
+ *
20
+ * @internal
21
+ */
22
+ export class PopoverPortal extends Component {
23
+ connectedCallback() {
24
+ super.connectedCallback();
25
+ // We don't want the portal to be focusable or visible for screen readers
26
+ this.ariaHidden = 'true';
27
+ }
28
+ /**
29
+ * When the portal removed from the DOM, we remove the popover from the container as well.
30
+ * @internal
31
+ */
32
+ disconnectedCallback() {
33
+ var _a;
34
+ super.disconnectedCallback();
35
+ (_a = this.onDisconnect) === null || _a === void 0 ? void 0 : _a.call(this);
36
+ }
37
+ }
@@ -2,6 +2,17 @@ import type Popover from './popover.component';
2
2
  export declare class PopoverUtils {
3
3
  /** @internal */
4
4
  private popover;
5
+ /**
6
+ * The portal element used when the popover is appended to another container.
7
+ * @internal
8
+ */
9
+ private portalElement;
10
+ /**
11
+ * Flag to indicate if the popover was diconnected because it was appended to another container, or
12
+ * it was actually removed from the DOM.
13
+ * @internal
14
+ */
15
+ private disconnectAfterAppendTo;
5
16
  /** @internal */
6
17
  private arrowPixelChange;
7
18
  constructor(popover: Popover);
@@ -24,6 +35,10 @@ export declare class PopoverUtils {
24
35
  * DOM element by its ID, and appends this popover as a child of that element.
25
36
  */
26
37
  setupAppendTo(): void;
38
+ /**
39
+ * Remove portal component to when the popover appended to somewhere else and removed from the DOM
40
+ */
41
+ cleanupAppendTo(): void;
27
42
  /**
28
43
  * Sets up the aria labels
29
44
  */
@@ -1,6 +1,18 @@
1
1
  import { ROLE } from '../../utils/roles';
2
+ import { TAG_NAME as POPOVER_PORTAL_TAG_NAME } from './popover.portal.component';
2
3
  export class PopoverUtils {
3
4
  constructor(popover) {
5
+ /**
6
+ * The portal element used when the popover is appended to another container.
7
+ * @internal
8
+ */
9
+ this.portalElement = null;
10
+ /**
11
+ * Flag to indicate if the popover was diconnected because it was appended to another container, or
12
+ * it was actually removed from the DOM.
13
+ * @internal
14
+ */
15
+ this.disconnectAfterAppendTo = false;
4
16
  /** @internal */
5
17
  this.arrowPixelChange = false;
6
18
  this.popover = popover;
@@ -78,13 +90,30 @@ export class PopoverUtils {
78
90
  * DOM element by its ID, and appends this popover as a child of that element.
79
91
  */
80
92
  setupAppendTo() {
93
+ var _a, _b;
81
94
  if (this.popover.appendTo) {
82
- const appendToElement = document.getElementById(this.popover.appendTo);
83
- if (appendToElement) {
84
- appendToElement.appendChild(this.popover);
95
+ const appendToEl = document.getElementById(this.popover.appendTo);
96
+ if (appendToEl && !Array.from(appendToEl.children).includes(this.popover)) {
97
+ this.disconnectAfterAppendTo = true;
98
+ this.portalElement = document.createElement(POPOVER_PORTAL_TAG_NAME);
99
+ this.portalElement.onDisconnect = () => {
100
+ this.popover.remove();
101
+ this.portalElement = null;
102
+ };
103
+ (_b = (_a = this.popover.parentElement) === null || _a === void 0 ? void 0 : _a.appendChild) === null || _b === void 0 ? void 0 : _b.call(_a, this.portalElement);
104
+ appendToEl.appendChild(this.popover);
85
105
  }
86
106
  }
87
107
  }
108
+ /**
109
+ * Remove portal component to when the popover appended to somewhere else and removed from the DOM
110
+ */
111
+ cleanupAppendTo() {
112
+ if (!this.disconnectAfterAppendTo && this.portalElement) {
113
+ this.portalElement.remove();
114
+ }
115
+ this.disconnectAfterAppendTo = false;
116
+ }
88
117
  /**
89
118
  * Sets up the aria labels
90
119
  */