{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