diff --git a/.changeset/afraid-socks-mix.md b/.changeset/afraid-socks-mix.md new file mode 100644 index 000000000..8f5abb994 --- /dev/null +++ b/.changeset/afraid-socks-mix.md @@ -0,0 +1,5 @@ +--- +"@hi-ui/hiui": patch +--- + +feat(popover): 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..2167f3423 --- /dev/null +++ b/.changeset/late-cars-walk.md @@ -0,0 +1,5 @@ +--- +"@hi-ui/popper": patch +--- + +chore: 扩充 PopperOverlayProps 参数, 'closeOnOutsideClick' | 'onExited' | 'crossGap' | 'arrow' | 'disabledPortal' | 'zIndex' 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 0631b4716..bd5c2d15d 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, @@ -23,11 +30,23 @@ export const Popover = forwardRef( shouldWrapChildren = false, autoWrapChildren = true, wrapTagName = 'span', + showTitleDivider = false, ...rest }, ref ) => { - const { rootProps, getTriggerProps, getPopperProps, getOverlayProps } = usePopover(rest) + const { + rootProps, + getTriggerProps, + getPopperProps, + getOverlayProps, + visibleAction, + } = usePopover(rest) + + useImperativeHandle(innerRef, () => ({ + open: visibleAction.on, + close: visibleAction.off, + })) const triggerMemo = useMemo(() => { let trigger: React.ReactElement | null | undefined @@ -62,7 +81,7 @@ export const Popover = forwardRef( return trigger }, [children, getTriggerProps, autoWrapChildren, shouldWrapChildren, wrapTagName]) - const cls = cx(prefixCls, className) + const cls = cx(prefixCls, showTitleDivider && `${prefixCls}--divided`, className) return ( <> @@ -79,6 +98,7 @@ export const Popover = forwardRef( ) export interface PopoverProps extends HiBaseHTMLProps<'div'>, UsePopoverProps { + innerRef?: React.Ref<{ open: () => void; close: () => void }> /** * 气泡卡片标题 */ @@ -99,6 +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 9fdde71b3..7ace0bd2d 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 { withPopover } from './with-api' + export * from './Popover' -export { Popover as default } from './Popover' + +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 af03b17c3..7f8efa0b1 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,12 +149,12 @@ export const usePopover = ({ return { ...popperProps, visible, - attachEl: triggerEl, + attachEl: attachEl ?? triggerEl, onClose: visibleAction.off, } - }, [visible, popper, visibleAction, triggerEl]) + }, [popper, visible, attachEl, triggerEl, visibleAction.off]) - return { rootProps: rest, getOverlayProps, getTriggerProps, getPopperProps } + return { rootProps: rest, getOverlayProps, getTriggerProps, getPopperProps, visibleAction } } export interface UsePopoverProps extends PopperOverlayProps { @@ -185,6 +186,10 @@ 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..97dc7b948 --- /dev/null +++ b/packages/ui/popover/src/with-api.tsx @@ -0,0 +1,74 @@ +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' + +import { prefix as popoverPrefix, Popover, PopoverProps } from './Popover' + +const prefixCls = popoverPrefix +const selector = `.${prefixCls}-wrapper` + +const popoverInstanceCache: { + [key: string]: () => void +} = {} + +const open = (target: HTMLElement, { key, disabledPortal, ...rest }: PopoverApiProps) => { + if (!key) { + key = uuid() + } + + const selectId = `${selector}__${key}` + let container: any = Container.getContainer( + selectId, + undefined, + (disabledPortal ? target.parentNode : undefined) as Element + ) + + const popoverRef = createRef() + + const ClonedPopover = createElement(Popover, { + innerRef: popoverRef, + container, + attachEl: target, + closeOnOutsideClick: false, + shouldWrapChildren: true, + onExited: () => { + // 卸载 + if (container) { + unmountComponentAtNode(container) + Container.removeContainer(selectId) + } + container = undefined + }, + ...rest, + }) + + requestAnimationFrame(() => { + render(ClonedPopover, container) + popoverRef.current.open() + }) + + const close = () => { + popoverRef.current?.close() + } + + if (key) { + popoverInstanceCache[key] = close + } + + return key +} + +const close = (key: string) => { + if (typeof popoverInstanceCache[key] === 'function') { + popoverInstanceCache[key]() + } + + delete popoverInstanceCache[key] +} + +export interface PopoverApiProps extends PopoverProps {} + +export function withPopover(instance: typeof Popover) { + return Object.assign(instance, { open, close }) +} diff --git a/packages/ui/popover/stories/basic.stories.tsx b/packages/ui/popover/stories/basic.stories.tsx index 8d9caa2b0..8c12e40e5 100644 --- a/packages/ui/popover/stories/basic.stories.tsx +++ b/packages/ui/popover/stories/basic.stories.tsx @@ -19,7 +19,7 @@ export const Basic = () => { <>

Basic

- + console.log(33)}>
diff --git a/packages/ui/popover/stories/content.stories.tsx b/packages/ui/popover/stories/content.stories.tsx new file mode 100644 index 000000000..3363569ba --- /dev/null +++ b/packages/ui/popover/stories/content.stories.tsx @@ -0,0 +1,130 @@ +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 popoverVisibleRef = React.useRef(false) + + const title = ( +
+ 文字标题 + } + onClick={() => { + setVisible(false) + popoverVisibleRef.current = false + }} + /> +
+ ) + + const content = ( +
+
+
+ + + + + + +
+
+
+ + +
+
+ ) + + return ( + <> +

Content

+
+ + + +
+ + ) +} diff --git a/packages/ui/popover/stories/index.stories.tsx b/packages/ui/popover/stories/index.stories.tsx index a243b6a80..7e1ea3f65 100644 --- a/packages/ui/popover/stories/index.stories.tsx +++ b/packages/ui/popover/stories/index.stories.tsx @@ -7,6 +7,8 @@ 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 { 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..1cf10f8cb --- /dev/null +++ b/packages/ui/popover/stories/with-api.stories.tsx @@ -0,0 +1,131 @@ +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 方式调用 + */ +export const WithApi = () => { + const FormItem = Form.Item + const key1 = 'key1' + + const Title = ({ title }) => { + return ( +
+ {title} + } onClick={() => Popover.close(key1)} /> +
+ ) + } + + const Content = () => { + const [loading, setLoading] = React.useState(false) + + return ( +
+
+
+ + + + + + +
+
+
+ + +
+
+ ) + } + + return ( + <> +

WithApi

+
+

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

+