From c7cae9fb5207e5ca270df089795d5130bc28810b Mon Sep 17 00:00:00 2001 From: Mikhail Shatikhin Date: Mon, 13 May 2024 14:35:47 +0500 Subject: [PATCH] fix: render portal in shadow-root --- packages/react-ui/internal/Popup/Popup.tsx | 122 ++++++++++++------ .../RenderContainer/RenderContainer.tsx | 28 ++-- .../internal/RenderLayer/RenderLayer.tsx | 20 +-- packages/react-ui/lib/listenFocusOutside.ts | 21 +-- .../react-ui/lib/widgets/WidgetContainer.tsx | 9 +- 5 files changed, 118 insertions(+), 82 deletions(-) diff --git a/packages/react-ui/internal/Popup/Popup.tsx b/packages/react-ui/internal/Popup/Popup.tsx index 7e4f2e1e79..f2fb49bb3c 100644 --- a/packages/react-ui/internal/Popup/Popup.tsx +++ b/packages/react-ui/internal/Popup/Popup.tsx @@ -26,6 +26,7 @@ import { isInstanceWithAnchorElement } from '../../lib/InstanceWithAnchorElement import { createPropsGetter } from '../../lib/createPropsGetter'; import { isInstanceOf } from '../../lib/isInstanceOf'; import { ThemeContext } from '../../lib/theming/ThemeContext'; +import { RenderContainerElement, RenderLayerConsumer } from '../RenderLayer'; import { PopupPin } from './PopupPin'; import { Offset, PopupHelper, PositionObject, Rect } from './PopupHelper'; @@ -228,6 +229,7 @@ export class Popup extends React.Component { public state: PopupState = { location: this.props.opened ? DUMMY_LOCATION : null }; private theme!: Theme; private emotion!: Emotion; + private container!: RenderContainerElement; private layoutEventsToken: Nullable>; private locationUpdateId: Nullable = null; private lastPopupContentElement: Nullable; @@ -298,19 +300,26 @@ export class Popup extends React.Component { public render() { return ( - - {(emotion) => { - this.emotion = emotion; + + {(container) => { + this.container = container; return ( - - {(theme) => { - this.theme = theme; - return this.renderMain(); + + {(emotion) => { + this.emotion = emotion; + return ( + + {(theme) => { + this.theme = theme; + return this.renderMain(); + }} + + ); }} - + ); }} - + ); } @@ -696,39 +705,72 @@ export class Popup extends React.Component { ); } - private getCoordinates(anchorRect: Rect, popupRect: Rect, positionName: string) { - const { margin: marginFromProps } = this.props; - const margin = - isNonNullable(marginFromProps) && !isNaN(marginFromProps) - ? marginFromProps - : parseInt(this.theme.popupMargin) || 0; - const position = PopupHelper.getPositionObject(positionName); - const popupOffset = this.getProps().popupOffset + this.getPinnedPopupOffset(anchorRect, position); + private getCoordinates(anchorRect: Rect, popupRect: Rect, positionName: string): { top: number; left: number } { + const calcCoordinates = () => { + const { margin: marginFromProps } = this.props; + const margin = + isNonNullable(marginFromProps) && !isNaN(marginFromProps) + ? marginFromProps + : parseInt(this.theme.popupMargin) || 0; + const position = PopupHelper.getPositionObject(positionName); + const popupOffset = this.getProps().popupOffset + this.getPinnedPopupOffset(anchorRect, position); + + switch (position.direction) { + case 'top': + return { + top: anchorRect.top - popupRect.height - margin, + left: this.getHorizontalPosition(anchorRect, popupRect, position.align, popupOffset), + }; + case 'bottom': + return { + top: anchorRect.top + anchorRect.height + margin, + left: this.getHorizontalPosition(anchorRect, popupRect, position.align, popupOffset), + }; + case 'left': + return { + top: this.getVerticalPosition(anchorRect, popupRect, position.align, popupOffset), + left: anchorRect.left - popupRect.width - margin, + }; + case 'right': + return { + top: this.getVerticalPosition(anchorRect, popupRect, position.align, popupOffset), + left: anchorRect.left + anchorRect.width + margin, + }; + default: + throw new Error(`Unexpected direction '${position.direction}'`); + } + }; - switch (position.direction) { - case 'top': - return { - top: anchorRect.top - popupRect.height - margin, - left: this.getHorizontalPosition(anchorRect, popupRect, position.align, popupOffset), - }; - case 'bottom': - return { - top: anchorRect.top + anchorRect.height + margin, - left: this.getHorizontalPosition(anchorRect, popupRect, position.align, popupOffset), - }; - case 'left': - return { - top: this.getVerticalPosition(anchorRect, popupRect, position.align, popupOffset), - left: anchorRect.left - popupRect.width - margin, - }; - case 'right': - return { - top: this.getVerticalPosition(anchorRect, popupRect, position.align, popupOffset), - left: anchorRect.left + anchorRect.width + margin, - }; - default: - throw new Error(`Unexpected direction '${position.direction}'`); + const containerCoordinates = this.getCoords(this.container?.firstElementChild); + const coordinates = calcCoordinates(); + + return this.container?.firstElementChild + ? { + top: coordinates.top - containerCoordinates.top, + left: coordinates.left - containerCoordinates.left, + } + : coordinates; + } + + private getCoords(element?: Element | null) { + if (!element) { + return { top: 0, left: 0 }; } + + const box = element.getBoundingClientRect(); + const body = globalObject.document?.body; + const docEl = globalObject.document?.documentElement; + + const scrollTop = globalObject.scrollY || docEl?.scrollTop || body?.scrollTop || 0; + const scrollLeft = globalObject.scrollX || docEl?.scrollLeft || body?.scrollLeft || 0; + + const clientTop = docEl?.clientTop || body?.clientTop || 0; + const clientLeft = docEl?.clientLeft || body?.clientLeft || 0; + + const top = box.top + scrollTop - clientTop; + const left = box.left + scrollLeft - clientLeft; + + return { top: Math.round(top), left: Math.round(left) }; } private getPinOffset(align: string) { diff --git a/packages/react-ui/internal/RenderContainer/RenderContainer.tsx b/packages/react-ui/internal/RenderContainer/RenderContainer.tsx index 7826f3d943..935595208f 100644 --- a/packages/react-ui/internal/RenderContainer/RenderContainer.tsx +++ b/packages/react-ui/internal/RenderContainer/RenderContainer.tsx @@ -5,6 +5,7 @@ import { Nullable } from '../../typings/utility-types'; import { getRandomID } from '../../lib/utils'; import { Upgrade } from '../../lib/Upgrades'; import { callChildRef } from '../../lib/callChildRef/callChildRef'; +import { RenderLayerConsumer, RenderContainerElement } from '../RenderLayer'; import { RenderInnerContainer } from './RenderInnerContainer'; import { RenderContainerProps } from './RenderContainerTypes'; @@ -20,7 +21,7 @@ export class RenderContainer extends React.Component { public shouldComponentUpdate(nextProps: RenderContainerProps) { if (!this.props.children && nextProps.children) { - this.mountContainer(); + this.mountContainer(undefined); } if (this.props.children && !nextProps.children) { this.unmountContainer(); @@ -33,15 +34,22 @@ export class RenderContainer extends React.Component { } public render() { + return {this.renderMain}; + } + + private renderMain = (root: RenderContainerElement) => { if (this.props.children) { - this.mountContainer(); + this.mountContainer(root); } return ; - } + }; + + private createContainer(root: RenderContainerElement) { + const domContainer = root + ? root.appendChild(root.ownerDocument.createElement('div')) + : globalObject.document?.createElement('div'); - private createContainer() { - const domContainer = globalObject.document?.createElement('div'); if (domContainer) { domContainer.setAttribute('class', Upgrade.getSpecificityClassName()); domContainer.setAttribute('data-rendered-container-id', `${this.rootId}`); @@ -49,12 +57,14 @@ export class RenderContainer extends React.Component { } } - private mountContainer() { + private mountContainer(root: RenderContainerElement) { if (!this.domContainer) { - this.createContainer(); + this.createContainer(root); } - if (this.domContainer && this.domContainer.parentNode !== globalObject.document?.body) { - globalObject.document?.body.appendChild(this.domContainer); + + const rootElement = root ?? globalObject.document?.body; + if (this.domContainer && this.domContainer.parentNode !== rootElement) { + rootElement?.appendChild(this.domContainer); if (this.props.containerRef) { callChildRef(this.props.containerRef, this.domContainer); diff --git a/packages/react-ui/internal/RenderLayer/RenderLayer.tsx b/packages/react-ui/internal/RenderLayer/RenderLayer.tsx index 55f916a52a..8487e35f4d 100644 --- a/packages/react-ui/internal/RenderLayer/RenderLayer.tsx +++ b/packages/react-ui/internal/RenderLayer/RenderLayer.tsx @@ -18,7 +18,8 @@ export interface RenderLayerProps extends CommonProps { type DefaultProps = Required>; -const RenderLayerContext = createContext>(null); +export type RenderContainerElement = Nullable; +const RenderLayerContext = createContext(null); export const RenderLayerProvider = RenderLayerContext.Provider; export const RenderLayerConsumer = RenderLayerContext.Consumer; @@ -48,7 +49,6 @@ export class RenderLayer extends React.Component { remove: () => void; } | null = null; private setRootNode!: TSetRootNode; - private container: Nullable = null; public componentDidMount() { if (this.getProps().active) { @@ -74,16 +74,9 @@ export class RenderLayer extends React.Component { public render() { return ( - - {(container) => { - this.container = container; - return ( - - {React.Children.only(this.props.children)} - - ); - }} - + + {React.Children.only(this.props.children)} + ); } @@ -135,7 +128,8 @@ export class RenderLayer extends React.Component { if ( !node || - (isInstanceOf(target, globalObject.Element) && containsTargetOrRenderContainer(target, this.container)(node)) + (event.composed && event.composedPath().indexOf(node) > -1) || + (isInstanceOf(target, globalObject.Element) && containsTargetOrRenderContainer(target)(node)) ) { return; } diff --git a/packages/react-ui/lib/listenFocusOutside.ts b/packages/react-ui/lib/listenFocusOutside.ts index d70e83fe35..7ab7e9a39f 100644 --- a/packages/react-ui/lib/listenFocusOutside.ts +++ b/packages/react-ui/lib/listenFocusOutside.ts @@ -2,8 +2,6 @@ import ReactDOM from 'react-dom'; import debounce from 'lodash.debounce'; import { globalObject } from '@skbkontur/global-object'; -import { Nullable } from '../typings/utility-types'; - import { isInstanceOf } from './isInstanceOf'; import { isFirefox } from './client'; @@ -53,7 +51,7 @@ function handleNativeFocus(event: UIEvent) { }); } -export function containsTargetOrRenderContainer(target: Element, renderContainer?: Nullable) { +export function containsTargetOrRenderContainer(target: Element) { return (element: Element) => { if (!element) { return false; @@ -61,7 +59,7 @@ export function containsTargetOrRenderContainer(target: Element, renderContainer if (element.contains(target)) { return true; } - const container = findRenderContainer(target, element, renderContainer); + const container = findRenderContainer(target, element); return !!container && element.contains(container); }; } @@ -69,12 +67,7 @@ export function containsTargetOrRenderContainer(target: Element, renderContainer /** * Searches RenderContainer placed in "rootNode" for "node" */ -export function findRenderContainer( - node: Element, - rootNode: Element, - renderContainer?: Nullable, - container?: Element, -): Element | null { +export function findRenderContainer(node: Element, rootNode: Element, container?: Element): Element | null { const currentNode = node.parentNode; if ( !currentNode || @@ -89,18 +82,16 @@ export function findRenderContainer( const newContainerId = currentNode.getAttribute('data-rendered-container-id'); if (newContainerId) { - const nextNode = (renderContainer ?? globalObject.document)?.querySelector( - `[data-render-container-id~="${newContainerId}"]`, - ); + const nextNode = globalObject.document?.querySelector(`[data-render-container-id~="${newContainerId}"]`); if (!nextNode) { throw Error(`Origin node for render container was not found`); } - return findRenderContainer(nextNode, rootNode, renderContainer, nextNode); + return findRenderContainer(nextNode, rootNode, nextNode); } - return findRenderContainer(currentNode, rootNode, renderContainer, container); + return findRenderContainer(currentNode, rootNode, container); } export function listen(elements: Element[] | (() => Element[]), callback: (event: Event) => void) { diff --git a/packages/react-ui/lib/widgets/WidgetContainer.tsx b/packages/react-ui/lib/widgets/WidgetContainer.tsx index b576bd846e..7bf52a07be 100644 --- a/packages/react-ui/lib/widgets/WidgetContainer.tsx +++ b/packages/react-ui/lib/widgets/WidgetContainer.tsx @@ -2,11 +2,10 @@ import React, { ReactNode, useState } from 'react'; import type { Emotion } from '@emotion/css/create-instance'; import { EmotionProvider, getEmotion } from '../theming/Emotion'; -import { RenderLayerProvider } from '../../internal/RenderLayer'; -import { Nullable } from '../../typings/utility-types'; +import { RenderContainerElement, RenderLayerProvider } from '../../internal/RenderLayer'; interface Props { - root: Nullable; + root: RenderContainerElement; children: ReactNode; } @@ -14,8 +13,8 @@ export const WidgetContainer = ({ root, children }: Props) => { const [styles, setStyles] = useState(); function setRef(el: HTMLDivElement) { - if (!styles) { - setStyles(getEmotion(el)); + if (!styles && el) { + setStyles(getEmotion(el, 'react-ui-widget')); } }