From 26fbcb264aba2e51761ab4edc4b8b6ef57796a13 Mon Sep 17 00:00:00 2001 From: zhouyun1 Date: Fri, 24 Nov 2023 17:32:47 +0800 Subject: [PATCH 1/7] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E4=BB=A5API?= =?UTF-8?q?=E7=9A=84=E6=96=B9=E5=BC=8F=E8=B0=83=E7=94=A8=E6=B0=94=E6=B3=A1?= =?UTF-8?q?=E5=8D=A1=E7=89=87=20(#2677)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/ui/popover/src/Popover.tsx | 27 +++++++-- packages/ui/popover/src/index.ts | 7 ++- packages/ui/popover/src/use-popover.tsx | 7 ++- packages/ui/popover/src/with-api.tsx | 60 +++++++++++++++++++ packages/ui/popover/stories/index.stories.tsx | 1 + .../ui/popover/stories/with-api.stories.tsx | 45 ++++++++++++++ packages/ui/popper/src/index.ts | 1 + 7 files changed, 140 insertions(+), 8 deletions(-) create mode 100644 packages/ui/popover/src/with-api.tsx create mode 100644 packages/ui/popover/stories/with-api.stories.tsx diff --git a/packages/ui/popover/src/Popover.tsx b/packages/ui/popover/src/Popover.tsx index 0631b4716..b420a80df 100644 --- a/packages/ui/popover/src/Popover.tsx +++ b/packages/ui/popover/src/Popover.tsx @@ -1,4 +1,10 @@ -import React, { cloneElement, isValidElement, forwardRef, useMemo } from 'react' +import React, { + cloneElement, + isValidElement, + forwardRef, + useMemo, + useImperativeHandle, +} from 'react' import { cx, getPrefixCls } from '@hi-ui/classname' import { __DEV__, invariant } from '@hi-ui/env' import { HiBaseHTMLProps } from '@hi-ui/core' @@ -7,7 +13,7 @@ import { usePopover, UsePopoverProps } from './use-popover' import { isString } from '@hi-ui/type-assertion' const _role = 'popover' -const _prefix = getPrefixCls(_role) +export const prefix = getPrefixCls(_role) /** * 气泡卡片 @@ -15,7 +21,8 @@ const _prefix = getPrefixCls(_role) export const Popover = forwardRef( ( { - prefixCls = _prefix, + prefixCls = prefix, + innerRef, className, children, title, @@ -28,6 +35,13 @@ export const Popover = forwardRef( ref ) => { const { rootProps, getTriggerProps, getPopperProps, getOverlayProps } = usePopover(rest) + const popperProps = getPopperProps() + console.log('popperProps', popperProps) + + useImperativeHandle(innerRef, () => ({ + open: () => popperProps.onOpen(), + close: () => popperProps.onClose(), + })) const triggerMemo = useMemo(() => { let trigger: React.ReactElement | null | undefined @@ -58,6 +72,7 @@ export const Popover = forwardRef( } } } + console.log('trigger', trigger) return trigger }, [children, getTriggerProps, autoWrapChildren, shouldWrapChildren, wrapTagName]) @@ -66,8 +81,8 @@ export const Popover = forwardRef( return ( <> - {triggerMemo} - + {/* {triggerMemo} */} +
{title ?
{title}
: null}
{content}
@@ -79,6 +94,7 @@ export const Popover = forwardRef( ) export interface PopoverProps extends HiBaseHTMLProps<'div'>, UsePopoverProps { + innerRef?: React.Ref<{ open: () => void; close: () => void }> /** * 气泡卡片标题 */ @@ -99,6 +115,7 @@ export interface PopoverProps extends HiBaseHTMLProps<'div'>, UsePopoverProps { * 指定包裹 children 的标签 */ wrapTagName?: React.ElementType + attachEl?: HTMLElement } if (__DEV__) { diff --git a/packages/ui/popover/src/index.ts b/packages/ui/popover/src/index.ts index 9fdde71b3..029580b6e 100644 --- a/packages/ui/popover/src/index.ts +++ b/packages/ui/popover/src/index.ts @@ -1,6 +1,11 @@ import './styles/index.scss' +import { Popover as PurePopover } from './Popover' +import { withModal } from './with-api' + export * from './Popover' -export { Popover as default } from './Popover' + +export const Popover = withModal(PurePopover) +export default Popover export * from './types' diff --git a/packages/ui/popover/src/use-popover.tsx b/packages/ui/popover/src/use-popover.tsx index af03b17c3..b88652b9d 100644 --- a/packages/ui/popover/src/use-popover.tsx +++ b/packages/ui/popover/src/use-popover.tsx @@ -18,6 +18,7 @@ export const usePopover = ({ trigger: triggerProp = 'click', mouseEnterDelay = 100, mouseLeaveDelay = 100, + attachEl, ...restProps }: UsePopoverProps) => { // TODO: 移除 popper,使用 hook 重写 @@ -148,10 +149,11 @@ export const usePopover = ({ return { ...popperProps, visible, - attachEl: triggerEl, + attachEl: attachEl ?? triggerEl, + onOpen: visibleAction.on, onClose: visibleAction.off, } - }, [visible, popper, visibleAction, triggerEl]) + }, [popper, visible, attachEl, triggerEl, visibleAction.off, visibleAction.on]) return { rootProps: rest, getOverlayProps, getTriggerProps, getPopperProps } } @@ -185,6 +187,7 @@ export interface UsePopoverProps extends PopperOverlayProps { * 设置基于 reference 元素的间隙偏移量 */ gutterGap?: number + attachEl?: HTMLElement } export type UsePopoverReturn = ReturnType diff --git a/packages/ui/popover/src/with-api.tsx b/packages/ui/popover/src/with-api.tsx new file mode 100644 index 000000000..0bcb03abc --- /dev/null +++ b/packages/ui/popover/src/with-api.tsx @@ -0,0 +1,60 @@ +import React, { createRef, createElement } from 'react' +import { render, unmountComponentAtNode } from 'react-dom' +import * as Container from '@hi-ui/container' +import { uuid } from '@hi-ui/use-id' + +import { prefix as popoverPrefix, Popover, PopoverProps } from './Popover' + +const prefixCls = popoverPrefix +const selector = `.${prefixCls}-wrapper` +const toastManagerRef = createRef() + +const open = (target: HTMLElement, { onOpen, onClose, ...rest }: PopoverApiProps) => { + const selectorId = `${selector}__${uuid()}` + let container: any = Container.getContainer(selectorId) + console.log('target', target) + + const ClonedPopover = createElement(Popover, { + ...rest, + // visible: true, + innerRef: toastManagerRef, + container, + attachEl: target, + closeOnOutsideClick: false, + shouldWrapChildren: true, + // children: attachEl, + onOpen: () => { + onOpen?.() + }, + onClose: () => { + console.log('close', toastManagerRef.current) + + onClose?.() + + setTimeout(() => { + if (container) { + unmountComponentAtNode(container as Element) + container = null + } + Container.removeContainer(selector) + }, 300) + }, + }) + + requestAnimationFrame(() => { + render(ClonedPopover, container) + toastManagerRef.current.open() + }) +} + +const close = () => { + console.log('close.') + + toastManagerRef.current.close() +} + +export interface PopoverApiProps extends PopoverProps {} + +export function withModal(instance: typeof Popover) { + return Object.assign(instance, { open, close }) +} diff --git a/packages/ui/popover/stories/index.stories.tsx b/packages/ui/popover/stories/index.stories.tsx index a243b6a80..61d86f768 100644 --- a/packages/ui/popover/stories/index.stories.tsx +++ b/packages/ui/popover/stories/index.stories.tsx @@ -7,6 +7,7 @@ export * from './trigger.stories' export * from './placement.stories' export * from './controlled.stories' export * from './gutter-gap.stories' +export * from './with-api.stories' export default { title: 'Data Display/Popover', diff --git a/packages/ui/popover/stories/with-api.stories.tsx b/packages/ui/popover/stories/with-api.stories.tsx new file mode 100644 index 000000000..48ae956d8 --- /dev/null +++ b/packages/ui/popover/stories/with-api.stories.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import Popover from '../src' +import Button from '@hi-ui/button' + +/** + * @title API 用法 + * @desc 用于列表中的每一项 + */ +export const WithApi = () => { + const title = 文字提示 + const content = ( + <> +
+
此处展示 Popover 具体内容
+
具体内容可以自行渲染
+
+
+ +
+ + ) + + const triggerRef = React.useRef(null) + + return ( + <> +

WithApi

+
+ , + }) + } + > + open + + {/* + + */} +
+ + ) +} diff --git a/packages/ui/popper/src/index.ts b/packages/ui/popper/src/index.ts index 287536eef..dfd878e4d 100644 --- a/packages/ui/popper/src/index.ts +++ b/packages/ui/popper/src/index.ts @@ -18,6 +18,7 @@ export type PopperOverlayProps = Pick< | 'disabledPortal' | 'arrow' | 'onOutsideClick' + | 'closeOnOutsideClick' > const omitProps = [ From ba6e0265da07964423d3a684dd068dcf50865dbc Mon Sep 17 00:00:00 2001 From: zhouyun1 Date: Wed, 29 Nov 2023 20:37:07 +0800 Subject: [PATCH 2/7] feat(popover&popper&container): #2667 --- .changeset/afraid-socks-mix.md | 5 + .changeset/clean-kids-march.md | 5 + .changeset/late-cars-walk.md | 5 + .changeset/smooth-waves-sin.md | 5 + packages/ui/popover/src/Popover.tsx | 29 ++-- packages/ui/popover/src/index.ts | 4 +- packages/ui/popover/src/styles/popover.scss | 13 +- packages/ui/popover/src/use-popover.tsx | 8 +- packages/ui/popover/src/with-api.tsx | 70 +++++---- .../ui/popover/stories/content.stories.tsx | 102 +++++++++++++ packages/ui/popover/stories/index.stories.tsx | 1 + .../ui/popover/stories/with-api.stories.tsx | 134 ++++++++++++++---- packages/utils/container/src/index.ts | 7 +- 13 files changed, 318 insertions(+), 70 deletions(-) create mode 100644 .changeset/afraid-socks-mix.md create mode 100644 .changeset/clean-kids-march.md create mode 100644 .changeset/late-cars-walk.md create mode 100644 .changeset/smooth-waves-sin.md create mode 100644 packages/ui/popover/stories/content.stories.tsx diff --git a/.changeset/afraid-socks-mix.md b/.changeset/afraid-socks-mix.md new file mode 100644 index 000000000..390e02022 --- /dev/null +++ b/.changeset/afraid-socks-mix.md @@ -0,0 +1,5 @@ +--- +"@hi-ui/hiui": patch +--- + +feat(popover&popper&container): 1.气泡卡片支持 API 的方式调用;2.气泡卡片增加分割线标题 diff --git a/.changeset/clean-kids-march.md b/.changeset/clean-kids-march.md new file mode 100644 index 000000000..20ac2390f --- /dev/null +++ b/.changeset/clean-kids-march.md @@ -0,0 +1,5 @@ +--- +"@hi-ui/container": minor +--- + +feat: getContainer 方法增加 customWrapper 参数,用于自定义容器节点 diff --git a/.changeset/late-cars-walk.md b/.changeset/late-cars-walk.md new file mode 100644 index 000000000..152e161f6 --- /dev/null +++ b/.changeset/late-cars-walk.md @@ -0,0 +1,5 @@ +--- +"@hi-ui/popper": patch +--- + +chore: PopperOverlayProps 类型增加 closeOnOutsideClick diff --git a/.changeset/smooth-waves-sin.md b/.changeset/smooth-waves-sin.md new file mode 100644 index 000000000..6a7c63918 --- /dev/null +++ b/.changeset/smooth-waves-sin.md @@ -0,0 +1,5 @@ +--- +"@hi-ui/popover": minor +--- + +feat: 1.增加 showTitleDivider api,设置后会是另外一种更紧凑的具有分割线的标题布局;2.增加以 API 的方式调用组件的能力 diff --git a/packages/ui/popover/src/Popover.tsx b/packages/ui/popover/src/Popover.tsx index b420a80df..bd5c2d15d 100644 --- a/packages/ui/popover/src/Popover.tsx +++ b/packages/ui/popover/src/Popover.tsx @@ -30,17 +30,22 @@ export const Popover = forwardRef( shouldWrapChildren = false, autoWrapChildren = true, wrapTagName = 'span', + showTitleDivider = false, ...rest }, ref ) => { - const { rootProps, getTriggerProps, getPopperProps, getOverlayProps } = usePopover(rest) - const popperProps = getPopperProps() - console.log('popperProps', popperProps) + const { + rootProps, + getTriggerProps, + getPopperProps, + getOverlayProps, + visibleAction, + } = usePopover(rest) useImperativeHandle(innerRef, () => ({ - open: () => popperProps.onOpen(), - close: () => popperProps.onClose(), + open: visibleAction.on, + close: visibleAction.off, })) const triggerMemo = useMemo(() => { @@ -72,17 +77,16 @@ export const Popover = forwardRef( } } } - console.log('trigger', trigger) return trigger }, [children, getTriggerProps, autoWrapChildren, shouldWrapChildren, wrapTagName]) - const cls = cx(prefixCls, className) + const cls = cx(prefixCls, showTitleDivider && `${prefixCls}--divided`, className) return ( <> - {/* {triggerMemo} */} - + {triggerMemo} +
{title ?
{title}
: null}
{content}
@@ -115,7 +119,14 @@ export interface PopoverProps extends HiBaseHTMLProps<'div'>, UsePopoverProps { * 指定包裹 children 的标签 */ wrapTagName?: React.ElementType + /** + * 吸附的元素 + */ attachEl?: HTMLElement + /** + * 显示标题分割线 + */ + showTitleDivider?: boolean } if (__DEV__) { diff --git a/packages/ui/popover/src/index.ts b/packages/ui/popover/src/index.ts index 029580b6e..7ace0bd2d 100644 --- a/packages/ui/popover/src/index.ts +++ b/packages/ui/popover/src/index.ts @@ -1,11 +1,11 @@ import './styles/index.scss' import { Popover as PurePopover } from './Popover' -import { withModal } from './with-api' +import { withPopover } from './with-api' export * from './Popover' -export const Popover = withModal(PurePopover) +export const Popover = withPopover(PurePopover) export default Popover export * from './types' diff --git a/packages/ui/popover/src/styles/popover.scss b/packages/ui/popover/src/styles/popover.scss index a840c7d7f..22e02f9f5 100644 --- a/packages/ui/popover/src/styles/popover.scss +++ b/packages/ui/popover/src/styles/popover.scss @@ -9,13 +9,24 @@ $prefix: '#{$component-prefix}-popover' !default; padding: use-spacing(10); border-radius: use-border-radius('lg'); + &--divided { + padding-top: use-spacing(7); + } + &__title { box-sizing: border-box; - padding-bottom: use-spacing(8); + margin-bottom: use-spacing(8); color: use-color('gray', 700); font-size: use-text-size('lg'); font-weight: use-text-weight('medium'); line-height: use-text-lineheight('lg'); + + .#{$prefix}--divided > & { + padding-bottom: use-spacing(7); + font-size: use-text-size('normal'); + line-height: use-text-lineheight('normal'); + border-bottom: use-border-size('normal') use-color('gray', 200); + } } &__content { diff --git a/packages/ui/popover/src/use-popover.tsx b/packages/ui/popover/src/use-popover.tsx index b88652b9d..7f8efa0b1 100644 --- a/packages/ui/popover/src/use-popover.tsx +++ b/packages/ui/popover/src/use-popover.tsx @@ -150,12 +150,11 @@ export const usePopover = ({ ...popperProps, visible, attachEl: attachEl ?? triggerEl, - onOpen: visibleAction.on, onClose: visibleAction.off, } - }, [popper, visible, attachEl, triggerEl, visibleAction.off, visibleAction.on]) + }, [popper, visible, attachEl, triggerEl, visibleAction.off]) - return { rootProps: rest, getOverlayProps, getTriggerProps, getPopperProps } + return { rootProps: rest, getOverlayProps, getTriggerProps, getPopperProps, visibleAction } } export interface UsePopoverProps extends PopperOverlayProps { @@ -187,6 +186,9 @@ export interface UsePopoverProps extends PopperOverlayProps { * 设置基于 reference 元素的间隙偏移量 */ gutterGap?: number + /** + * 吸附的元素 + */ attachEl?: HTMLElement } diff --git a/packages/ui/popover/src/with-api.tsx b/packages/ui/popover/src/with-api.tsx index 0bcb03abc..cee18ca7a 100644 --- a/packages/ui/popover/src/with-api.tsx +++ b/packages/ui/popover/src/with-api.tsx @@ -1,4 +1,4 @@ -import React, { createRef, createElement } from 'react' +import { createRef, createElement } from 'react' import { render, unmountComponentAtNode } from 'react-dom' import * as Container from '@hi-ui/container' import { uuid } from '@hi-ui/use-id' @@ -7,54 +7,68 @@ import { prefix as popoverPrefix, Popover, PopoverProps } from './Popover' const prefixCls = popoverPrefix const selector = `.${prefixCls}-wrapper` -const toastManagerRef = createRef() -const open = (target: HTMLElement, { onOpen, onClose, ...rest }: PopoverApiProps) => { - const selectorId = `${selector}__${uuid()}` - let container: any = Container.getContainer(selectorId) - console.log('target', target) +const tooltipInstanceCache: { + [key: string]: () => void +} = {} + +const open = (target: HTMLElement, { key, onClose, disabledPortal, ...rest }: PopoverApiProps) => { + if (!key) { + key = uuid() + } + + let container: any = Container.getContainer( + `${selector}__${key}`, + undefined, + (disabledPortal ? target.parentNode : undefined) as Element + ) + + const popoverRef = createRef() const ClonedPopover = createElement(Popover, { ...rest, - // visible: true, - innerRef: toastManagerRef, + innerRef: popoverRef, container, attachEl: target, closeOnOutsideClick: false, shouldWrapChildren: true, - // children: attachEl, - onOpen: () => { - onOpen?.() - }, - onClose: () => { - console.log('close', toastManagerRef.current) - - onClose?.() - - setTimeout(() => { - if (container) { - unmountComponentAtNode(container as Element) - container = null - } + placement: 'bottom-end', + onExited: () => { + // 卸载 + if (container) { + unmountComponentAtNode(container) Container.removeContainer(selector) - }, 300) + } + container = undefined }, }) requestAnimationFrame(() => { render(ClonedPopover, container) - toastManagerRef.current.open() + popoverRef.current.open() }) + + const close = () => { + popoverRef.current?.close() + } + + if (key) { + tooltipInstanceCache[key] = close + } + + return key } -const close = () => { - console.log('close.') +const close = (key: string) => { + if (typeof tooltipInstanceCache[key] === 'function') { + tooltipInstanceCache[key]() + } - toastManagerRef.current.close() + delete tooltipInstanceCache[key] } export interface PopoverApiProps extends PopoverProps {} -export function withModal(instance: typeof Popover) { +export function withPopover(instance: typeof Popover) { return Object.assign(instance, { open, close }) } diff --git a/packages/ui/popover/stories/content.stories.tsx b/packages/ui/popover/stories/content.stories.tsx new file mode 100644 index 000000000..53cd5b459 --- /dev/null +++ b/packages/ui/popover/stories/content.stories.tsx @@ -0,0 +1,102 @@ +import React from 'react' +import Popover from '../src' +import Button from '@hi-ui/button' +import Form from '@hi-ui/form' +import Input from '@hi-ui/input' +import { CloseOutlined } from '@hi-ui/icons' +import { IconButton } from '@hi-ui/icon-button' + +/** + * @title 自定义内容 + */ +export const Content = () => { + const FormItem = Form.Item + + const [visible, setVisible] = React.useState(false) + const [loading, setLoading] = React.useState(false) + + const title = ( +
+ 文字标题 + } onClick={() => setVisible(false)} /> +
+ ) + + const content = ( +
+
+
+ + + + + + +
+
+
+ + +
+
+ ) + + return ( + <> +

Content

+
+ + + +
+ + ) +} diff --git a/packages/ui/popover/stories/index.stories.tsx b/packages/ui/popover/stories/index.stories.tsx index 61d86f768..7e1ea3f65 100644 --- a/packages/ui/popover/stories/index.stories.tsx +++ b/packages/ui/popover/stories/index.stories.tsx @@ -7,6 +7,7 @@ export * from './trigger.stories' export * from './placement.stories' export * from './controlled.stories' export * from './gutter-gap.stories' +export * from './content.stories' export * from './with-api.stories' export default { diff --git a/packages/ui/popover/stories/with-api.stories.tsx b/packages/ui/popover/stories/with-api.stories.tsx index 48ae956d8..3ae8a8e84 100644 --- a/packages/ui/popover/stories/with-api.stories.tsx +++ b/packages/ui/popover/stories/with-api.stories.tsx @@ -1,44 +1,128 @@ import React from 'react' import Popover from '../src' import Button from '@hi-ui/button' +import Form from '@hi-ui/form' +import Input from '@hi-ui/input' +import { CloseOutlined } from '@hi-ui/icons' +import { IconButton } from '@hi-ui/icon-button' /** - * @title API 用法 - * @desc 用于列表中的每一项 + * @title API 方式调用 */ export const WithApi = () => { - const title = 文字提示 - const content = ( - <> -
-
此处展示 Popover 具体内容
-
具体内容可以自行渲染
-
-
- + const FormItem = Form.Item + const key1 = 'key1' + + const Title = ({ title }) => { + return ( +
+ {title} + } onClick={() => Popover.close(key1)} />
- - ) + ) + } - const triggerRef = React.useRef(null) + const Content = () => { + const [loading, setLoading] = React.useState(false) + + return ( +
+
+
+ + + + + + +
+
+
+ + +
+
+ ) + } return ( <>

WithApi

- , +

此处展示多个操作使用同一个容器,即 API 调用时,将 key 设置为同一个

+