diff --git a/docs/demo/portal.md b/docs/demo/portal.md deleted file mode 100644 index 6ecd6369..00000000 --- a/docs/demo/portal.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -title: portal -nav: - title: Demo - path: /demo ---- - - diff --git a/docs/examples/portal.tsx b/docs/examples/portal.tsx deleted file mode 100644 index 50b30c3a..00000000 --- a/docs/examples/portal.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import PortalWrapper from 'rc-util/es/PortalWrapper'; - -const Demo: React.FC = () => { - const divRef = React.useRef(null); - const outerRef = React.useRef(null); - - React.useEffect(() => { - console.log('>>>', divRef.current); - }, []); - - function getRef() { - return outerRef.current; - } - - return ( - <> - - {() =>
2333
} -
-
- - ); -}; - -export default Demo; diff --git a/src/Portal.tsx b/src/Portal.tsx index 1e0b4c88..a743aee8 100644 --- a/src/Portal.tsx +++ b/src/Portal.tsx @@ -45,8 +45,7 @@ const Portal = forwardRef((props, ref) => { parentRef.current.appendChild(containerRef.current); } return () => { - // [Legacy] This should not be handle by Portal but parent PortalWrapper instead. - // Since some component use `Portal` directly, we have to keep the logic here. + // [Legacy] Some components use `Portal` directly, we have to keep the logic here. containerRef.current?.parentNode?.removeChild(containerRef.current); }; }, []); diff --git a/src/PortalWrapper.tsx b/src/PortalWrapper.tsx deleted file mode 100644 index a5fa3555..00000000 --- a/src/PortalWrapper.tsx +++ /dev/null @@ -1,242 +0,0 @@ -/* eslint-disable no-underscore-dangle,react/require-default-props */ -import * as React from 'react'; -import raf from './raf'; -import Portal from './Portal'; -import type { PortalRef } from './Portal'; -import canUseDom from './Dom/canUseDom'; -import setStyle from './setStyle'; -import ScrollLocker from './Dom/scrollLocker'; - -let openCount = 0; -const supportDom = canUseDom(); - -/** @private Test usage only */ -export function getOpenCount() { - return process.env.NODE_ENV === 'test' ? openCount : 0; -} - -// https://github.com/ant-design/ant-design/issues/19340 -// https://github.com/ant-design/ant-design/issues/19332 -let cacheOverflow: React.CSSProperties = {}; - -const getParent = (getContainer: GetContainer) => { - if (!supportDom) { - return null; - } - if (getContainer) { - if (typeof getContainer === 'string') { - return document.querySelectorAll(getContainer)[0]; - } - if (typeof getContainer === 'function') { - return getContainer(); - } - if ( - typeof getContainer === 'object' && - getContainer instanceof window.HTMLElement - ) { - return getContainer; - } - } - return document.body; -}; - -export type GetContainer = string | HTMLElement | (() => HTMLElement); - -export interface PortalWrapperProps { - visible?: boolean; - getContainer?: GetContainer; - wrapperClassName?: string; - forceRender?: boolean; - children: (info: { - getOpenCount: () => number; - getContainer: () => HTMLElement; - switchScrollingEffect: () => void; - scrollLocker: ScrollLocker; - ref?: (c: any) => void; - }) => React.ReactNode; -} - -class PortalWrapper extends React.Component { - container?: HTMLElement; - - componentRef = React.createRef(); - - rafId?: number; - - scrollLocker: ScrollLocker; - - constructor(props: PortalWrapperProps) { - super(props); - this.scrollLocker = new ScrollLocker({ - container: getParent(props.getContainer) as HTMLElement, - }); - } - - renderComponent?: (info: { - afterClose: (...params: any[]) => void; - onClose: (...params: any[]) => void; - visible: boolean; - }) => void; - - componentDidMount() { - this.updateOpenCount(); - - if (!this.attachToParent()) { - this.rafId = raf(() => { - this.forceUpdate(); - }); - } - } - - componentDidUpdate(prevProps: PortalWrapperProps) { - this.updateOpenCount(prevProps); - this.updateScrollLocker(prevProps); - - this.setWrapperClassName(); - this.attachToParent(); - } - - updateScrollLocker = (prevProps?: Partial) => { - const { visible: prevVisible } = prevProps || {}; - const { getContainer, visible } = this.props; - - if ( - visible && - visible !== prevVisible && - supportDom && - getParent(getContainer) !== this.scrollLocker.getContainer() - ) { - this.scrollLocker.reLock({ - container: getParent(getContainer) as HTMLElement, - }); - } - }; - - updateOpenCount = (prevProps?: Partial) => { - const { visible: prevVisible, getContainer: prevGetContainer } = - prevProps || {}; - const { visible, getContainer } = this.props; - - // Update count - if ( - visible !== prevVisible && - supportDom && - getParent(getContainer) === document.body - ) { - if (visible && !prevVisible) { - openCount += 1; - } else if (prevProps) { - openCount -= 1; - } - } - - // Clean up container if needed - const getContainerIsFunc = - typeof getContainer === 'function' && - typeof prevGetContainer === 'function'; - if ( - getContainerIsFunc - ? getContainer.toString() !== prevGetContainer.toString() - : getContainer !== prevGetContainer - ) { - this.removeCurrentContainer(); - } - }; - - componentWillUnmount() { - const { visible, getContainer } = this.props; - if (supportDom && getParent(getContainer) === document.body) { - // 离开时不会 render, 导到离开时数值不变,改用 func 。。 - openCount = visible && openCount ? openCount - 1 : openCount; - } - this.removeCurrentContainer(); - raf.cancel(this.rafId); - } - - attachToParent = (force = false) => { - if (force || (this.container && !this.container.parentNode)) { - const parent = getParent(this.props.getContainer); - if (parent) { - parent.appendChild(this.container); - return true; - } - - return false; - } - - return true; - }; - - getContainer = () => { - if (!supportDom) { - return null; - } - if (!this.container) { - this.container = document.createElement('div'); - this.attachToParent(true); - } - this.setWrapperClassName(); - return this.container; - }; - - setWrapperClassName = () => { - const { wrapperClassName } = this.props; - if ( - this.container && - wrapperClassName && - wrapperClassName !== this.container.className - ) { - this.container.className = wrapperClassName; - } - }; - - removeCurrentContainer = () => { - // Portal will remove from `parentNode`. - // Let's handle this again to avoid refactor issue. - this.container?.parentNode?.removeChild(this.container); - }; - - /** - * Enhance ./switchScrollingEffect - * 1. Simulate document body scroll bar with - * 2. Record body has overflow style and recover when all of PortalWrapper invisible - * 3. Disable body scroll when PortalWrapper has open - * - * @memberof PortalWrapper - */ - switchScrollingEffect = () => { - if (openCount === 1 && !Object.keys(cacheOverflow).length) { - // Must be set after switchScrollingEffect - cacheOverflow = setStyle({ - overflow: 'hidden', - overflowX: 'hidden', - overflowY: 'hidden', - }); - } else if (!openCount) { - setStyle(cacheOverflow); - cacheOverflow = {}; - } - }; - - render() { - const { children, forceRender, visible } = this.props; - let portal = null; - const childProps = { - getOpenCount: () => openCount, - getContainer: this.getContainer, - switchScrollingEffect: this.switchScrollingEffect, - scrollLocker: this.scrollLocker, - }; - - if (forceRender || visible || this.componentRef.current) { - portal = ( - - {children(childProps)} - - ); - } - return portal; - } -} - -export default PortalWrapper; diff --git a/src/index.ts b/src/index.ts index 24c69d83..f99817ed 100644 --- a/src/index.ts +++ b/src/index.ts @@ -59,4 +59,4 @@ export { render, unmount } from './React/render'; export { spyElementPrototype, spyElementPrototypes } from './test/domHook'; export { default as Portal } from './Portal'; export type { PortalProps, PortalRef } from './Portal'; -export type { GetContainer } from './PortalWrapper'; +export type GetContainer = string | HTMLElement | (() => HTMLElement); diff --git a/tests/Portal.test.tsx b/tests/Portal.test.tsx index 6c84fecc..c137f12e 100644 --- a/tests/Portal.test.tsx +++ b/tests/Portal.test.tsx @@ -1,39 +1,8 @@ -/* eslint-disable react/no-array-index-key */ -import { act, render } from '@testing-library/react'; +import { render } from '@testing-library/react'; import React, { StrictMode, useEffect } from 'react'; import Portal from '../src/Portal'; -import PortalWrapper, { getOpenCount } from '../src/PortalWrapper'; describe('Portal', () => { - let domContainer: HTMLDivElement; - - // Mock for raf - window.requestAnimationFrame = callback => window.setTimeout(callback); - window.cancelAnimationFrame = id => window.clearTimeout(id); - - beforeEach(() => { - domContainer = document.createElement('div'); - document.body.appendChild(domContainer); - }); - - afterEach(() => { - document.body.removeChild(domContainer); - }); - - it('forceRender', () => { - const divRef = React.createRef(); - - const { unmount } = render( - - {() =>
2333
} -
, - ); - - expect(divRef.current).toBeTruthy(); - - unmount(); - }); - it('didUpdate', () => { const didUpdate = jest.fn(); @@ -60,136 +29,6 @@ describe('Portal', () => { expect(didUpdate).toHaveBeenCalledTimes(2); }); - describe('getContainer', () => { - it('string', () => { - const div = document.createElement('div'); - div.id = 'bamboo-light'; - document.body.appendChild(div); - - render( - - {() =>
2333
} -
, - ); - - expect(document.querySelector('#bamboo-light').childElementCount).toEqual( - 1, - ); - - document.body.removeChild(div); - }); - - it('function', () => { - const div = document.createElement('div'); - - render( - div}> - {() =>
2333
} -
, - ); - - expect(div.childElementCount).toEqual(1); - }); - - it('htmlElement', () => { - const div = document.createElement('div'); - - render( - - {() =>
2333
} -
, - ); - - expect(div.childElementCount).toEqual(1); - }); - - it('delay', () => { - jest.useFakeTimers(); - const divRef = React.createRef(); - render( -
- divRef.current}> - {() =>
} - -
-
, - ); - - act(() => { - jest.runAllTimers(); - }); - - expect(divRef.current.childElementCount).toEqual(1); - jest.useRealTimers(); - }); - }); - - describe('openCount', () => { - it('start as 0', () => { - expect(getOpenCount()).toEqual(0); - - const { rerender, unmount } = render( - {() =>
2333
}
, - ); - expect(getOpenCount()).toEqual(0); - - rerender({() =>
2333
}
); - expect(getOpenCount()).toEqual(1); - - unmount(); - }); - - it('correct count', () => { - const Demo = ({ - count, - visible, - }: { - count: number; - visible: boolean; - }) => { - return ( - <> - {new Array(count).fill(null).map((_, index) => ( - - {() =>
2333
} -
- ))} - - ); - }; - - expect(getOpenCount()).toEqual(0); - - const { rerender } = render(); - expect(getOpenCount()).toEqual(1); - - rerender(); - expect(getOpenCount()).toEqual(2); - - rerender(); - expect(getOpenCount()).toEqual(1); - - rerender(); - expect(getOpenCount()).toEqual(0); - }); - }); - - it('wrapperClassName', () => { - const { rerender } = render( - - {() =>
} - , - ); - expect(document.body.querySelector('.bamboo')).toBeTruthy(); - - rerender( - - {() =>
} - , - ); - expect(document.body.querySelector('.light')).toBeTruthy(); - }); - it('should restore to original place in StrictMode', () => { const parentContainer = document.createElement('div'); const curDomContainer = document.createElement('div');