diff --git a/package.json b/package.json index be3803af96..f50c60acc1 100644 --- a/package.json +++ b/package.json @@ -40,13 +40,13 @@ "babel-eslint": "10.0.1", "coveralls": "^3.0.3", "cross-env": "^4.0.0", + "eslint": "^5.9.0", "eslint-config-airbnb": "^17.1.0", "eslint-config-prettier": "^3.3.0", "eslint-plugin-import": "^2.14.0", "eslint-plugin-jsx-a11y": "^6.1.2", "eslint-plugin-lean-imports": "^0.3.3", "eslint-plugin-react": "^7.11.1", - "eslint": "^5.9.0", "husky": "^1.2.0", "lerna": "2.10.2", "lint-staged": "^8.0.5", diff --git a/packages/zent/RELEASE_NEXT.md b/packages/zent/RELEASE_NEXT.md index ec8a340c37..06fde19c61 100644 --- a/packages/zent/RELEASE_NEXT.md +++ b/packages/zent/RELEASE_NEXT.md @@ -67,6 +67,31 @@ 导出的组件名字变了,老的写法 +#### `Portal` + +```js +import { Portal } from 'zent'; +const { PurePortal } = Portal; + +const MyPortal1 = Portal.withEscToClose(Portal); +const MyPortal2 = Portal.withNonScrollable(Portal); +``` + +新的写法 + +```js +import { Portal, PurePortal } from 'zent' + +// 替代 withEscToClose +... + +// 替代 withNonScrollable +... +``` + +- 删除了 `LayeredPortal`,请用 `Portal` 替换。 +- 去除 `onMount` 和 `onUnmount`,使用方直接使用上层组件的 `componentDidMount` 和 `componentWillUnmount` 即可。 + ```js import { Layout } from 'zent'; diff --git a/packages/zent/__tests__/portal/withNonScrollable.js b/packages/zent/__tests__/portal/blockPageScroll.js similarity index 94% rename from packages/zent/__tests__/portal/withNonScrollable.js rename to packages/zent/__tests__/portal/blockPageScroll.js index 7c50bbb5a5..1305000719 100644 --- a/packages/zent/__tests__/portal/withNonScrollable.js +++ b/packages/zent/__tests__/portal/blockPageScroll.js @@ -5,10 +5,6 @@ import Portal from 'portal'; Enzyme.configure({ adapter: new Adapter() }); -const { withNonScrollable } = Portal; - -const MyPortal = withNonScrollable(Portal); - export default class NonScrollable extends Component { state = { visible: false, @@ -39,15 +35,16 @@ export default class NonScrollable extends Component { open )} -
Toggle the portal and inspect body.style.overflow in devtool
-
+ ); } diff --git a/packages/zent/__tests__/portal/withESCToClose.js b/packages/zent/__tests__/portal/closeOnESC.js similarity index 94% rename from packages/zent/__tests__/portal/withESCToClose.js rename to packages/zent/__tests__/portal/closeOnESC.js index 72dd434387..3a17df289d 100644 --- a/packages/zent/__tests__/portal/withESCToClose.js +++ b/packages/zent/__tests__/portal/closeOnESC.js @@ -5,9 +5,6 @@ import Portal from 'portal'; Enzyme.configure({ adapter: new Adapter() }); -const { withESCToClose } = Portal; -const MyPortal = withESCToClose(Portal); - class EscToClose extends Component { state = { visible: false, @@ -38,13 +35,14 @@ class EscToClose extends Component { open )} -
Press ESC to close portal
-
+ ); } diff --git a/packages/zent/__tests__/preview-image.js b/packages/zent/__tests__/preview-image.js index 1cf6f669c7..15545a362a 100644 --- a/packages/zent/__tests__/preview-image.js +++ b/packages/zent/__tests__/preview-image.js @@ -21,7 +21,6 @@ describe('previewImage render', () => { showRotateBtn: true, index: 0, }); - expect(document.querySelectorAll('.zent-portal').length).toBe(1); expect(document.querySelectorAll('.zent-image-p-anchor').length).toBe(1); expect(document.querySelectorAll('.zent-show-image').length).toBe(1); diff --git a/packages/zent/src/dialog/Dialog.tsx b/packages/zent/src/dialog/Dialog.tsx index 22c0a5b86f..034d912aaf 100644 --- a/packages/zent/src/dialog/Dialog.tsx +++ b/packages/zent/src/dialog/Dialog.tsx @@ -2,20 +2,14 @@ import * as React from 'react'; import { Component } from 'react'; import { CSSTransition } from 'react-transition-group'; -import Portal, { IPortalProps } from '../portal'; +import Portal from '../portal'; import isBrowser from '../utils/isBrowser'; -import { DialogElWrapper, DialogInnerEl } from './DialogEl'; +import { DialogElWrapper, DialogInnerEl, IMousePosition } from './DialogEl'; import { openDialog, closeDialog } from './open'; -const { withNonScrollable, withESCToClose } = Portal; -const DialogPortal = withNonScrollable(Portal as React.ComponentType< - IPortalProps ->); -const DialogPortalESCToClose = withESCToClose(DialogPortal); - const TIMEOUT = 300; // ms -let mousePosition = null; +let mousePosition: IMousePosition | null = null; // Inspired by antd and rc-dialog if (isBrowser) { @@ -31,19 +25,14 @@ export interface IDialogProps { title?: React.ReactNode; children?: React.ReactNode; footer?: React.ReactNode; - visible?: boolean; + visible: boolean; closeBtn?: boolean; - onClose?: ( - e: - | KeyboardEvent - | React.MouseEvent - | React.MouseEvent - ) => void; + onClose?: (e: KeyboardEvent | MouseEvent | TouchEvent) => void; mask?: boolean; maskClosable?: boolean; className?: string; - prefix?: string; - style?: React.CSSProperties; + prefix: string; + style: React.CSSProperties; onOpened?: () => void; onClosed?: () => void; } @@ -70,7 +59,7 @@ export class Dialog extends Component { static openDialog = openDialog; static closeDialog = closeDialog; - lastMousePosition = null; + lastMousePosition: IMousePosition | null = null; constructor(props: IDialogProps) { super(props); @@ -80,7 +69,7 @@ export class Dialog extends Component { }; } - onClose = (e: KeyboardEvent | React.MouseEvent) => { + onClose = (e: KeyboardEvent | MouseEvent | TouchEvent) => { const { onClose } = this.props; onClose && onClose(e); }; @@ -139,14 +128,13 @@ export class Dialog extends Component { this.lastMousePosition = null; } - // 有关闭按钮的时候同时具有ESC关闭的行为 - const PortalComponent = closeBtn ? DialogPortalESCToClose : DialogPortal; - return ( - { - + ); } } diff --git a/packages/zent/src/dialog/DialogEl.tsx b/packages/zent/src/dialog/DialogEl.tsx index 39ea167afc..9e15355e22 100644 --- a/packages/zent/src/dialog/DialogEl.tsx +++ b/packages/zent/src/dialog/DialogEl.tsx @@ -3,18 +3,20 @@ import { Component, createRef } from 'react'; import cx from 'classnames'; import focusWithoutScroll from '../utils/dom/focusWithoutScroll'; +export interface IMousePosition { + x: number; + y: number; +} + export interface IDialogInnerElProps { prefix?: string; title?: React.ReactNode; - onClose?: React.MouseEventHandler; + onClose?: (e: KeyboardEvent | MouseEvent | TouchEvent) => void; className?: string; closeBtn?: boolean; style?: React.CSSProperties; footer?: React.ReactNode; - mousePosition?: { - x: number; - y: number; - } | null; + mousePosition?: IMousePosition | null; } export class DialogInnerEl extends Component { @@ -41,7 +43,7 @@ export class DialogInnerEl extends Component { const origin = `${mousePosition.x - x}px ${mousePosition.y - y}px 0`; const style = this.dialogEl.style; ['Webkit', 'Moz', 'Ms', 'ms'].forEach(prefix => { - style[`${prefix}TransformOrigin`] = origin; + style[`${prefix}TransformOrigin` as any] = origin; }); style.transformOrigin = origin; } @@ -67,16 +69,15 @@ export class DialogInnerEl extends Component { ); } + onClickClose = (e: React.MouseEvent) => { + const { onClose } = this.props; + if (onClose) { + onClose(e as any); + } + }; + render() { - const { - onClose, - className, - prefix, - closeBtn, - footer, - style, - children, - } = this.props; + const { className, prefix, closeBtn, footer, style, children } = this.props; const Header = this.renderHeader(); @@ -84,7 +85,7 @@ export class DialogInnerEl extends Component { [`${prefix}-dialog-r-has-title`]: !!Header, }); const Closer = closeBtn && ( - ); @@ -113,8 +114,7 @@ export interface IDialogElWrapper { mask?: boolean; maskClosable?: boolean; visible?: boolean; - closing?: boolean; - onClose(e: React.MouseEvent): void; + onClose(e: MouseEvent | TouchEvent | KeyboardEvent): void; } export class DialogElWrapper extends Component { @@ -139,7 +139,7 @@ export class DialogElWrapper extends Component { this.props.mask && this.props.maskClosable ) { - this.props.onClose(e); + this.props.onClose(e as any); } }; diff --git a/packages/zent/src/dialog/open.tsx b/packages/zent/src/dialog/open.tsx index d1a849eb32..37245d01e5 100644 --- a/packages/zent/src/dialog/open.tsx +++ b/packages/zent/src/dialog/open.tsx @@ -29,7 +29,7 @@ export interface ICloseDialogOption { } interface IStandaloneDialogProps { - options: IOpenDialogOption & { dialogId: string }; + options: Partial & { dialogId: string }; container: HTMLDivElement; } @@ -112,7 +112,7 @@ export interface IOpenDialogOption extends Omit { /* 打开一个dialog,返回值是一个用来关闭dialog的函数。 */ -export function openDialog(options: IOpenDialogOption = {}) { +export function openDialog(options: Partial = {}) { if (!isBrowser) return noop; const { dialogId = uniqueId('__zent-dialog__'), parentComponent } = options; diff --git a/packages/zent/src/loading/FullScreenLoading.tsx b/packages/zent/src/loading/FullScreenLoading.tsx index 1712729064..518305455d 100644 --- a/packages/zent/src/loading/FullScreenLoading.tsx +++ b/packages/zent/src/loading/FullScreenLoading.tsx @@ -1,16 +1,13 @@ import * as React from 'react'; import cx from 'classnames'; -import isUndefined from 'lodash-es/isUndefined'; +import isNumber from 'lodash-es/isNumber'; -import PurePortal from '../portal/PurePortal'; -import withNonScrollable from '../portal/withNonScrollable'; import useDelayed from './hooks/useDelayed'; import { IFullScreenLoadingProps, FullScreenDefaultProps } from './props'; import LoadingMask from './components/LoadingMask'; +import { Portal } from '../portal'; -const NO_STYLE = {}; - -const NonScrollablePurePortal = withNonScrollable(PurePortal); +const NO_STYLE: Partial = {}; export function FullScreenLoading(props: IFullScreenLoadingProps) { const { @@ -29,22 +26,21 @@ export function FullScreenLoading(props: IFullScreenLoadingProps) { return null; } - const style = isUndefined(zIndex) ? NO_STYLE : { zIndex }; + const style = isNumber(zIndex) ? { zIndex: `${zIndex}` } : NO_STYLE; return ( - -
- -
-
+ + + ); } diff --git a/packages/zent/src/loading/props.ts b/packages/zent/src/loading/props.ts index d47d97e22a..c97ce96304 100644 --- a/packages/zent/src/loading/props.ts +++ b/packages/zent/src/loading/props.ts @@ -1,12 +1,12 @@ import * as React from 'react'; export interface ILoadingBaseProps { - loading?: boolean; - delay?: number; - icon?: 'youzan' | 'circle'; + loading: boolean; + delay: number; + icon: 'youzan' | 'circle'; iconSize?: number; iconText?: React.ReactNode; - textPosition?: 'top' | 'bottom' | 'left' | 'right'; + textPosition: 'top' | 'bottom' | 'left' | 'right'; className?: string; } diff --git a/packages/zent/src/pop/position.ts b/packages/zent/src/pop/position.ts index af8a4d431c..50e86f2851 100644 --- a/packages/zent/src/pop/position.ts +++ b/packages/zent/src/pop/position.ts @@ -2,7 +2,6 @@ import capitalize from 'lodash-es/capitalize'; import isFunction from 'lodash-es/isFunction'; import Popover from '../popover'; -import { CSSProperties } from 'react'; const { Position } = Popover; @@ -12,7 +11,7 @@ const ARROW_OFFSET_V = 9; const createPosition = (x, y, side) => { return { - getCSSStyle(): CSSProperties { + getCSSStyle(): Partial { return { position: 'absolute', left: `${Math.round(x)}px`, diff --git a/packages/zent/src/popover/Content.tsx b/packages/zent/src/popover/Content.tsx index bdb15defc8..d3f1efd447 100644 --- a/packages/zent/src/popover/Content.tsx +++ b/packages/zent/src/popover/Content.tsx @@ -49,7 +49,7 @@ export interface IPopoverContentState { */ export default class PopoverContent extends Component< IPopoverContentProps, - any + IPopoverContentState > { positionReady: boolean; positionedParent: Element | null; @@ -57,7 +57,7 @@ export default class PopoverContent extends Component< constructor(props) { super(props); this.state = { - position: null, + position: (invisiblePlacement as any)(props.prefix), }; // 标记 content 的位置是否 ready @@ -135,7 +135,6 @@ export default class PopoverContent extends Component< containerBoundingBoxViewport: parentBoundingBox, } ); - if (!isEqualPlacement(this.state.position, position)) { this.setState( { @@ -162,7 +161,6 @@ export default class PopoverContent extends Component< componentDidMount() { const { visible } = this.props; - if (visible) { this.adjustPosition(); } @@ -188,20 +186,14 @@ export default class PopoverContent extends Component< } = this.props; const { position } = this.state; - if (!position) { - return null; - } - const cls = cx(className, `${prefix}-popover`, id, position.toString()); return (
{children} diff --git a/packages/zent/src/popover/placement/invisible.ts b/packages/zent/src/popover/placement/invisible.ts index 45fe18b4c4..b824418004 100644 --- a/packages/zent/src/popover/placement/invisible.ts +++ b/packages/zent/src/popover/placement/invisible.ts @@ -9,13 +9,13 @@ const locate: PositionFunctionImpl = () => { const y = -100000; return { - getCSSStyle() { + getCSSStyle(): Partial { return { position: 'fixed', left: `${x}px`, top: `${y}px`, - zIndex: -10, - opacity: 0, + zIndex: '-10', + opacity: '0', }; }, diff --git a/packages/zent/src/popover/position-function.ts b/packages/zent/src/popover/position-function.ts index 021157e616..974b257328 100644 --- a/packages/zent/src/popover/position-function.ts +++ b/packages/zent/src/popover/position-function.ts @@ -1,5 +1,5 @@ export interface IPopoverPosition { - getCSSStyle: () => React.CSSProperties; + getCSSStyle: () => Partial; name: string; } diff --git a/packages/zent/src/portal/ClosablePortal.tsx b/packages/zent/src/portal/ClosablePortal.tsx deleted file mode 100644 index 7956eba4d3..0000000000 --- a/packages/zent/src/portal/ClosablePortal.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; -import { Component } from 'react'; - -import Portal, { IPortalProps } from './Portal'; - -export interface IClosablePortalProps extends IPortalProps {} - -// visible的逻辑放在Portal里实现会比较烦,因为没法利用React的update机制。 -export class ClosablePortal extends Component { - static defaultProps = { - visible: true, - }; - - render() { - const { visible, ...portalProps } = this.props; - return visible && ; - } -} - -export default ClosablePortal; diff --git a/packages/zent/src/portal/LayeredPortal.tsx b/packages/zent/src/portal/LayeredPortal.tsx deleted file mode 100644 index 6801cb5901..0000000000 --- a/packages/zent/src/portal/LayeredPortal.tsx +++ /dev/null @@ -1,182 +0,0 @@ -import * as React from 'react'; -import { Component } from 'react'; -import isFunction from 'lodash-es/isFunction'; - -import PurePortal, { IPurePortalProps } from './PurePortal'; -import { - getNodeFromSelector, - createContainerNode, - removeNodeFromDOMTree, - isDescendant, -} from './util'; - -export interface ILayeredPortalProps extends IPurePortalProps { - visible?: boolean; - layer?: string; - useLayerForClickAway?: boolean; - onClickAway?: (e: TouchEvent | MouseEvent) => void; - onLayerReady?: (node: HTMLElement) => void; - className?: string; - style?: React.CSSProperties; -} - -/* - Portal的核心,只负责管理child。index.js实际export的不是这个component. -*/ -export class LayeredPortal extends Component { - static defaultProps = { - selector: 'body', - layer: 'div', - className: '', - visible: true, - }; - - // DOM node, the container of the portal content - layerNode: HTMLElement | null = null; - - // DOM node, the parent node of portal content - parentNode: Element | null = null; - - purePortalRef = React.createRef(); - - contains(el: Element) { - const purePortal = this.purePortalRef.current; - if (!purePortal) { - return false; - } - return purePortal.contains(el); - } - - onUnmount = () => { - this.unrenderLayer(); - - const layerNode = this.getLayer(); - if (layerNode) { - const { onUnmount } = this.props; - isFunction(onUnmount) && onUnmount(); - } - }; - - getLayer = () => this.layerNode; - - onClickAway = event => { - if ( - event.defaultPrevented || - !this.props.onClickAway || - !this.props.visible - ) { - return; - } - - const layerNode = this.getLayer(); - if ( - !(event.target instanceof Node) || - (event.target !== layerNode && event.target === window) || - (document.documentElement.contains(event.target) && - !isDescendant(layerNode, event.target)) - ) { - this.props.onClickAway(event); - } - }; - - undecorateLayer = layerNode => { - if (this.props.useLayerForClickAway) { - layerNode.style.position = 'relative'; - layerNode.removeEventListener('touchstart', this.onClickAway); - layerNode.removeEventListener('click', this.onClickAway); - } else { - window.removeEventListener('touchstart', this.onClickAway); - window.removeEventListener('click', this.onClickAway); - } - }; - - decorateLayer = (layerNode: HTMLElement, props = this.props) => { - const { onLayerReady, className, style } = props; - - // 1, Customize the className and style for layer node. - layerNode.className = className || ''; - const cssMap = style || (props as any).css || {}; - const cssKeys = Object.keys(cssMap); - if (cssMap && cssKeys.length) { - layerNode.style.cssText = cssKeys - .map(k => `${k}: ${cssMap[k]}`) - .join('; '); - } - - // 2, Predefined layer decorations - if (this.props.useLayerForClickAway) { - layerNode.addEventListener('touchstart', this.onClickAway); - layerNode.addEventListener('click', this.onClickAway); - layerNode.style.position = - this.parentNode === document.body ? 'fixed' : 'absolute'; - layerNode.style.top = '0'; - layerNode.style.bottom = '0'; - layerNode.style.left = '0'; - layerNode.style.right = '0'; - } else if (this.props.onClickAway) { - setTimeout(() => { - window.addEventListener('touchstart', this.onClickAway); - window.addEventListener('click', this.onClickAway); - }, 0); - } - - // 3, Callback when layer node is ready - onLayerReady && onLayerReady(this.layerNode); - }; - - unrenderLayer = () => { - const layerNode = this.getLayer(); - - if (layerNode) { - this.undecorateLayer(layerNode); - - removeNodeFromDOMTree(layerNode); - - // Reset - this.layerNode = null; - this.parentNode = null; - - isFunction(this.props.onUnmount) && this.props.onUnmount(); - } - }; - - renderLayer = (props = this.props) => { - if (props.visible) { - // Cache the parentNode - if (!this.parentNode) { - const { selector } = props; - this.parentNode = getNodeFromSelector(selector); - } - - // Create the layer DOM node for portal content - const { layer } = props; - if (!this.layerNode) { - this.layerNode = createContainerNode(this.parentNode, layer); - } - - // customize the container, e.g. style, event listener - this.decorateLayer(this.layerNode); - } - }; - - render() { - this.renderLayer(); - - // Render the portal content to container node or parent node - const { children, render, visible } = this.props; - const content = render ? render() : children; - - return visible ? ( - - {content} - - ) : null; - } -} - -export default LayeredPortal; diff --git a/packages/zent/src/portal/Portal.tsx b/packages/zent/src/portal/Portal.tsx index 54c3540d3b..3ba4fd4ae6 100644 --- a/packages/zent/src/portal/Portal.tsx +++ b/packages/zent/src/portal/Portal.tsx @@ -1,47 +1,203 @@ import * as React from 'react'; -import { Component } from 'react'; -import cx from 'classnames'; +import { + useRef, + useImperativeHandle, + useLayoutEffect, + useMemo, + forwardRef, + useEffect, + useCallback, +} from 'react'; +import * as keycode from 'keycode'; +import noop from 'lodash-es/noop'; -import LayeredPortal, { ILayeredPortalProps } from './LayeredPortal'; -import withESCToClose from './withESCToClose'; -import withNonScrollable from './withNonScrollable'; -import PurePortal from './PurePortal'; +import PurePortal, { IPurePortalProps } from './PurePortal'; +import { getNodeFromSelector, hasScrollbarY } from './util'; +import { SCROLLBAR_WIDTH } from '../utils/getScrollbarWidth'; -export interface IPortalProps extends ILayeredPortalProps { - prefix?: string; +export interface IPortalProps extends Partial { + visible?: boolean; + layer?: string; + onLayerReady?: (node: HTMLElement) => void; + blockPageScroll?: boolean; + closeOnESC?: boolean; + closeOnClickOutside?: boolean; + useLayerForClickAway?: boolean; + onClose?: (e: KeyboardEvent | TouchEvent | MouseEvent) => void; + children?: React.ReactNode; + className?: string; + style?: Partial; } -export class Portal extends Component { - static defaultProps = { - prefix: 'zent', - visible: true, - }; - - static withESCToClose = withESCToClose; - static withNonScrollable = withNonScrollable; - static PurePortal = PurePortal; - static LayeredPortal = LayeredPortal; - - layeredPortalRef = React.createRef(); - - contains(el: Element) { - const layeredPortal = this.layeredPortalRef.current; - if (!layeredPortal) { - return false; - } - return layeredPortal.contains(el); - } +export interface IPortalImperativeHandlers { + contains(node: Node): boolean; + purePortalRef: React.RefObject; +} - render() { - const { prefix, className, ...other } = this.props; - return ( - +export const Portal = forwardRef( + (props, ref) => { + const { + visible = true, + layer = 'div', + selector = 'body', + useLayerForClickAway = false, + className, + style, + blockPageScroll = false, + closeOnESC = false, + closeOnClickOutside = false, + children, + append, + } = props; + const node = useMemo(() => document.createElement(layer), [layer]); + const parent = useMemo(() => getNodeFromSelector(selector), [selector]); + const propsRef = useRef(props); + propsRef.current = props; + const purePortalRef = useRef(null); + + // Methods for use on ref + const contains = useCallback((node: Node) => { + const purePortal = purePortalRef.current; + if (!purePortal) { + return false; + } + return purePortal.contains(node); + }, []); + useImperativeHandle( + ref, + () => ({ + contains, + purePortalRef, + }), + [] ); + + useLayoutEffect(() => { + if (!visible || !parent) { + return noop; + } + parent.appendChild(node); + className && (node.className = className); + if (style) { + const cssKeys = Object.keys(style) as Array; + if (cssKeys.length) { + node.style.cssText = cssKeys.map(k => `${k}: ${style[k]}`).join('; '); + } + } + return () => { + parent.removeChild(node); + }; + }, [visible, node, parent, style, className]); + + useLayoutEffect(() => { + if (!visible || !useLayerForClickAway) { + return noop; + } + const { position, top, bottom, left, right } = node.style; + node.style.position = parent === document.body ? 'fixed' : 'absolute'; + node.style.top = '0'; + node.style.bottom = '0'; + node.style.left = '0'; + node.style.right = '0'; + return () => { + node.style.position = position; + node.style.top = top; + node.style.bottom = bottom; + node.style.left = left; + node.style.right = right; + }; + }, [node, useLayerForClickAway, visible]); + + useLayoutEffect(() => { + if ( + !visible || + !blockPageScroll || + !parent || + !(parent instanceof HTMLElement) || + !hasScrollbarY(parent) + ) { + return noop; + } + const { overflowY, paddingRight } = parent.style; + const originalPadding = getComputedStyle(parent).paddingRight; + const newPadding = parseFloat(originalPadding || '0') + SCROLLBAR_WIDTH; + parent.style.overflowY = 'hidden'; + parent.style.paddingRight = `${newPadding}px`; + return () => { + parent.style.overflowY = overflowY; + parent.style.paddingRight = paddingRight; + }; + }, [parent, visible, blockPageScroll]); + + useLayoutEffect(() => { + if (!visible) { + return noop; + } + + function handler(event: TouchEvent | MouseEvent) { + const { closeOnClickOutside, onClose } = propsRef.current; + if (event.defaultPrevented || !closeOnClickOutside || !visible) { + return; + } + + const { target } = event; + if (!(target instanceof Node) || target === node || !contains(target)) { + onClose && onClose(event); + } + } + + let dispose = noop; + if (closeOnClickOutside) { + if (useLayerForClickAway) { + node.addEventListener('touchstart', handler); + node.addEventListener('click', handler); + dispose = () => { + node.removeEventListener('touchstart', handler); + node.removeEventListener('click', handler); + }; + } else { + window.addEventListener('touchstart', handler); + window.addEventListener('click', handler); + dispose = () => { + window.removeEventListener('touchstart', handler); + window.removeEventListener('click', handler); + }; + } + } + + const { onLayerReady } = propsRef.current; + onLayerReady && onLayerReady(node); + + return dispose; + }, [visible, useLayerForClickAway, !!closeOnClickOutside, node]); + + useEffect(() => { + if (!visible || !closeOnESC) { + return noop; + } + function onKeyUp(e: KeyboardEvent) { + const { onClose } = propsRef.current; + if (!onClose) { + return; + } + if (keycode(e) === 'esc') { + onClose(e); + } + } + document.body.addEventListener('keyup', onKeyUp); + return () => { + document.body.removeEventListener('keyup', onKeyUp); + }; + }, [closeOnESC, visible]); + + return visible ? ( + + {children} + + ) : null; } -} +); + +Portal.displayName = 'ZentPortal'; export default Portal; diff --git a/packages/zent/src/portal/PortalContent.tsx b/packages/zent/src/portal/PortalContent.tsx deleted file mode 100644 index 614c057dbe..0000000000 --- a/packages/zent/src/portal/PortalContent.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import * as React from 'react'; - -export interface IPortalContentProps { - onMount?: () => void; - onUnmount?: () => void; -} - -export default class PortalContent extends React.Component< - IPortalContentProps -> { - componentDidMount() { - const { onMount } = this.props; - - onMount && onMount(); - } - - componentWillUnmount() { - const { onUnmount } = this.props; - - onUnmount && onUnmount(); - } - - render() { - return this.props.children; - } -} diff --git a/packages/zent/src/portal/PurePortal.tsx b/packages/zent/src/portal/PurePortal.tsx index b9f94e8ce4..eecb662107 100644 --- a/packages/zent/src/portal/PurePortal.tsx +++ b/packages/zent/src/portal/PurePortal.tsx @@ -1,20 +1,21 @@ import * as React from 'react'; import { Component } from 'react'; import { createPortal } from 'react-dom'; -import memoize from '../utils/memorize-one'; +import memoize from '../utils/memorize-one'; import { getNodeFromSelector, removeAllChildren } from './util'; -import PortalContent, { IPortalContentProps } from './PortalContent'; import { IPortalContext, PortalContext } from './context'; -export interface IPurePortalProps extends IPortalContentProps { - render?: () => React.ReactNode; - selector?: string | HTMLElement; +export interface IPurePortalProps { + selector: string | HTMLElement; append?: boolean; } /** - * Pure portal, render the content (from render prop or from the only children) into the container + * A thin wrapper around React.createPortal with + * + * 1. Awareness of nested portals + * 2. `append=false` to mimic old `unstable_renderIntoContainer` behavior for backward compatibility */ export class PurePortal extends Component { static defaultProps = { @@ -25,12 +26,15 @@ export class PurePortal extends Component { context!: IPortalContext; private readonly childContext: IPortalContext = { - children: new Set(), + children: [], }; getContainer = memoize( - (selector: string | HTMLElement): Element => { + (selector: string | HTMLElement): Element | null => { const node = getNodeFromSelector(selector); + if (!node) { + return node; + } if (!this.props.append) { removeAllChildren(node); } @@ -39,7 +43,7 @@ export class PurePortal extends Component { } ); - contains(el: Element): boolean { + contains(el: Node): boolean { const container = this.getContainer(this.props.selector); if (!container) { return false; @@ -47,29 +51,28 @@ export class PurePortal extends Component { if (container.contains(el)) { return true; } - let ret = false; - this.childContext.children.forEach(child => { + for (const child of this.childContext.children) { if (child.contains(el)) { - ret = true; + return true; } - }); - return ret; + } + return false; } componentDidMount() { - this.context.children.add(this); + this.context.children.push(this); } componentWillUnmount() { - this.context.children.delete(this); + const index = this.context.children.indexOf(this); + if (index !== -1) { + this.context.children.splice(index, 1); + } } render() { - const { selector: container, onMount, onUnmount } = this.props; - - // Render the portal content to container node or parent node - const { children, render } = this.props; - const content = render ? render() : children; + const { selector: container } = this.props; + const { children } = this.props; const domNode = this.getContainer(container); if (!domNode) { @@ -78,9 +81,7 @@ export class PurePortal extends Component { return createPortal( - - {content} - + {children} , domNode ); diff --git a/packages/zent/src/portal/README_en-US.md b/packages/zent/src/portal/README_en-US.md index 5659d80177..0c7a36acd7 100644 --- a/packages/zent/src/portal/README_en-US.md +++ b/packages/zent/src/portal/README_en-US.md @@ -19,85 +19,24 @@ Portal provides a first-class way to render children into a DOM node that exists | children | Only supports one child | string | No | | | | selector | DOM node to render child | string or DOM Element | No | `'body'` | legal CSS selector or certain DOM node | | visible | Whether to render child | bool | No | `true` | | -| onMount | Callback after child is mounted | func | No | | | -| onUnmount | Callback after child is unmounted | func | No | | | | layer | The layer curtain tag name | string | No | `div` | | | useLayerForClickAway | Whether to use a layer for click away from `Portal` | boolean | No | false | | -| onClickAway | The callback when user clicks away from `Portal` | function | No | | | -| onLayerReady | The hook when layer is ready | function | No | | | -| className | The layer class name | string | No | `''` | | +| closeOnClickOutside | Close portal when click outside of portal | bool | No | `false` | `true` | +| closeOnESC | Close portal when pressing ESC | bool | No | `false` | `true` | +| blockPageScroll | Block page scroll when portal is open | bool | No | `false` | `true` | +| onClose | Callback when portal closes | (e: event) => void | No | | | +| onLayerReady | The hook when layer is ready | (node: HTMLElement) => void | No | | | +| className | The layer class name | string | No | | | | style | The layer style | object | No | | | | css | (Deprecated, use style instead) Extra css style. such as, `{ 'margin-left': '10px' }` | object | No | `{}` | | -| prefix | Custom prefix | string | No | `'zent'` | | -`Portal` provides some high-level components(HOC),including some logics that are generally used in popovers. - -#### withESCToClose - -Implements close on ESC. - -| Property | Description | Type | Required | Default | -| ------- | --------------- | ---------- | ---- | ------ | -| visible | Is portal visible | bool | Yes | `true` | -| onClose | Callback when portal closes | func | Yes | | | - -```jsx -import { Portal as _Portal } from 'zent'; -const { withESCToClose } = _Portal; -const Portal = withESCToClose(_Portal); -``` - -#### withNonScrollable - -Disable scroll on body when portal is open. - -| Property | Description | Type | Required | Default | -| ------- | ------------------------- | ---- | ------ | ---- | -| visible | Is Portal visible | bool | Yes | `true` | - -```jsx -import { Portal as _Portal } from 'zent'; -const { withNonScrollable } = _Portal; -const Portal = withNonScrollable(_Portal); -``` +There's a `contains` method on `Portal` instance which can be used to check if a DOM node is a decedent of the portal. This method works with nested portals. ### Principle - The widget is mainly used to insert it's `child` to given DOM node, and it is removed from DOM when component is unmounted. - A certain degree of repaint occurs when any props are modified, and `children`, `selector`'s change will trigger component `unmount` to `mount`; when other props is modified, only existing DOM node attributes update. -### Known issues - -- Using string `ref` on Portal's `children` throws error, to avoid this question, you can use functional `ref`. the reason is Portal's `chilren` has no owner, if you want to read more detail about this issue, click [ Here](https://github.com/facebook/react/blob/v15.0.2/src/renderers/shared/reconciler/ReactRef.js#L18). Using string `ref` on Portal's `children` is also not encouraged by official react team. - -- On `15.0.2` version, React has a bug that the `context` rely on `state` does not update in some case(refer to example: 02-context), please wait React version updates - -## LayeredPortal - -Layered portal widget。 - -### Guides - -This component is which `Portal` depends on internally, the difference against `Portal` is that `LayeredPortal` does not contain any pre-defined classNames such as prefix - -### LayeredPortal-API - -| Property | Description | Type | Required | Default | Alternative | -| --------- | ----------------- | ---------- | ----------- | -------- | -------------------- | -| children | Only supports one child | string | No | | | -| render | Render the content of `LayeredPortal`, prior to children | func | No | | | -| selector | DOM node to render child | string or DOM Element | No | `'body'` | legal CSS selector or certain DOM node | -| visible | Whether to render child | bool | No | `true` | | -| onMount | Callback after child is mounted | func | No | | | -| onUnmount | Callback after child is unmounted | func | No | | | -| layer | The layer curtain tag name | string | No | `div` | | -| useLayerForClickAway | Whether to use a layer for click away from `Portal` | boolean | No | false | | -| onClickAway | The callback when user clicks away from `Portal` | function | No | | | -| onLayerReady | The hook when layer is ready | function | No | | | -| className | The layer class name | string | No | `''` | | -| style | The layer style | object | No | | | -| css | (Deprecated, use style instead) Extra css style. such as, `{ 'margin-left': '10px' }` | object | No | `{}` | | - ## PurePortal Pure portal widget。 @@ -111,7 +50,5 @@ Portal behaves like React 16 Portal,which will overwrite all content inside it | Property | Description | Type | Required | Default | Alternative | | --------- | ----------------- | ---------- | ----------- | -------- | -------------------- | | children | Only supports one child | string | No | | | -| render | Render the content of `LayeredPortal`, prior to children | func | No | | | | selector | DOM node to render child | string or DOM Element | No | `'body'` | legal CSS selector or certain DOM node | -| onMount | Callback after child is mounted | func | No | | | -| onUnmount | Callback after child is unmounted | func | No | | | +| append | Should append content to the container, if false, the container will be cleaned | bool | No | false | | diff --git a/packages/zent/src/portal/README_zh-CN.md b/packages/zent/src/portal/README_zh-CN.md index 74931b6e68..fcb1fa2b40 100644 --- a/packages/zent/src/portal/README_zh-CN.md +++ b/packages/zent/src/portal/README_zh-CN.md @@ -20,84 +20,24 @@ group: 基础 | children | 只支持一个child | string | 否 | | | | selector | 渲染child的DOM节点 | string or DOM Element | 否 | `'body'` | 合法的CSS selector或者某个DOM节点 | | visible | 是否渲染child | bool | 否 | `true` | | -| onMount | `children` 被 mount 之后的回调函数 | func | 否 | | | -| onUnmount | `children` 被 unmount 之后的回调函数 | func | 否 | | | | layer | 遮罩的标签名 | string | 否 | `div` | | -| useLayerForClickAway | 是否使用遮罩来触发`Portal`关闭 | boolean | 否 | false | | -| onClickAway | 点击到非`Portal`处的回调 | function | 否 | | | -| onLayerReady | 遮罩准备好时的hook | function | 否 | | | -| className | 遮罩的className | string | 否 | `''` | | +| useLayerForClickAway | 是否使用遮罩来触发 `Portal` 关闭 | bool | 否 | `false` | | +| onLayerReady | 遮罩准备好时的hook | (node: HTMLElement) => void | 否 | | +| closeOnClickOutside | 点击到 `Portal` 外部时关闭 | function | 否 | | +| closeOnESC | 按下 ESC 键时关闭 | bool | 否 | `false` | | +| onClose | 关闭时回调函数 | (e: Event) => void | 否 | | +| blockPageScroll | 打开时禁止页面滚动 | bool | 否 | `false` | | +| className | 遮罩的className | string | 否 | | | | style | 遮罩的style | object | 否 | | | | css | (已废弃, 请使用style)额外的css样式. 例如, `{ 'margin-left': '10px' }` | object | 否 | `{}` | | -| prefix | 自定义前缀 | string | 否 | `'zent'` | | -`Portal` 另外还提供了几个高阶组件(HOC),提供了一些弹层常用的逻辑。 - -#### withESCToClose - -封装了按ESC关闭的逻辑. - -| 参数 | 说明 | 类型 | 是否必须 | 默认值 | -| ------- | ------------------------- | ---- | ------ | ---- | -| visible | 注意这个属性原始的Portal是可选的 | bool | 是 | `true` | -| onClose | ESC按下是的回调函数 | func | 是 | | | - -```jsx -import { Portal as _Portal } from 'zent'; -const { withESCToClose } = _Portal; -const Portal = withESCToClose(_Portal); -``` - -#### withNonScrollable - -封装了禁止container滚动的逻辑. - -| 参数 | 说明 | 类型 | 是否必须 | 默认值 | -| ------- | ------------------------- | ---- | ------ | -| visible | 注意这个属性原始的Portal是可选的 | bool | 是 | `true` | - -```jsx -import { Portal as _Portal } from 'zent'; -const { withNonScrollable } = _Portal; -const Portal = withNonScrollable(_Portal); -``` +`Portal` 实例上有一个 `contains` 方法可以用来判断一个 DOM 节点是否是它的子节点,这个方法对嵌套的 `Portal` 内的子节点一样有效。 ### 组件原理 - 组件的主要功能是把其 `child` 插入到一个给定的 DOM node中, 并且在组件被 `unmount` 的时候将其 `child` 属性对应的 DOM 节点删除. - 任意 props 被修改后会触发一定程度的重绘, `children`, `selector`被修改会导致组件 `unmount` 再 `mount`;其它props被修改仅更新现有 DOM node 的属性. -### 已知问题 - -- 在 Portal 的 `children` 上使用字符串形式的 `ref` 会报错, 可以使用函数形式的 `ref` 绕过这个问题. 其原因是 Portal 的 `children` 没有owner, 使用函数形式的`ref`可以绕过这个问题的原因参见[ Here](https://github.com/facebook/react/blob/v15.0.2/src/renderers/shared/reconciler/ReactRef.js#L18). 此外官方也不鼓励使用字符串形式的 `ref`. - -- `15.0.2` 版本的 React 有个 bug 会导致某些情况下依赖 `state` 的 `context` 不更新 (参考 example: 02-context), 请等待 React 版本的统一升级. - -## LayeredPortal 遮罩传送门 - -遮罩传送门组件。 - -### 使用场景 - -这个组件是`Portal`依赖的内部实现,与`Portal`不同的地方在于`LayeredPortal`没有预设的任何样式,例如prefix等。 - -### LayeredPortal-API - -| 参数 | 说明 | 类型 | 是否必须 | 默认值 | 备选值 | -| --------- | ----------------- | ---------- | ----------- | -------- | ------------------- | -| children | 只支持一个child | string | 否 | | | -| render | 返回传送门需要渲染的内容,优先级高于children | func | 否 | | | -| selector | 渲染child的DOM节点 | string or DOM Element | 否 | `'body'` | 合法的CSS selector或者某个DOM节点 | -| visible | 是否渲染child | bool | 否 | `true` | | -| onMount | `children` 被 mount 之后的回调函数 | func | 否 | | | -| onUnmount | `children` 被 unmount 之后的回调函数 | func | 否 | | | -| layer | 遮罩的标签名 | string | 否 | `div` | | -| useLayerForClickAway | 是否使用遮罩来触发`Portal`关闭 | boolean | 否 | false | | -| onClickAway | 点击到非`Portal`处的回调 | function | 否 | | | -| onLayerReady | 遮罩准备好时的hook | function | 否 | | | -| className | 遮罩的className | string | 否 | `''` | | -| style | 遮罩的style | object | 否 | | | - ## PurePortal 覆盖式传送门 覆盖式传送门组件。 @@ -111,7 +51,5 @@ const Portal = withNonScrollable(_Portal); | 参数 | 说明 | 类型 | 是否必须 | 默认值 | 备选值 | | --------- | ----------------- | ---------- | ----------- | -------- | ------------------- | | children | 只支持一个child | string | 否 | | | -| render | 返回传送门需要渲染的内容,优先级高于children | func | 否 | | | -| selector | 渲染child的DOM节点 | string or DOM Element | 是 | `'body'` | 合法的CSS selector或者某个DOM节点 | -| onMount | `children` 被 mount 之后的回调函数 | func | 否 | | | -| onUnmount | `children` 被 unmount 之后的回调函数 | func | 否 | | | +| selector | 渲染 child 的 DOM 节点 | string or DOM Element | 是 | `'body'` | 合法的CSS selector 或者某个 DOM 节点 | +| append | 是否在将内容添加到容器中,如果是 false 会覆盖容器的内容 | bool | 否 | false | | diff --git a/packages/zent/src/portal/context.ts b/packages/zent/src/portal/context.ts index 73cc242c5f..66f1144b86 100644 --- a/packages/zent/src/portal/context.ts +++ b/packages/zent/src/portal/context.ts @@ -2,9 +2,12 @@ import PurePortal from './PurePortal'; import { createContext } from 'react'; export interface IPortalContext { - children: Set; + /** + * Array is faster than Set according to perf + */ + children: PurePortal[]; } export const PortalContext = createContext({ - children: new Set(), + children: [], }); diff --git a/packages/zent/src/portal/demos/basic.md b/packages/zent/src/portal/demos/basic.md index 37ee08660e..9f80f6b618 100644 --- a/packages/zent/src/portal/demos/basic.md +++ b/packages/zent/src/portal/demos/basic.md @@ -18,10 +18,9 @@ en-US: --- ```jsx -import { Portal, Button } from 'zent'; +import { Portal, Button, PurePortal } from 'zent'; -const PurePortal = Portal.PurePortal; -const WrappedPortal = Portal.withNonScrollable(Portal.withESCToClose(Portal)); +// const WrappedPortal = Portal.withNonScrollable(Portal.withESCToClose(Portal)); class PortalBasic extends Component { state = { @@ -51,13 +50,15 @@ class PortalBasic extends Component { ) : null} -
{i18n.bodyPortalContent}
-
+
); } diff --git a/packages/zent/src/portal/index.ts b/packages/zent/src/portal/index.ts index bdbee6b90c..20f265c1cf 100644 --- a/packages/zent/src/portal/index.ts +++ b/packages/zent/src/portal/index.ts @@ -1,5 +1,4 @@ import Portal from './Portal'; -export * from './Portal'; export * from './PurePortal'; -export * from './LayeredPortal'; +export * from './Portal'; export default Portal; diff --git a/packages/zent/src/portal/util.ts b/packages/zent/src/portal/util.ts index b6a35a6555..f829133f5d 100644 --- a/packages/zent/src/portal/util.ts +++ b/packages/zent/src/portal/util.ts @@ -1,7 +1,9 @@ -export function getNodeFromSelector(selector: string | Element): Element { +export function getNodeFromSelector( + selector: string | Element +): Element | null { const node = typeof selector === 'string' ? document.querySelector(selector) : selector; - return node || document.body; + return node; } export function createContainerNode(parent: Node, tag = 'div') { @@ -9,26 +11,15 @@ export function createContainerNode(parent: Node, tag = 'div') { return parent.appendChild(div); } -export function removeNodeFromDOMTree(node: Node) { - const { parentNode } = node; - if (parentNode) { - parentNode.removeChild(node); - } -} - -export function isDescendant(parent: Node, child: Node) { - let node = child.parentNode; - - while (node !== null) { - if (node === parent) return true; - node = node.parentNode; - } - - return false; -} - export function removeAllChildren(node: Node) { while (node && node.firstChild) { node.removeChild(node.firstChild); } } + +export function hasScrollbarY(element: Element) { + if (element === document.body) { + return element.scrollHeight > window.innerHeight; + } + return element.scrollHeight > element.clientHeight; +} diff --git a/packages/zent/src/portal/withESCToClose.tsx b/packages/zent/src/portal/withESCToClose.tsx deleted file mode 100644 index 989c292f0e..0000000000 --- a/packages/zent/src/portal/withESCToClose.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import * as React from 'react'; -import { Component } from 'react'; -import * as keycode from 'keycode'; - -export interface IESCToCloseWrapperProps { - onClose(e: KeyboardEvent): void; - visible?: boolean; -} - -/* - Not exported in index.js. - - Provides an HOC component for ESC to close functionality, useful in some cases. -*/ -export default function withESCToClose

( - Closable: React.ComponentType

-) { - return class ESCToCloseWrapper extends Component< - IESCToCloseWrapperProps & P - > { - onKeyUp = (evt: KeyboardEvent) => { - if (keycode(evt) === 'esc') { - this.props.onClose(evt); - } - }; - - on() { - document.body.addEventListener('keyup', this.onKeyUp, true); - } - - off() { - document.body.removeEventListener('keyup', this.onKeyUp, true); - } - - componentDidMount() { - if (this.props.visible) { - this.on(); - } - } - - componentWillUnmount() { - if (this.props.visible) { - this.off(); - } - } - - componentWillReceiveProps(nextProps) { - if (nextProps.visible !== this.props.visible) { - if (nextProps.visible) { - this.on(); - } else { - this.off(); - } - } - } - - render() { - const { onClose, ...props } = this.props; - return ; - } - }; -} diff --git a/packages/zent/src/portal/withNonScrollable.tsx b/packages/zent/src/portal/withNonScrollable.tsx deleted file mode 100644 index 1ffb7a36b6..0000000000 --- a/packages/zent/src/portal/withNonScrollable.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import * as React from 'react'; -import { Component } from 'react'; -import isUndefined from 'lodash-es/isUndefined'; - -import { getNodeFromSelector } from './util'; -import { IPurePortalProps } from './PurePortal'; - -export interface INonScrollableWrapperProps { - selector?: string | HTMLElement; - visible?: boolean; -} - -/* - Provides an HOC component for ensuring container is non-scrollable during component - lifecycle. - - PurePortal has no `visible` prop. -*/ -export default function withNonScrollable

( - Portal: React.ComponentType

-) { - let portalVisibleCount = 0; - let originalOverflow; - - return class NonScrollableWrapper extends Component< - P & INonScrollableWrapperProps - > { - static defaultProps = { - selector: 'body', - }; - - restoreStyle() { - portalVisibleCount--; - - if (portalVisibleCount <= 0) { - const node = getNodeFromSelector(this.props.selector); - if (node instanceof HTMLElement) { - node.style.overflow = originalOverflow; - } - } - } - - saveStyle() { - portalVisibleCount++; - - if (portalVisibleCount === 1) { - const node = getNodeFromSelector(this.props.selector); - if (node instanceof HTMLElement) { - const { style } = node; - originalOverflow = style.overflow; - style.overflow = 'hidden'; - } - } - } - - componentDidMount() { - const { visible } = this.props; - - if (isUndefined(visible) || visible) { - this.saveStyle(); - } - } - - componentWillUnmount() { - const { visible } = this.props; - - if (isUndefined(visible) || visible) { - this.restoreStyle(); - } - } - - componentWillReceiveProps(nextProps) { - if (this.props.visible !== nextProps.visible) { - if (nextProps.visible === false) { - this.restoreStyle(); - } else { - this.saveStyle(); - } - } - } - - render() { - return ; - } - }; -} diff --git a/packages/zent/src/preview-image/Image.tsx b/packages/zent/src/preview-image/Image.tsx index 97154881b2..fe0ffb2238 100644 --- a/packages/zent/src/preview-image/Image.tsx +++ b/packages/zent/src/preview-image/Image.tsx @@ -7,10 +7,6 @@ import { I18nReceiver as Receiver } from '../i18n'; import Portal from '../portal'; import Icon from '../icon'; -// 有关闭按钮的时候同时具有ESC关闭的行为 -const { withNonScrollable, withESCToClose } = Portal; -const ImagePortalESCToClose = withESCToClose(withNonScrollable(Portal)); - export interface IPreviewImageProps { className: string; prefix: string; @@ -18,7 +14,7 @@ export interface IPreviewImageProps { images: any[]; index: number; onClose(): void; - scaleRatio?: number; + scaleRatio: number; } export default class Image extends Component { @@ -38,7 +34,7 @@ export default class Image extends Component { scaleRatio: 1.5, }; - onMaskClick = e => { + onMaskClick = (e: React.MouseEvent) => { if (e.target === e.currentTarget) { this.props.onClose(); } @@ -142,10 +138,12 @@ export default class Image extends Component { }); return ( -

@@ -218,7 +216,7 @@ export default class Image extends Component {
- +
); } } diff --git a/packages/zent/src/preview-image/previewImage.tsx b/packages/zent/src/preview-image/previewImage.tsx index 9e5bde4bb3..dd268b2b72 100644 --- a/packages/zent/src/preview-image/previewImage.tsx +++ b/packages/zent/src/preview-image/previewImage.tsx @@ -16,7 +16,7 @@ export interface IPreviewImageConfig { export function previewImage(options: IPreviewImageConfig = {}) { const { parentComponent, ...rest } = options; - let container = document.createElement('div'); + let container: HTMLElement | null = document.createElement('div'); const closePreviewMask = () => { if (!container) { @@ -24,7 +24,7 @@ export function previewImage(options: IPreviewImageConfig = {}) { } ReactDOM.unmountComponentAtNode(container); - container = undefined; + container = null; }; const props = { diff --git a/packages/zent/src/select/Popup.tsx b/packages/zent/src/select/Popup.tsx index 9971120b67..743843a7b6 100644 --- a/packages/zent/src/select/Popup.tsx +++ b/packages/zent/src/select/Popup.tsx @@ -62,7 +62,7 @@ class Popup extends Component { this.focused = false; } - componentWillMount() { + componentDidMount() { const { autoWidth, popover } = this.props; if (autoWidth) { this.setState({ @@ -71,9 +71,6 @@ class Popup extends Component { }, }); } - } - - componentDidMount() { this.popup.addEventListener('mousewheel', this.handleScroll); } diff --git a/packages/zent/src/utils/component/BodyEventHandler.tsx b/packages/zent/src/utils/component/BodyEventHandler.tsx new file mode 100644 index 0000000000..412d187b62 --- /dev/null +++ b/packages/zent/src/utils/component/BodyEventHandler.tsx @@ -0,0 +1,31 @@ +import * as React from 'react'; + +export interface IBodyEventHandlerProps< + K extends keyof HTMLBodyElementEventMap +> { + eventName: K; + useCapture?: boolean; + callback(e: HTMLBodyElementEventMap[K]): void; +} + +export class BodyEventHandler< + K extends keyof HTMLBodyElementEventMap +> extends React.Component> { + handler = (e: HTMLBodyElementEventMap[K]) => { + this.props.callback(e); + }; + + componentDidMount() { + const { eventName, useCapture } = this.props; + document.body.addEventListener(eventName, this.handler, useCapture); + } + + componentWillUnmount() { + const { eventName, useCapture } = this.props; + document.body.removeEventListener(eventName, this.handler, useCapture); + } + + render() { + return null; + } +} diff --git a/packages/zent/src/utils/component/WindowResizeHandler.tsx b/packages/zent/src/utils/component/WindowResizeHandler.tsx index af4cbb3ea1..9a7b8d6f6f 100644 --- a/packages/zent/src/utils/component/WindowResizeHandler.tsx +++ b/packages/zent/src/utils/component/WindowResizeHandler.tsx @@ -26,7 +26,7 @@ export default class WindowResizeHandler extends Component< _prevViewportSize: { width: number; height: number; - }; + } | null = null; onResize = (evt: UIEvent) => { const viewportSize = getViewportSize(); diff --git a/packages/zent/src/utils/getScrollbarWidth.ts b/packages/zent/src/utils/getScrollbarWidth.ts new file mode 100644 index 0000000000..1c9916502f --- /dev/null +++ b/packages/zent/src/utils/getScrollbarWidth.ts @@ -0,0 +1,22 @@ +import isBrowser from './isBrowser'; + +function getScrollbarWidth() { + if (!isBrowser) { + return 0; + } + const scrollDiv = document.createElement('div'); + Object.assign(scrollDiv.style, { + position: 'absolute', + top: '-9999px', + width: '50px', + height: '50px', + overflow: 'scroll', + }); + document.body.appendChild(scrollDiv); + const scrollbarWidth = + scrollDiv.getBoundingClientRect().width - scrollDiv.clientWidth; + document.body.removeChild(scrollDiv); + return scrollbarWidth; +} + +export const SCROLLBAR_WIDTH = getScrollbarWidth();