/** @jsxImportSource react */ import type { Root } from "cx-react"; import { isBinding, isBindingObject } from "../../data/Binding"; import { Store } from "../../data/Store"; import { View } from "../../data/View"; import { startAppLoop } from "../../ui/app/startAppLoop"; import { ContainerBase, StyledContainerConfig } from "../../ui/Container"; import { FocusManager, offFocusOut, oneFocusOut } from "../../ui/FocusManager"; import { Instance } from "../../ui/Instance"; import { BooleanProp, NumberProp } from "../../ui/Prop"; import { RenderingContext } from "../../ui/RenderingContext"; import { VDOM, Widget } from "../../ui/Widget"; import { ZIndexManager } from "../../ui/ZIndexManager"; import { addEventListenerWithOptions } from "../../util/addEventListenerWithOptions"; import { closest, isSelfOrDescendant } from "../../util/DOM"; import { getActiveElement } from "../../util/getActiveElement"; import { getTopLevelBoundingClientRect } from "../../util/getTopLevelBoundingClientRect"; import { isDataRecord } from "../../util/isDataRecord"; import { isNumber } from "../../util/isNumber"; import { KeyCode } from "../../util/KeyCode"; import { parseStyle } from "../../util/parseStyle"; import { SubscriberList } from "../../util/SubscriberList"; import { ddDetect, ddMouseDown, ddMouseUp } from "../drag-drop/ops"; import { captureMouseOrTouch, getCursorPos } from "./captureMouse"; /* Features: - renders itself on top of other elements - provide resizing capabilities - adds positioning hook and ability to position itself in the center of the page - provides header, body, and footer elements and updates body's height on resize (move this to Window) - stop mouse events from bubbling to parents, but allow keystrokes */ export interface OverlayConfig extends StyledContainerConfig { /** Set to `true` to enable resizing. */ resizable?: BooleanProp; /** Set to `true` to enable dragging the overlay. */ draggable?: BooleanProp; /** Base CSS class to be applied to the field. Defaults to `overlay`. */ baseClass?: string; /** Width of resize handle area. */ resizeWidth?: number; /** Set to `true` to initially place the overlay in the center of the page. */ center?: boolean; /** Set to `true` to initially place the overlay in the center of the page horizontally. */ centerX?: boolean; /** Set to `true` to initially place the overlay in the center of the page vertically. */ centerY?: boolean; /** Set to `true` to add a modal backdrop which masks mouse events for the rest of the page. */ modal?: boolean; /** Set to `true` to add a modal backdrop which will dismiss the window when clicked. */ backdrop?: boolean; /** Set to `true` to force the element to be rendered inline, instead of being appended to the body element. * Inline overlays have z-index set to a very high value, to ensure they are displayed on top of the other content. */ inline?: boolean; /** Set to `true` to automatically focus the top level overlay element. */ autoFocus?: boolean; /** Set to `true` to automatically focus the first focusable child in the overlay. */ autoFocusFirstChild?: boolean; /** Set to `true` to append the set animate state after the initial render. Appended CSS class may be used to add show/hide animations. */ animate?: boolean; /** Number of milliseconds to wait, before removing the element from the DOM. Used in combination with the animate property. */ destroyDelay?: number; /** Automatically dismiss overlay if it loses focus. */ dismissOnFocusOut?: boolean; /** Set to true to make the top level overlay element focusable. */ focusable?: boolean; /** Set to `true` to dismiss the window if the user presses the back button in the browser. */ dismissOnPopState?: boolean; /** A callback function which fires while the overlay is being moved around. */ onMove?: string | ((e: Event, instance: Instance, component: any) => void); /** A callback function which fires while the overlay is being resized. */ onResize?: string | ((e: Event, instance: Instance, component: any) => void); /** zIndex */ zIndex?: NumberProp; /** Set to `true` to make the window automatically close if Esc is pressed on the keyboard. Default value is false.*/ closeOnEscape?: boolean; /** Custom CSS styling for the container element. */ containerStyle?: string; /** Callback for focus out event. */ onFocusOut?: string; /** Callback for mouse enter event. */ onMouseEnter?: string; /** Callback for mouse leave event. */ onMouseLeave?: string; /** Callback for backdrop click. */ onBackdropClick?: string; /** Callback for mouse down event. */ onMouseDown?: string; /** Callback for key down event. */ onKeyDown?: string; /** Callback fired before dismiss. */ onBeforeDismiss?: string | (() => boolean); /** Callback fired when overlay will dismiss. */ overlayWillDismiss?: (instance: Instance, component: any) => boolean; /** Callback for click event. */ onClick?: string; } export interface OverlayOpenOptions { initiatingEvent?: React.SyntheticEvent; name?: string; dismiss?: () => void; parentEl?: Element; subscribeToBeforeDismiss?: (callback: () => boolean) => void; destroyDelay?: number; removeParentDOMElement?: boolean; } export interface ConfigureOverlayContainerContext { relatedElement?: HTMLElement | null; initiatingEvent?: React.SyntheticEvent; } export class OverlayInstance = Overlay> extends Instance { declare positionChangeSubscribers: SubscriberList; declare dismiss?: () => void; onBeforeDismiss?: () => boolean; declare beaconEl?: HTMLElement | null; } export class OverlayBase< Config extends OverlayConfig = OverlayConfig, InstanceType extends OverlayInstance = OverlayInstance, > extends ContainerBase { static configureOverlayContainer?: (containerEl: HTMLElement, context: ConfigureOverlayContainerContext) => void; // Properties declared here to support prototype assignments declare styled: true; declare baseClass: string; declare resizable?: BooleanProp; declare resizeWidth: number; declare center?: boolean; declare centerX?: boolean; declare centerY?: boolean; declare modal?: boolean; declare backdrop?: boolean; declare inline?: boolean; declare autoFocus?: boolean; declare autoFocusFirstChild?: boolean; declare animate?: boolean; declare draggable?: BooleanProp; declare destroyDelay?: number; declare dismissOnFocusOut?: boolean; declare focusable?: boolean; declare containerStyle?: string; declare dismissOnPopState?: boolean; declare closeOnEscape?: boolean; declare onFocusOut?: string; declare onMouseLeave?: string; declare onMouseEnter?: string; declare onKeyDown?: string; declare onMove?: string | ((e: Event, instance: Instance, component: any) => void); declare onResize?: string | ((e: Event, instance: Instance, component: any) => void); declare onClick?: string; declare onMouseDown?: string; declare onBackdropClick?: string; declare overlayWillDismiss?: (instance: Instance, component: any) => boolean; declare style?: any; declare pad?: boolean; declare needsBeacon: boolean; init() { if (this.center) this.centerX = this.centerY = this.center; super.init(); } declareData(...args: any[]) { super.declareData(...args, { shadowStyle: { structured: true, }, resizable: undefined, draggable: undefined, zIndex: undefined, }); } prepareData(context: RenderingContext, instance: InstanceType): void { let { data } = instance; data.stateMods = { ...data.stateMods, inline: this.inline, modal: this.modal, pad: this.pad, resizable: data.resizable, draggable: data.draggable, animate: this.animate, shadow: this.modal || this.backdrop, }; super.prepareData(context, instance); } initInstance(context: RenderingContext, instance: InstanceType): void { instance.positionChangeSubscribers = new SubscriberList(); super.initInstance(context, instance); } explore(context: RenderingContext, instance: InstanceType): void { if (isBinding(this.visible)) { if (!instance.dismiss) { instance.dismiss = () => { if (instance.onBeforeDismiss && instance.onBeforeDismiss() === false) return; instance.set("visible", false); }; } } else if (context.options.dismiss) instance.dismiss = context.options.dismiss; if (instance.dismiss) { context.push("parentOptions", { ...context.parentOptions, dismiss: instance.dismiss, }); } if (instance.cache("dismiss", instance.dismiss)) instance.markShouldUpdate(context); context.push("parentPositionChangeEvent", instance.positionChangeSubscribers); super.explore(context, instance); } exploreCleanup(context: RenderingContext, instance: InstanceType): void { if (instance.dismiss) context.pop("parentOptions"); context.pop("parentPositionChangeEvent"); } render(context: RenderingContext, instance: InstanceType, key: string): any { if (this.needsBeacon) return ( ( {this.renderContents(context, instance)} )} /> ); return ( {this.renderContents(context, instance)} ); } renderContents(context: RenderingContext, instance: InstanceType): any { return this.renderChildren(context, instance); } getConfigureOverlayContainerContext( instance?: InstanceType, initiatingEvent?: React.SyntheticEvent, ): ConfigureOverlayContainerContext { return { initiatingEvent, relatedElement: instance?.beaconEl, }; } overlayDidMount(instance: InstanceType, component: any): void { let { el } = component; if (this.centerX) if (!el.style.left) el.style.left = `${(window.innerWidth - el.offsetWidth) / 2}px`; if (this.centerY) if (!el.style.top) el.style.top = `${Math.max(0, (window.innerHeight - el.offsetHeight) / 2)}px`; } overlayDidUpdate(instance: InstanceType, component: any): void {} overlayWillUnmount(instance: InstanceType, component: any): void {} handleFocusOut(instance: InstanceType, component: any): void { if (this.onFocusOut) instance.invoke("onFocusOut", instance, component); if (this.dismissOnFocusOut && instance.dismiss) instance.dismiss(); } handleKeyDown(e: any, instance: InstanceType, component?: any): void | false { if (this.onKeyDown && instance.invoke("onKeyDown", e, instance, component) === false) return false; if (this.closeOnEscape && e.keyCode == KeyCode.esc && instance.dismiss) { instance.dismiss(); e.stopPropagation(); } } handleMouseLeave(instance: InstanceType, component: any): void { if (this.onMouseLeave) instance.invoke("onMouseLeave", instance, component); } handleMouseEnter(instance: InstanceType, component: any): void { if (this.onMouseEnter) instance.invoke("onMouseEnter", instance, component); } getOverlayContainer(): HTMLElement { return document.body; } containerFactory(instance?: InstanceType, initiatingEvent?: React.SyntheticEvent): HTMLElement { let el = document.createElement("div"); let container = this.getOverlayContainer(); container.appendChild(el); el.style.position = "absolute"; if (this.containerStyle) Object.assign(el.style, parseStyle(this.containerStyle)); if (OverlayBase.configureOverlayContainer) OverlayBase.configureOverlayContainer(el, this.getConfigureOverlayContainerContext(instance, initiatingEvent)); return el; } open(storeOrInstance?: View | Instance, options?: OverlayOpenOptions): () => void { if (!this.initialized) this.init(); let el = this.containerFactory(undefined, options?.initiatingEvent); el.style.display = "hidden"; let beforeDismiss: (() => boolean) | null = null; let stop: any; options = { destroyDelay: this.destroyDelay, removeParentDOMElement: true, ...options, parentEl: el, dismiss: () => { if (beforeDismiss && beforeDismiss() === false) return; stop(); beforeDismiss = null; }, subscribeToBeforeDismiss: (cb: () => boolean) => { beforeDismiss = cb; }, }; options.name = options.name || "overlay"; stop = startAppLoop(el, storeOrInstance, this, options); return options.dismiss!; } handleMove(e: any, instance: InstanceType, component: any): void { let { widget } = instance; if (!widget.onMove || instance.invoke("onMove", e, instance, component) !== false) { instance.store.silently(() => { if (isDataRecord(this.style) && isBindingObject(this.style.top)) { instance.store.set(this.style.top.bind, component.el.style.top); } if (isDataRecord(this.style) && isBindingObject(this.style.left)) { instance.store.set(this.style.left.bind, component.el.style.left); } }); } instance.positionChangeSubscribers.notify(); } handleResize(e: any, instance: InstanceType, component: any): void { let { widget } = instance; if (!widget.onResize || instance.invoke("onResize", e, instance, component) !== false) { instance.store.silently(() => { if (isDataRecord(this.style) && isBindingObject(this.style.width)) { instance.store.set(this.style.width.bind, component.el.style.width); } if (isDataRecord(this.style) && isBindingObject(this.style.height)) { instance.store.set(this.style.height.bind, component.el.style.height); } }); } instance.positionChangeSubscribers.notify(); } } OverlayBase.prototype.styled = true; OverlayBase.prototype.baseClass = "overlay"; OverlayBase.prototype.resizable = false; OverlayBase.prototype.resizeWidth = 7; OverlayBase.prototype.center = false; OverlayBase.prototype.centerX = false; OverlayBase.prototype.centerY = false; OverlayBase.prototype.modal = false; OverlayBase.prototype.backdrop = false; OverlayBase.prototype.inline = false; OverlayBase.prototype.autoFocus = false; OverlayBase.prototype.autoFocusFirstChild = false; OverlayBase.prototype.animate = false; OverlayBase.prototype.draggable = false; OverlayBase.prototype.destroyDelay = 0; OverlayBase.prototype.dismissOnFocusOut = false; OverlayBase.prototype.focusable = false; OverlayBase.prototype.containerStyle = undefined; OverlayBase.prototype.dismissOnPopState = false; OverlayBase.prototype.closeOnEscape = false; OverlayBase.prototype.needsBeacon = false; export class Overlay extends OverlayBase {} Widget.alias("overlay", Overlay); interface OverlayContentProps { onRef: (el: HTMLDivElement | null) => void; className: string; style: any; tabIndex: number | undefined; onFocus: () => void; onBlur: () => void; onKeyDown: (e: any) => void; onMouseMove: (e: any, captureData?: any) => void; onMouseUp: (e: any) => void; onMouseDown: (e: any) => void; onTouchStart: (e: any) => void; onTouchEnd: (e: any) => void; onTouchMove: (e: any, captureData?: any) => void; onMouseEnter: (e: any) => void; onMouseLeave: (e: any) => void; onClick: (e: any) => void; onDidUpdate: () => void; focusableOverlayContainer?: boolean; children: any; } //TODO: all el related logic should be moved here class OverlayContent extends VDOM.Component { render() { return (
{this.props.children}
); } componentDidUpdate() { this.props.onDidUpdate(); } } export interface OverlayComponentProps { instance: OverlayInstance; parentEl?: HTMLElement; subscribeToBeforeDismiss?: (cb: () => boolean) => void; children: any; beaconEl: HTMLElement | null; } export interface OverlayComponentState { animated?: boolean; mods?: Record; } //TODO: This should be called OverlayPortal export class OverlayComponent< Props extends OverlayComponentProps = OverlayComponentProps, State extends OverlayComponentState = OverlayComponentState, > extends VDOM.Component { declare el?: HTMLElement | null; declare containerEl?: HTMLElement | null; declare ownedEl?: HTMLElement | null; onOverlayRef?: (el: HTMLElement | null) => void; declare shadowEl?: HTMLElement | null; declare dismissed?: boolean; declare unmounting?: boolean; onPopState?: () => void; unsubscribeWheelBlock?: () => void; declare customStyle: any; declare root: Root; constructor(props: Props) { super(props); this.state = {} as State; this.customStyle = {}; } render() { let { instance, parentEl } = this.props; let { widget } = instance; if (widget.inline || parentEl) return this.renderOverlay(); if (!this.containerEl) { instance.beaconEl = this.props.beaconEl; this.ownedEl = widget.containerFactory(instance); this.ownedEl.style.display = "hidden"; this.containerEl = this.ownedEl; } // content is rendered in componentDidUpdate if portals are not supported return VDOM.DOM.createPortal ? VDOM.DOM.createPortal!(this.renderOverlay(), this.containerEl) : null; } renderOverlay() { let { widget, data } = this.props.instance; let { CSS, baseClass } = widget; if (!this.onOverlayRef) this.onOverlayRef = (el) => { this.el = el; }; let content = ( {this.renderOverlayBody()} ); let result = content; if (widget.modal || widget.backdrop) { result = (
{ this.shadowEl = el; }} className={CSS.element(baseClass, "shadow", { animated: this.state.animated, "animate-enter": this.state.animated && !this.dismissed, animate: widget.animate, })} style={parseStyle(data.shadowStyle)} >
{content}
); } return result; } renderOverlayBody() { return this.props.children; } onFocus() { FocusManager.nudge(); this.onFocusIn(); if (this.el) oneFocusOut(this, this.el, this.onFocusOut.bind(this)); } onBlur() { FocusManager.nudge(); } onFocusIn() {} onFocusOut() { let { widget } = this.props.instance; widget.handleFocusOut(this.props.instance, this); } onMouseEnter(e: React.MouseEvent) { let { widget } = this.props.instance; widget.handleMouseEnter(this.props.instance, this); } onMouseLeave(e: React.MouseEvent) { let { widget } = this.props.instance; widget.handleMouseLeave(this.props.instance, this); } onClick(e: React.MouseEvent) { let { instance } = this.props; let { widget } = instance; if (widget.onClick) instance.invoke("onClick", e, instance, this); } onKeyDown(e: React.KeyboardEvent) { let { widget } = this.props.instance; widget.handleKeyDown(e, this.props.instance, this); } getResizePrefix(e: MouseEvent | React.MouseEvent | React.TouchEvent) { let { widget, data } = this.props.instance; if (!data.resizable) return ""; let cursor = getCursorPos(e); let bounds = getTopLevelBoundingClientRect(this.el!); let leftMargin = cursor.clientX - bounds.left; let rightMargin = bounds.right - cursor.clientX; let topMargin = cursor.clientY - bounds.top; let bottomMargin = bounds.bottom - cursor.clientY; let prefix = ""; if (topMargin >= 0 && topMargin < widget.resizeWidth) prefix += "n"; else if (bottomMargin >= 0 && bottomMargin < widget.resizeWidth) prefix += "s"; if (leftMargin >= 0 && leftMargin < widget.resizeWidth) prefix += "w"; else if (rightMargin >= 0 && rightMargin < widget.resizeWidth) prefix += "e"; return prefix; } onMouseDown(e: React.MouseEvent | React.TouchEvent) { let { instance } = this.props; let { widget, data } = instance; if (widget.onMouseDown && instance.invoke("onMouseDown", e, instance) === false) return; let prefix = this.getResizePrefix(e); if (prefix) { //e.preventDefault(); let rect = getTopLevelBoundingClientRect(this.el!); let cursor = getCursorPos(e); let captureData = { prefix: prefix, dl: cursor.clientX - rect.left, dt: cursor.clientY - rect.top, dr: cursor.clientX - rect.right, db: cursor.clientY - rect.bottom, rect: rect, }; captureMouseOrTouch(e, this.onMouseMove.bind(this), undefined, captureData, prefix + "-resize"); } else if (data.draggable) { ddMouseDown(e); } //e.stopPropagation(); } onBackdropClick(e: React.MouseEvent) { e.stopPropagation(); let { instance } = this.props; let { widget } = instance; if (widget.onBackdropClick) instance.invoke("onBackdropClick", e, instance); if (widget.backdrop) { if (instance.dismiss) instance.dismiss(); } else if (widget.modal) { FocusManager.focus(this.el!); } } onMouseUp(e: React.MouseEvent | React.TouchEvent) { ddMouseUp(); e.stopPropagation(); } onMouseMove(e: MouseEvent, captureData: any) { // handle dragging let { instance } = this.props; let { data, widget } = instance; let detect = ddDetect(e); if (data.draggable && detect) { this.startMoveOperation(e); return; } if (captureData && captureData.prefix) { let { prefix, rect, dl, dt, dr, db } = captureData; let cursor = getCursorPos(e); if (prefix.indexOf("w") != -1) this.setCustomStyle({ left: cursor.clientX - dl + "px", width: rect.right - cursor.clientX + dl + "px", right: "auto", }); if (prefix.indexOf("n") != -1) this.setCustomStyle({ top: cursor.clientY - dt + "px", height: rect.bottom - cursor.clientY + dt + "px", bottom: "auto", }); if (prefix.indexOf("e") != -1) this.setCustomStyle({ width: cursor.clientX - dr - rect.left + "px", left: `${rect.left}px`, right: "auto", }); if (prefix.indexOf("s") != -1) this.setCustomStyle({ height: cursor.clientY - db - rect.top + "px", top: `${rect.top}px`, bottom: "auto", }); if (prefix.indexOf("w") >= 0 || prefix.indexOf("n") >= 0) widget.handleMove(e, instance, this); widget.handleResize(e, instance, this); } else { let prefix = this.getResizePrefix(e); this.setCustomStyle({ cursor: prefix ? prefix + "-resize" : undefined, }); } } startMoveOperation(e: MouseEvent | React.MouseEvent | React.TouchEvent) { if (this.el && !this.getResizePrefix(e)) { e.stopPropagation(); let rect = getTopLevelBoundingClientRect(this.el); let cursor = getCursorPos(e); let data = { dx: cursor.clientX - rect.left, dy: cursor.clientY - rect.top, }; captureMouseOrTouch( e, this.onMove.bind(this), undefined, data, getComputedStyle(e.target as HTMLElement).cursor, ); } } onMove(e: MouseEvent, data: any) { if (data) { let cursor = getCursorPos(e); e.preventDefault(); this.setCustomStyle({ left: cursor.clientX - data.dx + "px", top: cursor.clientY - data.dy + "px", right: "auto", bottom: "auto", }); let { instance } = this.props; let { widget } = instance; widget.handleMove(e, instance, this); } } onBeforeDismiss() { let { instance } = this.props; let { widget } = instance; if (widget.overlayWillDismiss && widget.overlayWillDismiss(instance, this) === false) return false; this.dismissed = true; //this.el might be null if visible is set to false if (this.el) { this.el.className = this.getOverlayCssClass(); // if (widget.animate) // this.setState({ // animated: false // }); } return true; } componentDidMount() { let { instance, subscribeToBeforeDismiss, parentEl } = this.props; let { widget, data } = instance; this.setZIndex(isNumber(data.zIndex) ? data.zIndex : ZIndexManager.next()); this.componentDidUpdate(); widget.overlayDidMount(instance, this); if (this.containerEl) this.containerEl.style.removeProperty("display"); else if (parentEl) parentEl.style.removeProperty("display"); let childHasFocus = isSelfOrDescendant(this.el!, getActiveElement()); if (childHasFocus) oneFocusOut(this, this.el, this.onFocusOut.bind(this)); else { if (!widget.autoFocusFirstChild || !FocusManager.focusFirstChild(this.el!)) if (widget.focusable && widget.autoFocus) FocusManager.focus(this.el!); } instance.onBeforeDismiss = this.onBeforeDismiss.bind(this); if (subscribeToBeforeDismiss) { subscribeToBeforeDismiss(instance.onBeforeDismiss); } if (widget.animate) { setTimeout(() => { if (!this.unmounting) this.setState({ animated: true, }); }, 0); } if (widget.dismissOnPopState) { this.onPopState = () => { this.props.instance.dismiss?.(); }; window.addEventListener("popstate", this.onPopState); } if (this.shadowEl) this.unsubscribeWheelBlock = addEventListenerWithOptions( this.shadowEl, "wheel", (e) => { if (e.shiftKey || e.ctrlKey) return; //check if there is a scrollable element within the shadow or overlay contents //such that its scrollbar is not at the very end let scrollAllowed = false; closest(e.target as Element, (el) => { if ( (e.deltaY > 0 && el.scrollTop < el.scrollHeight - el.clientHeight) || (e.deltaY < 0 && el.scrollTop > 0) ) { scrollAllowed = true; return true; } return el == e.currentTarget; }); if (!scrollAllowed) e.preventDefault(); }, { passive: false }, ); } componentWillUnmount() { if (this.onPopState) window.removeEventListener("popstate", this.onPopState); if (this.unsubscribeWheelBlock) this.unsubscribeWheelBlock(); offFocusOut(this); this.unmounting = true; let { widget } = this.props.instance; let { baseClass, CSS } = widget; // //we didn't have a chance to call onBeforeDismiss if (this.state.animated && this.el) { this.el.className = this.getOverlayCssClass(); if (this.shadowEl) this.shadowEl.className = CSS.element(baseClass, "shadow", { animate: widget.animate, "animate-leave": true, }); } widget.overlayWillUnmount(this.props.instance, this); if (this.ownedEl) { setTimeout(() => { this.root?.unmount(); if (this.ownedEl?.parentNode) this.ownedEl.parentNode.removeChild(this.ownedEl); this.ownedEl = null; }, widget.destroyDelay); } delete this.containerEl; } setZIndex(zIndex: number) { if (this.shadowEl) this.shadowEl.style.zIndex = zIndex.toString(); this.setCustomStyle({ zIndex: zIndex.toString(), }); } setCustomStyle(style: Partial) { Object.assign(this.customStyle, style); if (this.el) Object.assign(this.el.style, this.customStyle); } getOverlayStyle() { let { data } = this.props.instance; return { ...data.style, ...this.customStyle, }; } setCSSState(mods: Record) { let m: Record = { ...this.state.mods }; let changed = false; for (let k in mods) if (m[k] !== mods[k]) { m[k] = mods[k]; changed = true; } if (changed) this.setState({ mods: mods, }); } getOverlayCssClass() { let { data, widget } = this.props.instance; let { CSS } = widget; return ( CSS.expand( data.classNames, CSS.state({ ...this.state.mods, animated: this.state.animated && !this.unmounting && !this.dismissed, "animate-enter": this.state.animated && !this.dismissed, "animate-leave": widget.animate && this.dismissed, }), ) ?? "" ); } overlayDidUpdate() { if (this.el && !this.dismissed) { let { widget } = this.props.instance; widget.overlayDidUpdate(this.props.instance, this); this.el.className = this.getOverlayCssClass(); Object.assign(this.el.style, this.getOverlayStyle()); } } componentDidUpdate() { if (this.containerEl && !VDOM.DOM.createPortal) { this.root = VDOM.DOM.createRoot(this.containerEl); this.root.render(this.renderOverlay()); } this.overlayDidUpdate(); } } interface OverlayBeaconProps { childrenFactory: (beacon: HTMLElement) => React.ReactNode; } export interface OverlayBeaconState { beaconRendered?: boolean; } export class OverlayBeacon extends VDOM.Component { el: HTMLElement | null; render() { return ( <>
{ this.el = el; }} style={{ position: "absolute", display: "none" }} /> {this.el && this.props.childrenFactory(this.el)} ); } componentDidMount(): void { this.setState({ beaconRendered: true }); } }