From e0e50f4ba3d52438bdb4e340335ac56c6195756b Mon Sep 17 00:00:00 2001 From: hkaikai <617760820@qq.com> Date: Thu, 8 Aug 2024 20:37:13 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat(popup):=20=E5=AF=B9=E9=BD=90vue=20mobi?= =?UTF-8?q?le?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fix #407 --- site/mobile/main.jsx | 2 +- site/web/main.jsx | 2 +- src/_util/renderTNode.ts | 22 ++++++ src/_util/renderToContainer.ts | 15 ++++ src/common.ts | 2 +- src/overlay/Overlay.tsx | 2 + src/popup/Popup.tsx | 99 +++++++++++++++++++------ src/popup/_example/base.jsx | 77 ++++++------------- src/popup/_example/custom-close.jsx | 35 +++++++++ src/popup/_example/placement-bottom.jsx | 26 ++++--- src/popup/_example/placement-center.jsx | 26 ++++--- src/popup/_example/placement-left.jsx | 26 ++++--- src/popup/_example/placement-right.jsx | 26 ++++--- src/popup/_example/placement-top.jsx | 26 ++++--- src/popup/_example/style/index.less | 65 ++++++++-------- src/popup/_example/with-title.jsx | 32 ++++++++ src/popup/defaultProps.ts | 5 +- src/popup/popup.en-US.md | 24 ++++++ src/popup/popup.md | 23 ++++-- src/popup/style/index.js | 2 +- src/popup/type.ts | 46 +++++++++++- 21 files changed, 397 insertions(+), 186 deletions(-) create mode 100644 src/_util/renderTNode.ts create mode 100644 src/popup/_example/custom-close.jsx create mode 100644 src/popup/_example/with-title.jsx create mode 100644 src/popup/popup.en-US.md diff --git a/site/mobile/main.jsx b/site/mobile/main.jsx index 953e9aba..73a19955 100644 --- a/site/mobile/main.jsx +++ b/site/mobile/main.jsx @@ -4,7 +4,7 @@ import App from './App'; import '../style/mobile/index.less'; import '../../src/_common/style/mobile/_reset.less'; -import '../../src/_common/style/mobile/index.less'; +// import '../../src/_common/style/mobile/index.less'; ReactDOM.render( diff --git a/site/web/main.jsx b/site/web/main.jsx index 2ea5f040..9b9be49d 100644 --- a/site/web/main.jsx +++ b/site/web/main.jsx @@ -2,7 +2,7 @@ import React from 'react'; import ReactDOM from 'react-dom'; import App from './App'; -import '@common/style/mobile/index.less'; +// import '@common/style/mobile/index.less'; import 'tdesign-site-components'; import 'tdesign-site-components/lib/styles/style.css'; diff --git a/src/_util/renderTNode.ts b/src/_util/renderTNode.ts new file mode 100644 index 00000000..4d8f6094 --- /dev/null +++ b/src/_util/renderTNode.ts @@ -0,0 +1,22 @@ +import { ReactNode } from 'react'; +import { TNode } from '../common'; + +interface JSXRenderContext> { + defaultNode?: ReactNode; + params?: T; + wrap?: (node: ReactNode) => ReactNode; +} + +export const renderTNode = (node: TNode, options: JSXRenderContext): ReactNode => { + const wrap = options.wrap ?? ((node) => node); + if (typeof node === 'function') { + return wrap(node(options.params)); + } + if (node === true) { + return wrap(options.defaultNode); + } + if (!node) { + return null; + } + return wrap(node); +}; diff --git a/src/_util/renderToContainer.ts b/src/_util/renderToContainer.ts index 24876335..cacfca6e 100644 --- a/src/_util/renderToContainer.ts +++ b/src/_util/renderToContainer.ts @@ -1,7 +1,10 @@ import { createPortal } from 'react-dom'; import { ReactElement, ReactPortal } from 'react'; +import isFunction from 'lodash/isFunction'; +import isString from 'lodash/isString'; import { resolveContainer } from './getContainer'; import { canUseDom } from './canUseDom'; +import { AttachNode } from '../common'; export type GetContainer = HTMLElement | (() => HTMLElement) | null; @@ -12,3 +15,15 @@ export function renderToContainer(getContainer: GetContainer, node: ReactElement } return node; } + +export function getAttach(node: AttachNode): HTMLElement { + const attachNode = isFunction(node) ? node() : node; + + if (isString(attachNode)) { + return document.querySelector(attachNode); + } + if (attachNode instanceof HTMLElement) { + return attachNode; + } + return document.body; +} diff --git a/src/common.ts b/src/common.ts index 89c59d0e..cf41a595 100644 --- a/src/common.ts +++ b/src/common.ts @@ -5,7 +5,7 @@ import { ReactElement, ReactNode, CSSProperties, FormEvent, DragEvent, Synthetic // TElement 表示 API 只接受传入组件 export type TElement = T extends undefined ? ReactElement : (props: T) => ReactElement; // 1. TNode = ReactNode; 2. TNode = (props: T) => ReactNode -export type TNode = T extends undefined ? ReactNode : (props: T) => ReactNode; +export type TNode = T extends undefined ? ReactNode | (() => ReactNode) : ReactNode | ((props: T) => ReactNode); export type AttachNodeReturnValue = HTMLElement | Element | Document; export type AttachNode = CSSSelector | ((triggerNode?: HTMLElement) => AttachNodeReturnValue); diff --git a/src/overlay/Overlay.tsx b/src/overlay/Overlay.tsx index c45f916e..9aa20ee1 100644 --- a/src/overlay/Overlay.tsx +++ b/src/overlay/Overlay.tsx @@ -21,6 +21,7 @@ export interface OverlayProps extends NativeProps { afterClose?: () => void; stopPropagation?: PropagationEvent[]; children?: React.ReactNode; + duration?: number; } const opacityRecord = { @@ -62,6 +63,7 @@ const Overlay: FC = (props) => { tension: 200, friction: 30, clamp: true, + duration: props.duration, }, onStart: () => { setActive(true); diff --git a/src/popup/Popup.tsx b/src/popup/Popup.tsx index ec465cdc..10da1e94 100644 --- a/src/popup/Popup.tsx +++ b/src/popup/Popup.tsx @@ -1,18 +1,21 @@ import React, { FC, useState } from 'react'; import classnames from 'classnames'; import { useSpring, animated } from 'react-spring'; +import { CloseIcon } from 'tdesign-icons-react'; import Overlay from '../overlay'; import useDefault from '../_util/useDefault'; -import { PropagationEvent } from '../_util/withStopPropagation'; import withNativeProps, { NativeProps } from '../_util/withNativeProps'; import { TdPopupProps } from './type'; import useConfig from '../_util/useConfig'; import { popupDefaultProps } from './defaultProps'; +import { renderToContainer, getAttach } from '../_util/renderToContainer'; +import { renderTNode } from '../_util/renderTNode'; export interface PopupProps extends TdPopupProps, NativeProps {} enum PopupSourceEnum { OVERLAY = 'overlay', + CLOSEBTN = 'close-btn', } enum PlacementEnum { @@ -24,28 +27,66 @@ enum PlacementEnum { } const Popup: FC = (props) => { - const { children, placement, showOverlay, visible, defaultVisible, zIndex, overlayProps, onVisibleChange } = props; + const { + children, + placement, + showOverlay, + visible, + defaultVisible, + zIndex, + overlayProps, + preventScrollThrough, + attach, + destroyOnClose, + closeBtn, + closeOnOverlayClick, + onClose, + onClosed, + onOpen, + onOpened, + onVisibleChange, + } = props; const { classPrefix } = useConfig(); const name = `${classPrefix}-popup`; + const duration = 300; + const [show, setShow] = useDefault(visible, defaultVisible, onVisibleChange); const [active, setActive] = useState(show); - const handleOverlayClick = () => { + const handleOverlayClick = (e) => { + if (!closeOnOverlayClick) return; + onClose?.(e); setShow(false, PopupSourceEnum.OVERLAY); }; + const handleCloseClick = (e) => { + onClose?.(e); + setShow(false, PopupSourceEnum.CLOSEBTN); + }; + const { progress, opacity } = useSpring({ progress: show ? 0 : 100, opacity: show ? 1 : 0, + config: { + duration, + }, onStart: () => { + if (show) { + onOpen?.(); + } setActive(true); }, onRest: () => { setActive(show); + if (show) { + onOpened?.(); + } else { + onClosed?.(); + } }, }); @@ -63,36 +104,50 @@ const Popup: FC = (props) => { if (placement === PlacementEnum.RIGHT) { return `translateX(${p}%)`; } + if (placement === PlacementEnum.CENTER) { + return `scale(${1 - p / 100}) translate3d(-50%, -50%, 0)`; + } }), opacity: opacity.to((o) => { if (placement === PlacementEnum.CENTER) { return o; } }), - }; - - const rootStyles = { zIndex, - display: active ? 'block' : 'none', + display: active ? null : 'none', + transition: 'none', + transformOrigin: '0 0', }; - return withNativeProps( - props, -
- {showOverlay && ( - + const closeBtnNode = renderTNode(closeBtn, { + defaultNode: , + params: props, + wrap: (node) => ( +
+ {node} +
+ ), + }); + let node = ( + <> + + {withNativeProps( + props, + + {closeBtnNode} + {children} + , )} - - {active && children} - -
, + ); + node = attach ? renderToContainer(getAttach(attach), node) : node; + return (!destroyOnClose || active) && node; }; Popup.displayName = 'Popup'; diff --git a/src/popup/_example/base.jsx b/src/popup/_example/base.jsx index 101bc0b3..72e485ef 100644 --- a/src/popup/_example/base.jsx +++ b/src/popup/_example/base.jsx @@ -1,64 +1,29 @@ -import React, { useState } from 'react'; -import { Popup, Button } from 'tdesign-mobile-react'; +import React from 'react'; import TDemoBlock from '../../../site/mobile/components/DemoBlock'; import TDemoHeader from '../../../site/mobile/components/DemoHeader'; -import './style/index.less' +import './style/index.less'; +import PlacementTop from './placement-top'; +import PlacementLeft from './placement-left'; +import PlacementCenter from './placement-center'; +import PlacementBottom from './placement-bottom'; +import PlacementRight from './placement-right'; +import WithTitle from './with-title'; +import CustomClose from './custom-close'; export default function Base() { - - const [visible1, setVisible1] = useState(false) - const [visible2, setVisible2] = useState(false) - const [visible3, setVisible3] = useState(false) - const [visible4, setVisible4] = useState(false) - const [visible5, setVisible5] = useState(false) - - const handleVisible1Change = (visible) => { - setVisible1(visible); - } - const handleVisible2Change = (visible) => { - setVisible2(visible); - } - - const handleVisible3Change = (visible) => { - setVisible3(visible); - } - - const handleVisible4Change = (visible) => { - setVisible4(visible); - } - - const handleVisible5Change = (visible) => { - setVisible5(visible); - } - return ( -
- - -
-
- - - - - -
- -
-
- -
-
- -
-
- -
-
- -
-
-
+
+ + + + + + + + + + +
); diff --git a/src/popup/_example/custom-close.jsx b/src/popup/_example/custom-close.jsx new file mode 100644 index 00000000..b73a3e79 --- /dev/null +++ b/src/popup/_example/custom-close.jsx @@ -0,0 +1,35 @@ +import React, { useState } from 'react'; +import { CloseCircleIcon } from 'tdesign-icons-react'; +import { Popup, Button } from 'tdesign-mobile-react'; + +export default function Base() { + const [visible, setVisible] = useState(false); + + const handleVisibleChange = (visible) => { + setVisible(visible); + }; + + const onHide = () => setVisible(false); + + return ( + <> + + + + + + + ); +} diff --git a/src/popup/_example/placement-bottom.jsx b/src/popup/_example/placement-bottom.jsx index 27473acc..a8a57169 100644 --- a/src/popup/_example/placement-bottom.jsx +++ b/src/popup/_example/placement-bottom.jsx @@ -1,23 +1,25 @@ import React, { useState } from 'react'; import { Popup, Button } from 'tdesign-mobile-react'; -import './style/index.less' export default function Base() { - - const [visible, setVisible] = useState(false) + const [visible, setVisible] = useState(false); const handleVisibleChange = (visible) => { setVisible(visible); - } + }; return ( -
-
- -
- -
-
-
+ <> + + + + ); } diff --git a/src/popup/_example/placement-center.jsx b/src/popup/_example/placement-center.jsx index 085ec390..86755a73 100644 --- a/src/popup/_example/placement-center.jsx +++ b/src/popup/_example/placement-center.jsx @@ -1,23 +1,25 @@ import React, { useState } from 'react'; import { Popup, Button } from 'tdesign-mobile-react'; -import './style/index.less' export default function Base() { - - const [visible, setVisible] = useState(false) + const [visible, setVisible] = useState(false); const handleVisibleChange = (visible) => { setVisible(visible); - } + }; return ( -
-
- -
- -
-
-
+ <> + + + + ); } diff --git a/src/popup/_example/placement-left.jsx b/src/popup/_example/placement-left.jsx index 9b00d3d3..40eb1810 100644 --- a/src/popup/_example/placement-left.jsx +++ b/src/popup/_example/placement-left.jsx @@ -1,23 +1,25 @@ import React, { useState } from 'react'; import { Popup, Button } from 'tdesign-mobile-react'; -import './style/index.less' export default function Base() { - - const [visible, setVisible] = useState(false) + const [visible, setVisible] = useState(false); const handleVisibleChange = (visible) => { setVisible(visible); - } + }; return ( -
-
- -
- -
-
-
+ <> + + + + ); } diff --git a/src/popup/_example/placement-right.jsx b/src/popup/_example/placement-right.jsx index 182142f3..38da7b2b 100644 --- a/src/popup/_example/placement-right.jsx +++ b/src/popup/_example/placement-right.jsx @@ -1,23 +1,25 @@ import React, { useState } from 'react'; import { Popup, Button } from 'tdesign-mobile-react'; -import './style/index.less' export default function Base() { - - const [visible, setVisible] = useState(false) + const [visible, setVisible] = useState(false); const handleVisibleChange = (visible) => { setVisible(visible); - } + }; return ( -
-
- -
- -
-
-
+ <> + + + + ); } diff --git a/src/popup/_example/placement-top.jsx b/src/popup/_example/placement-top.jsx index efbf6b2e..d07a2c99 100644 --- a/src/popup/_example/placement-top.jsx +++ b/src/popup/_example/placement-top.jsx @@ -1,23 +1,25 @@ import React, { useState } from 'react'; import { Popup, Button } from 'tdesign-mobile-react'; -import './style/index.less' export default function Base() { - - const [visible, setVisible] = useState(false) + const [visible, setVisible] = useState(false); const handleVisibleChange = (visible) => { setVisible(visible); - } + }; return ( -
-
- -
- -
-
-
+ <> + + + + ); } diff --git a/src/popup/_example/style/index.less b/src/popup/_example/style/index.less index 2498b967..3991b236 100644 --- a/src/popup/_example/style/index.less +++ b/src/popup/_example/style/index.less @@ -1,35 +1,38 @@ -.tdesign-mobile-demo { - background-color: #ffffff; +.tdesign-mobile-popup-demo { + background-color: #ffffff; + flex: 1; + padding-bottom: 16px; - .vertical { - height: 240px; - background-color: #fff; - } - - .horizontal { - width: 307px; - height: 100%; - background-color: #fff; - } - - .center { - width: 88px; - height: 88px; - border-radius: 4px; - background-color: #fff; - } - - &__button-group { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; + button + button { + margin-top: 16px; + } +} + +.tdesign-mobile-popup-demo__with-title.header { + display: flex; + align-items: center; + .btn { + font-size: 16px; + padding: 16px; + &--cancel { + color: var(--td-text-color-secondary, rgba(0, 0, 0, 0.6)); } - - &__button { - width: 343px; - height: 44px; - border-radius: 4px; - margin-bottom: 16px; + &--confirm { + color: #0052d9; } + } + .title { + flex: 1; + text-align: center; + font-weight: 600; + font-size: 18px; + color: var(--td-text-color-primary, rgba(0, 0, 0, 0.9)); + } +} + +.design-mobile-popup-demo__custom-close.close-btn { + position: absolute; + left: 50%; + margin-left: -16px; + bottom: -56px; } diff --git a/src/popup/_example/with-title.jsx b/src/popup/_example/with-title.jsx new file mode 100644 index 00000000..5f5d38b2 --- /dev/null +++ b/src/popup/_example/with-title.jsx @@ -0,0 +1,32 @@ +import React, { useState } from 'react'; +import { Popup, Button } from 'tdesign-mobile-react'; + +export default function Base() { + const [visible, setVisible] = useState(false); + + const handleVisibleChange = (visible) => { + setVisible(visible); + }; + + const onHide = () => setVisible(false); + + return ( + <> + + + +
+
+ 取消 +
+
标题文字
+
+ 确定 +
+
+
+ + ); +} diff --git a/src/popup/defaultProps.ts b/src/popup/defaultProps.ts index 0203bd6e..41f3cc40 100644 --- a/src/popup/defaultProps.ts +++ b/src/popup/defaultProps.ts @@ -5,8 +5,11 @@ import { TdPopupProps } from './type'; export const popupDefaultProps: TdPopupProps = { + attach: 'body', + closeOnOverlayClick: true, + destroyOnClose: false, overlayProps: {}, placement: 'top', + preventScrollThrough: true, showOverlay: true, - visible: false, }; diff --git a/src/popup/popup.en-US.md b/src/popup/popup.en-US.md new file mode 100644 index 00000000..5e24d03d --- /dev/null +++ b/src/popup/popup.en-US.md @@ -0,0 +1,24 @@ +:: BASE_DOC :: + +## API + +### Popup Props + +name | type | default | description | required +-- | -- | -- | -- | -- +attach | String / Function | 'body' | Typescript:`AttachNode`。[see more ts definition](https://github.com/TDesignOteam/tdesign-mobile-react/blob/develop/src/common.ts) | N +children | TNode | - | Typescript:`string \| TNode`。[see more ts definition](https://github.com/TDesignOteam/tdesign-mobile-react/blob/develop/src/common.ts) | N +closeBtn | TNode | - | Typescript:`boolean \| TNode`。[see more ts definition](https://github.com/TDesignOteam/tdesign-mobile-react/blob/develop/src/common.ts) | N +closeOnOverlayClick | Boolean | true | \- | N +destroyOnClose | Boolean | false | \- | N +overlayProps | Object | {} | Typescript:`OverlayProps`,[Overlay API Documents](./overlay?tab=api)。[see more ts definition](https://github.com/TDesignOteam/tdesign-mobile-react/tree/develop/src/popup/type.ts) | N +placement | String | top | options: top/left/right/bottom/center | N +preventScrollThrough | Boolean | true | \- | N +showOverlay | Boolean | true | \- | N +visible | Boolean | - | Typescript:`boolean` | N +zIndex | Number | - | \- | N +onClose | Function | | Typescript:`(context: { e: MouseEvent }) => void`
| N +onClosed | Function | | Typescript:`() => void`
| N +onOpen | Function | | Typescript:`() => void`
| N +onOpened | Function | | Typescript:`() => void`
| N +onVisibleChange | Function | | Typescript:`(visible: boolean, trigger: PopupSource) => void`
[see more ts definition](https://github.com/TDesignOteam/tdesign-mobile-react/tree/develop/src/popup/type.ts)。
`type PopupSource = 'close-btn' \| 'overlay'`
| N diff --git a/src/popup/popup.md b/src/popup/popup.md index 9e75cdbb..0802f43c 100644 --- a/src/popup/popup.md +++ b/src/popup/popup.md @@ -1,17 +1,24 @@ :: BASE_DOC :: ## API + ### Popup Props -名称 | 类型 | 默认值 | 说明 | 必传 +名称 | 类型 | 默认值 | 描述 | 必传 -- | -- | -- | -- | -- -className | String | - | 类名 | N -style | Object | - | 样式,TS 类型:`React.CSSProperties` | N -children | TNode | - | 触发元素,同 triggerElement。TS 类型:`string | TNode`。[通用类型定义](https://github.com/Tencent/tdesign-mobile-react/blob/develop/src/common.ts) | N -overlayProps | Object | {} | 遮罩层的属性,透传至 overlay | N +attach | String / Function | 'body' | 制定挂载节点。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body。TS 类型:`AttachNode`。[通用类型定义](https://github.com/TDesignOteam/tdesign-mobile-react/blob/develop/src/common.ts) | N +children | TNode | - | 触发元素,同 triggerElement。TS 类型:`string \| TNode`。[通用类型定义](https://github.com/TDesignOteam/tdesign-mobile-react/blob/develop/src/common.ts) | N +closeBtn | TNode | - | 是否展示关闭按钮,值为 `true` 显示默认关闭按钮;值为 `false` 则不显示关闭按钮;也可以自定义关闭按钮。TS 类型:`boolean \| TNode`。[通用类型定义](https://github.com/TDesignOteam/tdesign-mobile-react/blob/develop/src/common.ts) | N +closeOnOverlayClick | Boolean | true | 点击遮罩层是否关闭 | N +destroyOnClose | Boolean | false | 是否在关闭浮层时销毁浮层 | N +overlayProps | Object | {} | 遮罩层的属性,透传至 overlay。TS 类型:`OverlayProps`,[Overlay API Documents](./overlay?tab=api)。[详细类型定义](https://github.com/TDesignOteam/tdesign-mobile-react/tree/develop/src/popup/type.ts) | N placement | String | top | 浮层出现位置。可选项:top/left/right/bottom/center | N +preventScrollThrough | Boolean | true | 是否阻止背景滚动 | N showOverlay | Boolean | true | 是否显示遮罩层 | N -visible | Boolean | false | 是否显示浮层。TS 类型:`boolean` | N -defaultVisible | Boolean | false | 是否显示浮层。非受控属性。TS 类型:`boolean` | N +visible | Boolean | - | 是否显示浮层。TS 类型:`boolean` | N zIndex | Number | - | 组件层级,Web 侧样式默认为 5500,移动端和小程序样式默认为 1500 | N -onVisibleChange | Function | | TS 类型:`(visible: boolean, trigger: PopupSource) => void`
当浮层隐藏或显示时触发。[详细类型定义](https://github.com/Tencent/tdesign-mobile-react/tree/develop/src/popup/type.ts)。
`type PopupSource = 'close-btn' | 'overlay'`
| N +onClose | Function | | TS 类型:`(context: { e: MouseEvent }) => void`
组件准备关闭时触发 | N +onClosed | Function | | TS 类型:`() => void`
组件关闭且动画结束后执行 | N +onOpen | Function | | TS 类型:`() => void`
组件准备展示时触发 | N +onOpened | Function | | TS 类型:`() => void`
组件展示且动画结束后执行 | N +onVisibleChange | Function | | TS 类型:`(visible: boolean, trigger: PopupSource) => void`
当浮层隐藏或显示时触发。[详细类型定义](https://github.com/TDesignOteam/tdesign-mobile-react/tree/develop/src/popup/type.ts)。
`type PopupSource = 'close-btn' \| 'overlay'`
| N diff --git a/src/popup/style/index.js b/src/popup/style/index.js index 52b4cca6..762249d7 100644 --- a/src/popup/style/index.js +++ b/src/popup/style/index.js @@ -1 +1 @@ -import '../../_common/style/mobile/components/popup/_index.less'; +import '../../_common/style/mobile/components/popup/v2/_index.less'; diff --git a/src/popup/type.ts b/src/popup/type.ts index ddb8c3c4..82298b21 100644 --- a/src/popup/type.ts +++ b/src/popup/type.ts @@ -4,15 +4,34 @@ * 该文件为脚本自动生成文件,请勿随意修改。如需修改请联系 PMC * */ -import { TNode } from '../common'; - import { OverlayProps } from '../overlay'; +import { TNode, AttachNode } from '../common'; +import { MouseEvent } from 'react'; export interface TdPopupProps { + /** + * 制定挂载节点。数据类型为 String 时,会被当作选择器处理,进行节点查询。示例:'body' 或 () => document.body + * @default 'body' + */ + attach?: AttachNode; /** * 触发元素,同 triggerElement */ children?: TNode; + /** + * 是否展示关闭按钮,值为 `true` 显示默认关闭按钮;值为 `false` 则不显示关闭按钮;也可以自定义关闭按钮 + */ + closeBtn?: TNode; + /** + * 点击遮罩层是否关闭 + * @default true + */ + closeOnOverlayClick?: boolean; + /** + * 是否在关闭浮层时销毁浮层 + * @default false + */ + destroyOnClose?: boolean; /** * 遮罩层的属性,透传至 overlay * @default {} @@ -23,6 +42,11 @@ export interface TdPopupProps { * @default top */ placement?: 'top' | 'left' | 'right' | 'bottom' | 'center'; + /** + * 是否阻止背景滚动 + * @default true + */ + preventScrollThrough?: boolean; /** * 是否显示遮罩层 * @default true @@ -30,18 +54,32 @@ export interface TdPopupProps { showOverlay?: boolean; /** * 是否显示浮层 - * @default false */ visible?: boolean; /** * 是否显示浮层,非受控属性 - * @default false */ defaultVisible?: boolean; /** * 组件层级,Web 侧样式默认为 5500,移动端和小程序样式默认为 1500 */ zIndex?: number; + /** + * 组件准备关闭时触发 + */ + onClose?: (context: { e: MouseEvent }) => void; + /** + * 组件关闭且动画结束后执行 + */ + onClosed?: () => void; + /** + * 组件准备展示时触发 + */ + onOpen?: () => void; + /** + * 组件展示且动画结束后执行 + */ + onOpened?: () => void; /** * 当浮层隐藏或显示时触发 */ From 5915aa0ebbffcbbe60ba922c5602707d36c1cce6 Mon Sep 17 00:00:00 2001 From: hkaikai <617760820@qq.com> Date: Fri, 9 Aug 2024 09:57:23 +0800 Subject: [PATCH 2/7] =?UTF-8?q?refactor(popup):=20demo=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/popup/_example/base.jsx | 124 ++++++++++++++++++++++-- src/popup/_example/placement-bottom.jsx | 25 ----- src/popup/_example/placement-center.jsx | 25 ----- src/popup/_example/placement-left.jsx | 25 ----- src/popup/_example/placement-right.jsx | 25 ----- src/popup/_example/placement-top.jsx | 25 ----- 6 files changed, 118 insertions(+), 131 deletions(-) delete mode 100644 src/popup/_example/placement-bottom.jsx delete mode 100644 src/popup/_example/placement-center.jsx delete mode 100644 src/popup/_example/placement-left.jsx delete mode 100644 src/popup/_example/placement-right.jsx delete mode 100644 src/popup/_example/placement-top.jsx diff --git a/src/popup/_example/base.jsx b/src/popup/_example/base.jsx index 72e485ef..a4191c09 100644 --- a/src/popup/_example/base.jsx +++ b/src/popup/_example/base.jsx @@ -1,14 +1,126 @@ -import React from 'react'; +import React, { useState } from 'react'; import TDemoBlock from '../../../site/mobile/components/DemoBlock'; import TDemoHeader from '../../../site/mobile/components/DemoHeader'; import './style/index.less'; -import PlacementTop from './placement-top'; -import PlacementLeft from './placement-left'; -import PlacementCenter from './placement-center'; -import PlacementBottom from './placement-bottom'; -import PlacementRight from './placement-right'; import WithTitle from './with-title'; import CustomClose from './custom-close'; +import Button from '../../button'; +import Popup from '..'; + +function PlacementBottom() { + const [visible, setVisible] = useState(false); + + const handleVisibleChange = (visible) => { + setVisible(visible); + }; + + return ( + <> + + + + + ); +} + +function PlacementTop() { + const [visible, setVisible] = useState(false); + + const handleVisibleChange = (visible) => { + setVisible(visible); + }; + + return ( + <> + + + + + ); +} + +function PlacementLeft() { + const [visible, setVisible] = useState(false); + + const handleVisibleChange = (visible) => { + setVisible(visible); + }; + + return ( + <> + + + + + ); +} + +function PlacementRight() { + const [visible, setVisible] = useState(false); + + const handleVisibleChange = (visible) => { + setVisible(visible); + }; + + return ( + <> + + + + + ); +} + +function PlacementCenter() { + const [visible, setVisible] = useState(false); + + const handleVisibleChange = (visible) => { + setVisible(visible); + }; + + return ( + <> + + + + + ); +} export default function Base() { return ( diff --git a/src/popup/_example/placement-bottom.jsx b/src/popup/_example/placement-bottom.jsx deleted file mode 100644 index a8a57169..00000000 --- a/src/popup/_example/placement-bottom.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { useState } from 'react'; -import { Popup, Button } from 'tdesign-mobile-react'; - -export default function Base() { - const [visible, setVisible] = useState(false); - - const handleVisibleChange = (visible) => { - setVisible(visible); - }; - - return ( - <> - - - - - ); -} diff --git a/src/popup/_example/placement-center.jsx b/src/popup/_example/placement-center.jsx deleted file mode 100644 index 86755a73..00000000 --- a/src/popup/_example/placement-center.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { useState } from 'react'; -import { Popup, Button } from 'tdesign-mobile-react'; - -export default function Base() { - const [visible, setVisible] = useState(false); - - const handleVisibleChange = (visible) => { - setVisible(visible); - }; - - return ( - <> - - - - - ); -} diff --git a/src/popup/_example/placement-left.jsx b/src/popup/_example/placement-left.jsx deleted file mode 100644 index 40eb1810..00000000 --- a/src/popup/_example/placement-left.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { useState } from 'react'; -import { Popup, Button } from 'tdesign-mobile-react'; - -export default function Base() { - const [visible, setVisible] = useState(false); - - const handleVisibleChange = (visible) => { - setVisible(visible); - }; - - return ( - <> - - - - - ); -} diff --git a/src/popup/_example/placement-right.jsx b/src/popup/_example/placement-right.jsx deleted file mode 100644 index 38da7b2b..00000000 --- a/src/popup/_example/placement-right.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { useState } from 'react'; -import { Popup, Button } from 'tdesign-mobile-react'; - -export default function Base() { - const [visible, setVisible] = useState(false); - - const handleVisibleChange = (visible) => { - setVisible(visible); - }; - - return ( - <> - - - - - ); -} diff --git a/src/popup/_example/placement-top.jsx b/src/popup/_example/placement-top.jsx deleted file mode 100644 index d07a2c99..00000000 --- a/src/popup/_example/placement-top.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import React, { useState } from 'react'; -import { Popup, Button } from 'tdesign-mobile-react'; - -export default function Base() { - const [visible, setVisible] = useState(false); - - const handleVisibleChange = (visible) => { - setVisible(visible); - }; - - return ( - <> - - - - - ); -} From 93b394e8d7d5332068b7ade630c6b86045dfc8bb Mon Sep 17 00:00:00 2001 From: hkaikai <617760820@qq.com> Date: Fri, 9 Aug 2024 10:19:10 +0800 Subject: [PATCH 3/7] =?UTF-8?q?refactor(popup):=20useDefaultProps=20?= =?UTF-8?q?=E4=BD=BF=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/popup/Popup.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/popup/Popup.tsx b/src/popup/Popup.tsx index 10da1e94..99410313 100644 --- a/src/popup/Popup.tsx +++ b/src/popup/Popup.tsx @@ -8,6 +8,7 @@ import withNativeProps, { NativeProps } from '../_util/withNativeProps'; import { TdPopupProps } from './type'; import useConfig from '../_util/useConfig'; import { popupDefaultProps } from './defaultProps'; +import useDefaultProps from '../hooks/useDefaultProps'; import { renderToContainer, getAttach } from '../_util/renderToContainer'; import { renderTNode } from '../_util/renderTNode'; @@ -45,7 +46,7 @@ const Popup: FC = (props) => { onOpen, onOpened, onVisibleChange, - } = props; + } = useDefaultProps(props, popupDefaultProps); const { classPrefix } = useConfig(); @@ -151,6 +152,5 @@ const Popup: FC = (props) => { }; Popup.displayName = 'Popup'; -Popup.defaultProps = popupDefaultProps; export default Popup; From 217e1332cce31035964a35f65e569c52dd15d280 Mon Sep 17 00:00:00 2001 From: hkaikai <617760820@qq.com> Date: Fri, 9 Aug 2024 11:10:06 +0800 Subject: [PATCH 4/7] =?UTF-8?q?fix(popup):=20=E4=BF=AE=E5=A4=8Dbutton?= =?UTF-8?q?=E7=BC=96=E8=AF=91=E6=8A=A5=E9=94=99=E9=97=AE=E9=A2=98,popup=20?= =?UTF-8?q?demo=E6=94=B9tsx?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- site/mobile/mobile.config.js | 4 +- src/button/Button.tsx | 4 +- src/popup/_example/base.tsx | 48 +++++++++++++++++++ .../{custom-close.jsx => custom-close.tsx} | 2 +- src/popup/_example/{base.jsx => index.tsx} | 2 +- .../{with-title.jsx => with-title.tsx} | 2 +- 6 files changed, 56 insertions(+), 6 deletions(-) create mode 100644 src/popup/_example/base.tsx rename src/popup/_example/{custom-close.jsx => custom-close.tsx} (95%) rename src/popup/_example/{base.jsx => index.tsx} (99%) rename src/popup/_example/{with-title.jsx => with-title.tsx} (96%) diff --git a/site/mobile/mobile.config.js b/site/mobile/mobile.config.js index ecf91279..055c745b 100644 --- a/site/mobile/mobile.config.js +++ b/site/mobile/mobile.config.js @@ -38,7 +38,7 @@ export default { { title: 'Popup 弹出层', name: 'popup', - component: () => import('tdesign-mobile-react/popup/_example/base.jsx'), + component: () => import('tdesign-mobile-react/popup/_example/index.tsx'), }, { title: 'Progress 进度条', @@ -216,6 +216,6 @@ export default { title: 'Result 结果', name: 'result', component: () => import('tdesign-mobile-react/result/_example/index.tsx'), - } + }, ], }; diff --git a/src/button/Button.tsx b/src/button/Button.tsx index d3e602ca..5430dbd5 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -6,7 +6,9 @@ import { TdButtonProps } from './type'; import noop from '../_util/noop'; import { buttonDefaultProps } from './defaultProps'; -export interface ButtonProps extends TdButtonProps, Omit, 'content'> {} +export interface ButtonProps + extends TdButtonProps, + Omit, 'content' | 'children'> {} const Button = forwardRef((props: ButtonProps, ref: React.Ref) => { const { diff --git a/src/popup/_example/base.tsx b/src/popup/_example/base.tsx new file mode 100644 index 00000000..a4219680 --- /dev/null +++ b/src/popup/_example/base.tsx @@ -0,0 +1,48 @@ +import React, { useState } from 'react'; +import Button from '../../button'; +import Popup from '..'; + +const placementList = [ + { value: 'top', text: '顶部弹出' }, + { value: 'left', text: '左侧弹出' }, + { value: 'center', text: '中间弹出' }, + { value: 'bottom', text: '底部弹出' }, + { value: 'right', text: '右侧弹出' }, +] as const; + +type PlacementValue = (typeof placementList)[number]['value']; + +export default function Base() { + const [visible, setVisible] = useState(false); + const [placement, setPlacement] = useState('top'); + const onClick = (value) => { + setPlacement(value); + setVisible(true); + }; + const handleVisibleChange = (visible) => { + setVisible(visible); + }; + return ( +
+ {placementList.map((item) => ( + + ))} + +
+ ); +} diff --git a/src/popup/_example/custom-close.jsx b/src/popup/_example/custom-close.tsx similarity index 95% rename from src/popup/_example/custom-close.jsx rename to src/popup/_example/custom-close.tsx index b73a3e79..67bbd65f 100644 --- a/src/popup/_example/custom-close.jsx +++ b/src/popup/_example/custom-close.tsx @@ -2,7 +2,7 @@ import React, { useState } from 'react'; import { CloseCircleIcon } from 'tdesign-icons-react'; import { Popup, Button } from 'tdesign-mobile-react'; -export default function Base() { +export default function CustomClose() { const [visible, setVisible] = useState(false); const handleVisibleChange = (visible) => { diff --git a/src/popup/_example/base.jsx b/src/popup/_example/index.tsx similarity index 99% rename from src/popup/_example/base.jsx rename to src/popup/_example/index.tsx index a4191c09..3602cc11 100644 --- a/src/popup/_example/base.jsx +++ b/src/popup/_example/index.tsx @@ -122,7 +122,7 @@ function PlacementCenter() { ); } -export default function Base() { +export default function Index() { return (
diff --git a/src/popup/_example/with-title.jsx b/src/popup/_example/with-title.tsx similarity index 96% rename from src/popup/_example/with-title.jsx rename to src/popup/_example/with-title.tsx index 5f5d38b2..c4c2749d 100644 --- a/src/popup/_example/with-title.jsx +++ b/src/popup/_example/with-title.tsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import { Popup, Button } from 'tdesign-mobile-react'; -export default function Base() { +export default function WithTitle() { const [visible, setVisible] = useState(false); const handleVisibleChange = (visible) => { From d22c7e29ba7dbe907fd61038acc4e1a695de2fda Mon Sep 17 00:00:00 2001 From: hkaikai <617760820@qq.com> Date: Mon, 19 Aug 2024 18:32:43 +0800 Subject: [PATCH 5/7] =?UTF-8?q?refactor(popup):=20=E6=9B=B4=E6=96=B0overla?= =?UTF-8?q?y=20=E4=BD=BF=E7=94=A8;=E5=8A=A8=E7=94=BB=E6=94=B9=E7=94=A8=20C?= =?UTF-8?q?SSTransition?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/common.ts | 2 +- src/popup/Popup.tsx | 104 +++++++++++++++----------------------------- 2 files changed, 35 insertions(+), 71 deletions(-) diff --git a/src/common.ts b/src/common.ts index cf41a595..89c59d0e 100644 --- a/src/common.ts +++ b/src/common.ts @@ -5,7 +5,7 @@ import { ReactElement, ReactNode, CSSProperties, FormEvent, DragEvent, Synthetic // TElement 表示 API 只接受传入组件 export type TElement = T extends undefined ? ReactElement : (props: T) => ReactElement; // 1. TNode = ReactNode; 2. TNode = (props: T) => ReactNode -export type TNode = T extends undefined ? ReactNode | (() => ReactNode) : ReactNode | ((props: T) => ReactNode); +export type TNode = T extends undefined ? ReactNode : (props: T) => ReactNode; export type AttachNodeReturnValue = HTMLElement | Element | Document; export type AttachNode = CSSSelector | ((triggerNode?: HTMLElement) => AttachNodeReturnValue); diff --git a/src/popup/Popup.tsx b/src/popup/Popup.tsx index 6fdc2f43..62650e46 100644 --- a/src/popup/Popup.tsx +++ b/src/popup/Popup.tsx @@ -1,7 +1,8 @@ -import React, { forwardRef, useRef } from 'react'; +import React, { forwardRef, useMemo, useRef, useState } from 'react'; import classnames from 'classnames'; import { CloseIcon } from 'tdesign-icons-react'; import { CSSTransition } from 'react-transition-group'; +import { CSSTransitionClassNames } from 'react-transition-group/CSSTransition'; import Overlay from '../overlay'; import useDefault from '../_util/useDefault'; import withNativeProps, { NativeProps } from '../_util/withNativeProps'; @@ -19,14 +20,6 @@ enum PopupSourceEnum { CLOSEBTN = 'close-btn', } -// enum PlacementEnum { -// TOP = 'top', -// BOTTOM = 'bottom', -// LEFT = 'left', -// RIGHT = 'right', -// CENTER = 'center', -// } - const Popup = forwardRef((props) => { const { children, @@ -42,9 +35,9 @@ const Popup = forwardRef((props) => { closeBtn, closeOnOverlayClick, onClose, - // onClosed, - // onOpen, - // onOpened, + onClosed, + onOpen, + onOpened, onVisibleChange, } = useDefaultProps(props, popupDefaultProps); @@ -56,7 +49,7 @@ const Popup = forwardRef((props) => { const [show, setShow] = useDefault(visible, defaultVisible, onVisibleChange); - // const [active, setActive] = useState(show); + const [active, setActive] = useState(show); const handleOverlayClick = (e) => { if (!closeOnOverlayClick) return; @@ -69,56 +62,22 @@ const Popup = forwardRef((props) => { setShow(false, PopupSourceEnum.CLOSEBTN); }; - // const { progress, opacity } = useSpring({ - // progress: show ? 0 : 100, - // opacity: show ? 1 : 0, - // config: { - // duration, - // }, - // onStart: () => { - // if (show) { - // onOpen?.(); - // } - // setActive(true); - // }, - // onRest: () => { - // setActive(show); - // if (show) { - // onOpened?.(); - // } else { - // onClosed?.(); - // } - // }, - // }); - - const contentStyle = { - // transform: progress.to((p) => { - // if (placement === PlacementEnum.BOTTOM) { - // return `translateY(${p}%)`; - // } - // if (placement === PlacementEnum.TOP) { - // return `translateY(-${p}%)`; - // } - // if (placement === PlacementEnum.LEFT) { - // return `translateX(-${p}%)`; - // } - // if (placement === PlacementEnum.RIGHT) { - // return `translateX(${p}%)`; - // } - // if (placement === PlacementEnum.CENTER) { - // return `scale(${1 - p / 100}) translate3d(-50%, -50%, 0)`; - // } - // }), - // opacity: opacity.to((o) => { - // if (placement === PlacementEnum.CENTER) { - // return o; - // } - // }), - zIndex, - // display: active ? null : 'none', - // transition: 'none', - // transformOrigin: '0 0', - }; + const contentStyle = useMemo( + () => ({ + zIndex, + display: active ? null : 'none', + animationFillMode: 'forwards', + }), + [zIndex, active], + ); + + const classNames = useMemo( + () => ({ + enterActive: placement === 'center' ? 'fade-zoom-enter-active' : `slide-${placement}-enter-active`, + exitActive: placement === 'center' ? 'fade-zoom-leave-active' : `slide-${placement}-leave-active`, + }), + [placement], + ); const closeBtnNode = !closeBtn ? null : (
@@ -137,15 +96,20 @@ const Popup = forwardRef((props) => { /> { + onOpen?.(); + setActive(true); + }} + onEntered={() => { + onOpened?.(); + }} + onExited={() => { + onClosed?.(); + setActive(false); }} > {withNativeProps( From 32815f5ebebfd769803bf18dc4f6dcadcb96310b Mon Sep 17 00:00:00 2001 From: hkaikai <617760820@qq.com> Date: Tue, 20 Aug 2024 16:11:40 +0800 Subject: [PATCH 6/7] =?UTF-8?q?refactor(popup):=20portal=20=E5=BA=94?= =?UTF-8?q?=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/_util/renderToContainer.ts | 15 --------------- src/button/Button.tsx | 4 +--- src/popup/Popup.tsx | 4 ++-- 3 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/_util/renderToContainer.ts b/src/_util/renderToContainer.ts index cacfca6e..24876335 100644 --- a/src/_util/renderToContainer.ts +++ b/src/_util/renderToContainer.ts @@ -1,10 +1,7 @@ import { createPortal } from 'react-dom'; import { ReactElement, ReactPortal } from 'react'; -import isFunction from 'lodash/isFunction'; -import isString from 'lodash/isString'; import { resolveContainer } from './getContainer'; import { canUseDom } from './canUseDom'; -import { AttachNode } from '../common'; export type GetContainer = HTMLElement | (() => HTMLElement) | null; @@ -15,15 +12,3 @@ export function renderToContainer(getContainer: GetContainer, node: ReactElement } return node; } - -export function getAttach(node: AttachNode): HTMLElement { - const attachNode = isFunction(node) ? node() : node; - - if (isString(attachNode)) { - return document.querySelector(attachNode); - } - if (attachNode instanceof HTMLElement) { - return attachNode; - } - return document.body; -} diff --git a/src/button/Button.tsx b/src/button/Button.tsx index 5430dbd5..d3e602ca 100644 --- a/src/button/Button.tsx +++ b/src/button/Button.tsx @@ -6,9 +6,7 @@ import { TdButtonProps } from './type'; import noop from '../_util/noop'; import { buttonDefaultProps } from './defaultProps'; -export interface ButtonProps - extends TdButtonProps, - Omit, 'content' | 'children'> {} +export interface ButtonProps extends TdButtonProps, Omit, 'content'> {} const Button = forwardRef((props: ButtonProps, ref: React.Ref) => { const { diff --git a/src/popup/Popup.tsx b/src/popup/Popup.tsx index 62650e46..aba1bbe3 100644 --- a/src/popup/Popup.tsx +++ b/src/popup/Popup.tsx @@ -9,9 +9,9 @@ import withNativeProps, { NativeProps } from '../_util/withNativeProps'; import { TdPopupProps } from './type'; import { popupDefaultProps } from './defaultProps'; import useDefaultProps from '../hooks/useDefaultProps'; -import { renderToContainer, getAttach } from '../_util/renderToContainer'; import parseTNode from '../_util/parseTNode'; import { usePrefixClass } from '../hooks/useClass'; +import Portal from '../common/Portal'; export interface PopupProps extends TdPopupProps, NativeProps {} @@ -122,7 +122,7 @@ const Popup = forwardRef((props) => { ); - return attach ? renderToContainer(getAttach(attach), node) : node; + return {node}; }); Popup.displayName = 'Popup'; From ea9853f8f7a49ab236034064ffe76c297db912c3 Mon Sep 17 00:00:00 2001 From: anlyyao Date: Wed, 21 Aug 2024 14:49:12 +0800 Subject: [PATCH 7/7] test: update csr and ssr snap --- src/_common | 2 +- test/snap/__snapshots__/csr.test.jsx.snap | 363 ++++++++++++++++++++++ test/snap/__snapshots__/ssr.test.jsx.snap | 8 + 3 files changed, 372 insertions(+), 1 deletion(-) diff --git a/src/_common b/src/_common index cfcad47f..41d40c78 160000 --- a/src/_common +++ b/src/_common @@ -1 +1 @@ -Subproject commit cfcad47f9c27eaf31ad50de07347f8c07685a090 +Subproject commit 41d40c788e2793b7707ee1ca0719d3d66bba838d diff --git a/test/snap/__snapshots__/csr.test.jsx.snap b/test/snap/__snapshots__/csr.test.jsx.snap index fe16d4e4..8131c420 100644 --- a/test/snap/__snapshots__/csr.test.jsx.snap +++ b/test/snap/__snapshots__/csr.test.jsx.snap @@ -8826,6 +8826,361 @@ exports[`csr snapshot test > csr test src/overlay/_example/index.tsx 1`] = `
`; +exports[`csr snapshot test > csr test src/popup/_example/base.tsx 1`] = ` +
+
+ + + + + + +
+`; + +exports[`csr snapshot test > csr test src/popup/_example/custom-close.tsx 1`] = ` +
+ + +
+`; + +exports[`csr snapshot test > csr test src/popup/_example/index.tsx 1`] = ` +
+
+
+

+ Popup 弹窗层 +

+

+ 由其他控件触发,屏幕滑出或弹出一块自定义内容区域 +

+
+
+
+

+ 01 组件类型 +

+

+ 基础弹出层 +

+
+
+ +