xref: /aosp_15_r20/external/perfetto/ui/src/widgets/portal.ts (revision 6dbdd20afdafa5e3ca9b8809fa73465d530080dc)
1*6dbdd20aSAndroid Build Coastguard Worker// Copyright (C) 2023 The Android Open Source Project
2*6dbdd20aSAndroid Build Coastguard Worker//
3*6dbdd20aSAndroid Build Coastguard Worker// Licensed under the Apache License, Version 2.0 (the "License");
4*6dbdd20aSAndroid Build Coastguard Worker// you may not use this file except in compliance with the License.
5*6dbdd20aSAndroid Build Coastguard Worker// You may obtain a copy of the License at
6*6dbdd20aSAndroid Build Coastguard Worker//
7*6dbdd20aSAndroid Build Coastguard Worker//      http://www.apache.org/licenses/LICENSE-2.0
8*6dbdd20aSAndroid Build Coastguard Worker//
9*6dbdd20aSAndroid Build Coastguard Worker// Unless required by applicable law or agreed to in writing, software
10*6dbdd20aSAndroid Build Coastguard Worker// distributed under the License is distributed on an "AS IS" BASIS,
11*6dbdd20aSAndroid Build Coastguard Worker// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*6dbdd20aSAndroid Build Coastguard Worker// See the License for the specific language governing permissions and
13*6dbdd20aSAndroid Build Coastguard Worker// limitations under the License.
14*6dbdd20aSAndroid Build Coastguard Worker
15*6dbdd20aSAndroid Build Coastguard Workerimport m from 'mithril';
16*6dbdd20aSAndroid Build Coastguard Worker
17*6dbdd20aSAndroid Build Coastguard Workertype Style = string | Partial<CSSStyleDeclaration>;
18*6dbdd20aSAndroid Build Coastguard Worker
19*6dbdd20aSAndroid Build Coastguard Workerexport interface MountOptions {
20*6dbdd20aSAndroid Build Coastguard Worker  // Optionally specify an element in which to place our portal.
21*6dbdd20aSAndroid Build Coastguard Worker  // Defaults to body.
22*6dbdd20aSAndroid Build Coastguard Worker  container?: Element;
23*6dbdd20aSAndroid Build Coastguard Worker}
24*6dbdd20aSAndroid Build Coastguard Worker
25*6dbdd20aSAndroid Build Coastguard Workerexport interface PortalAttrs {
26*6dbdd20aSAndroid Build Coastguard Worker  // Space delimited class list forwarded to our portal element.
27*6dbdd20aSAndroid Build Coastguard Worker  className?: string;
28*6dbdd20aSAndroid Build Coastguard Worker  // Inline styles forwarded to our portal element.
29*6dbdd20aSAndroid Build Coastguard Worker  style?: Style;
30*6dbdd20aSAndroid Build Coastguard Worker  // Called before our portal is created, allowing customization of where in the
31*6dbdd20aSAndroid Build Coastguard Worker  // DOM the portal is mounted.
32*6dbdd20aSAndroid Build Coastguard Worker  // The dom parameter is a dummy element representing where the portal would be
33*6dbdd20aSAndroid Build Coastguard Worker  // located if it were rendered into the normal tree hierarchy.
34*6dbdd20aSAndroid Build Coastguard Worker  onBeforeContentMount?: (dom: Element) => MountOptions;
35*6dbdd20aSAndroid Build Coastguard Worker  // Called after our portal is created and its content rendered.
36*6dbdd20aSAndroid Build Coastguard Worker  onContentMount?: (portalElement: HTMLElement) => void;
37*6dbdd20aSAndroid Build Coastguard Worker  // Called after our portal's content is updated.
38*6dbdd20aSAndroid Build Coastguard Worker  onContentUpdate?: (portalElement: HTMLElement) => void;
39*6dbdd20aSAndroid Build Coastguard Worker  // Called before our portal is removed.
40*6dbdd20aSAndroid Build Coastguard Worker  onContentUnmount?: (portalElement: HTMLElement) => void;
41*6dbdd20aSAndroid Build Coastguard Worker}
42*6dbdd20aSAndroid Build Coastguard Worker
43*6dbdd20aSAndroid Build Coastguard Worker// A portal renders children into a a div outside of the normal hierarchy of the
44*6dbdd20aSAndroid Build Coastguard Worker// parent component, usually in order to stack elements on top of others.
45*6dbdd20aSAndroid Build Coastguard Worker// Useful for creating overlays, dialogs, and popups.
46*6dbdd20aSAndroid Build Coastguard Workerexport class Portal implements m.ClassComponent<PortalAttrs> {
47*6dbdd20aSAndroid Build Coastguard Worker  private portalElement?: HTMLElement;
48*6dbdd20aSAndroid Build Coastguard Worker  private containerElement?: Element;
49*6dbdd20aSAndroid Build Coastguard Worker  private contentComponent: m.Component;
50*6dbdd20aSAndroid Build Coastguard Worker
51*6dbdd20aSAndroid Build Coastguard Worker  constructor({children}: m.CVnode<PortalAttrs>) {
52*6dbdd20aSAndroid Build Coastguard Worker    // Create a temporary component that we can mount in oncreate, and unmount
53*6dbdd20aSAndroid Build Coastguard Worker    // in onremove, but inject the new portal content (children) into it each
54*6dbdd20aSAndroid Build Coastguard Worker    // render cycle. This is initialized here rather than in oncreate to avoid
55*6dbdd20aSAndroid Build Coastguard Worker    // having to make it optional or use assertExists().
56*6dbdd20aSAndroid Build Coastguard Worker    this.contentComponent = {view: () => children};
57*6dbdd20aSAndroid Build Coastguard Worker  }
58*6dbdd20aSAndroid Build Coastguard Worker
59*6dbdd20aSAndroid Build Coastguard Worker  view() {
60*6dbdd20aSAndroid Build Coastguard Worker    // Dummy element renders nothing but permits DOM access in lifecycle hooks.
61*6dbdd20aSAndroid Build Coastguard Worker    return m('span', {style: {display: 'none'}});
62*6dbdd20aSAndroid Build Coastguard Worker  }
63*6dbdd20aSAndroid Build Coastguard Worker
64*6dbdd20aSAndroid Build Coastguard Worker  oncreate({attrs, dom}: m.CVnodeDOM<PortalAttrs>) {
65*6dbdd20aSAndroid Build Coastguard Worker    const {
66*6dbdd20aSAndroid Build Coastguard Worker      onContentMount = () => {},
67*6dbdd20aSAndroid Build Coastguard Worker      onBeforeContentMount = (): MountOptions => ({}),
68*6dbdd20aSAndroid Build Coastguard Worker    } = attrs;
69*6dbdd20aSAndroid Build Coastguard Worker
70*6dbdd20aSAndroid Build Coastguard Worker    const {container = document.body} = onBeforeContentMount(dom);
71*6dbdd20aSAndroid Build Coastguard Worker    this.containerElement = container;
72*6dbdd20aSAndroid Build Coastguard Worker
73*6dbdd20aSAndroid Build Coastguard Worker    this.portalElement = document.createElement('div');
74*6dbdd20aSAndroid Build Coastguard Worker    container.appendChild(this.portalElement);
75*6dbdd20aSAndroid Build Coastguard Worker    this.applyPortalProps(attrs);
76*6dbdd20aSAndroid Build Coastguard Worker
77*6dbdd20aSAndroid Build Coastguard Worker    m.mount(this.portalElement, this.contentComponent);
78*6dbdd20aSAndroid Build Coastguard Worker
79*6dbdd20aSAndroid Build Coastguard Worker    onContentMount(this.portalElement);
80*6dbdd20aSAndroid Build Coastguard Worker  }
81*6dbdd20aSAndroid Build Coastguard Worker
82*6dbdd20aSAndroid Build Coastguard Worker  onbeforeupdate({children}: m.CVnode<PortalAttrs>) {
83*6dbdd20aSAndroid Build Coastguard Worker    // Update the mounted content's view function to return the latest portal
84*6dbdd20aSAndroid Build Coastguard Worker    // content passed in via children, without changing the component itself.
85*6dbdd20aSAndroid Build Coastguard Worker    this.contentComponent.view = () => children;
86*6dbdd20aSAndroid Build Coastguard Worker  }
87*6dbdd20aSAndroid Build Coastguard Worker
88*6dbdd20aSAndroid Build Coastguard Worker  onupdate({attrs}: m.CVnodeDOM<PortalAttrs>) {
89*6dbdd20aSAndroid Build Coastguard Worker    const {onContentUpdate = () => {}} = attrs;
90*6dbdd20aSAndroid Build Coastguard Worker    if (this.portalElement) {
91*6dbdd20aSAndroid Build Coastguard Worker      this.applyPortalProps(attrs);
92*6dbdd20aSAndroid Build Coastguard Worker      onContentUpdate(this.portalElement);
93*6dbdd20aSAndroid Build Coastguard Worker    }
94*6dbdd20aSAndroid Build Coastguard Worker  }
95*6dbdd20aSAndroid Build Coastguard Worker
96*6dbdd20aSAndroid Build Coastguard Worker  private applyPortalProps(attrs: PortalAttrs) {
97*6dbdd20aSAndroid Build Coastguard Worker    if (this.portalElement) {
98*6dbdd20aSAndroid Build Coastguard Worker      this.portalElement.className = attrs.className ?? '';
99*6dbdd20aSAndroid Build Coastguard Worker      Object.assign(this.portalElement.style, attrs.style);
100*6dbdd20aSAndroid Build Coastguard Worker    }
101*6dbdd20aSAndroid Build Coastguard Worker  }
102*6dbdd20aSAndroid Build Coastguard Worker
103*6dbdd20aSAndroid Build Coastguard Worker  onremove({attrs}: m.CVnodeDOM<PortalAttrs>) {
104*6dbdd20aSAndroid Build Coastguard Worker    const {onContentUnmount = () => {}} = attrs;
105*6dbdd20aSAndroid Build Coastguard Worker    const container = this.containerElement ?? document.body;
106*6dbdd20aSAndroid Build Coastguard Worker    if (this.portalElement) {
107*6dbdd20aSAndroid Build Coastguard Worker      if (container.contains(this.portalElement)) {
108*6dbdd20aSAndroid Build Coastguard Worker        onContentUnmount(this.portalElement);
109*6dbdd20aSAndroid Build Coastguard Worker        // Rendering null ensures previous vnodes are removed properly.
110*6dbdd20aSAndroid Build Coastguard Worker        m.mount(this.portalElement, null);
111*6dbdd20aSAndroid Build Coastguard Worker        container.removeChild(this.portalElement);
112*6dbdd20aSAndroid Build Coastguard Worker      }
113*6dbdd20aSAndroid Build Coastguard Worker    }
114*6dbdd20aSAndroid Build Coastguard Worker  }
115*6dbdd20aSAndroid Build Coastguard Worker}
116