From be5047cdca844067e20cf6125cc197fecf503671 Mon Sep 17 00:00:00 2001 From: "PL196\\40380" <403802162@qq.com> Date: Sun, 8 Sep 2024 13:37:23 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .docs/docs/libraries/dialog.md | 6 ++ .docs/docs/libraries/tooltip.md | 2 +- .../components/_styles/common/transition.scss | 82 +++++++++++++------ packages/components/_styles/root-common.scss | 4 + packages/components/dialog/dialog-overlay.tsx | 26 +++--- packages/components/dialog/dialog-wrap.tsx | 2 +- packages/components/dialog/dialog.tsx | 48 ++++++++--- packages/components/dialog/memo-children.tsx | 15 ++++ packages/components/dialog/type.ts | 4 +- packages/components/tooltip/popup.tsx | 2 +- packages/core/utils/dom.ts | 22 +++++ 11 files changed, 165 insertions(+), 48 deletions(-) create mode 100644 packages/components/dialog/memo-children.tsx diff --git a/.docs/docs/libraries/dialog.md b/.docs/docs/libraries/dialog.md index 7f9424c..19a877f 100644 --- a/.docs/docs/libraries/dialog.md +++ b/.docs/docs/libraries/dialog.md @@ -150,6 +150,12 @@ export default () => { | header | 头部内容 | ReactNode | — | | footer | 底部内容 | ReactNode | — | | getContainer | 指定 Dialog 挂载的节点, 需要始终返回唯一 dom 节点 | () => HTMLElement | document.body | +| transitionName | [动画名称, 请查看描述](https://reactcommunity.org/react-transition-group/css-transition) | string | — | +| duration | 执行动画的时长(以毫秒为单位)的 | number | 300 | | beforeClose | 关闭前的回调,会暂停 Dialog 的关闭. 回调函数内执行 done 参数方法的时候才是真正关闭对话框的时候 | Function | — | | onClose | 点击遮罩层或右上角叉或取消按钮的回调 | function(e) | — | | afterClose | Dialog 完全关闭后的回调 | Function | — | + +#### 注意 + +- `` 默认关闭后状态不会自动清空,如果希望每次打开都是新内容,请设置 `destroyOnClose`。 diff --git a/.docs/docs/libraries/tooltip.md b/.docs/docs/libraries/tooltip.md index 1cb73c9..a05bdaf 100644 --- a/.docs/docs/libraries/tooltip.md +++ b/.docs/docs/libraries/tooltip.md @@ -98,7 +98,7 @@ export default App; | strategy | 描述要使用的定位策略。默认情况下,它是absolute | string | absolute | | hideAfterTime | 消失的延迟,以毫秒为单位 | number | 100 | | showAfterTime | 出现延迟,以毫秒为单位 | number | 100 | -| transitionName | 动画名称 | string | — | +| transitionName | [动画名称, 请查看描述](https://reactcommunity.org/react-transition-group/css-transition) | string | — | | duration | 执行动画的时长(以毫秒为单位)的 | number | 200 | | disabled | 是否禁止 | boolean | false | | destroyTooltipOnHide | 关闭后是否销毁 Tooltip | boolean | false | diff --git a/packages/components/_styles/common/transition.scss b/packages/components/_styles/common/transition.scss index 26c2427..94bc560 100644 --- a/packages/components/_styles/common/transition.scss +++ b/packages/components/_styles/common/transition.scss @@ -1,47 +1,79 @@ @use "../core/config" as *; +@use "../core/function" as *; // https://reactcommunity.org/react-transition-group/css-transition +@mixin zoomBigOutFn($duration) { + transform: scale(0); + opacity: 0; + animation-duration: $duration; + animation-fill-mode: both; + animation-play-state: paused; + animation-timing-function: getCssVar("motion-ease-out-circ"); + transform-origin: 50% 50%; +} +@mixin zoomBigInOutFn($duration) { + animation-duration: $duration; + animation-fill-mode: both; + animation-play-state: paused; + animation-timing-function: getCssVar("motion-ease-in-out-circ"); + transform-origin: 50% 50%; +} /* -* 中间放大 -- 用于tooltip +* 中间快速放大 -- 用于tooltip */ -.#{$namespace}-zoom-in-top-exit-active { - animation-name: #{$namespace}-zoom-in-top-out; - animation-duration: 0.2s; - animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); - transform-origin: center center; - &[data-popper-placement^="top"] { - transform-origin: center center; - } +.#{$namespace}-zoom-big-fast-enter { + @include zoomBigOutFn(0.1s); } -.#{$namespace}-zoom-in-top-enter-active { - animation-name: #{$namespace}-zoom-in-top-in; - animation-duration: 0.2s; - animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); - transform-origin: center center; - &[data-popper-placement^="top"] { - transform-origin: center center; - } +.#{$namespace}-zoom-big-fast-exit { + @include zoomBigInOutFn(0.1s); +} +.#{$namespace}-zoom-big-fast-enter-active { + animation-name: #{$namespace}-zoom-big-in; + animation-play-state: running; +} +.#{$namespace}-zoom-big-fast-exit-active { + animation-name: #{$namespace}-zoom-big-out; + animation-play-state: running; } -@keyframes #{$namespace}-zoom-in-top-in { + +/* +* 中间放大 -- 用于Popover +*/ +.#{$namespace}-zoom-big-enter { + @include zoomBigOutFn(0.2s); +} +.#{$namespace}-zoom-big-exit { + @include zoomBigInOutFn(0.21s); +} +.#{$namespace}-zoom-big-enter-active { + animation-name: #{$namespace}-zoom-big-in; + animation-play-state: running; +} +.#{$namespace}-zoom-big-exit-active { + animation-name: #{$namespace}-zoom-big-out; + animation-play-state: running; +} + +@keyframes #{$namespace}-zoom-big-in { 0% { - transform: scaleY(0.8); + transform: scale(0.8); opacity: 0; } 100% { - transform: scaleY(1); + transform: scale(1); opacity: 1; } } -@keyframes #{$namespace}-zoom-in-top-out { +@keyframes #{$namespace}-zoom-big-out { 0% { - transform: scaleY(1); + transform: scale(1); opacity: 1; } 100% { - transform: scaleY(0.8); + transform: scale(0.8); opacity: 0; } } @@ -78,7 +110,9 @@ transform: scale(1); } -/** 折叠面板 */ +/* + * 折叠面板 + */ .#{$namespace}-collapse-transition-exit-active, .#{$namespace}-collapse-transition-enter-active { transition: diff --git a/packages/components/_styles/root-common.scss b/packages/components/_styles/root-common.scss index 046143d..a32cd16 100644 --- a/packages/components/_styles/root-common.scss +++ b/packages/components/_styles/root-common.scss @@ -25,6 +25,10 @@ // 全局组件的高度,比如input类型的 @include set-component-css-var("component-size", $common-component-size); + + // Animation (cubic Bezier curve)--缓和 + --#{$namespace}-motion-ease-out-circ: cubic-bezier(0.08, 0.82, 0.17, 1); + --#{$namespace}-motion-ease-in-out-circ: cubic-bezier(0.78, 0.14, 0.15, 0.86); } // for light diff --git a/packages/components/dialog/dialog-overlay.tsx b/packages/components/dialog/dialog-overlay.tsx index a24c22e..7c6d474 100644 --- a/packages/components/dialog/dialog-overlay.tsx +++ b/packages/components/dialog/dialog-overlay.tsx @@ -2,13 +2,15 @@ * @Date: 2024-08-03 22:09:25 * @Description: Modify here please */ -import React, { useContext, useMemo } from "react"; +import React, { useContext, useMemo, useRef } from "react"; import classNames from "classnames"; import { ConfigContext } from "../config-provider"; import { useNamespace } from "@camelia/core/hooks"; import { KeyCode } from "@camelia/core"; import Visible from "../_internal/visible"; +import MemoChildren from "./memo-children"; + import { useSameTarget } from "./composables/use-same-target"; import type { IOverlayProps, IDialogProps } from "./type"; @@ -17,18 +19,20 @@ type RepeatOverlayProps = IOverlayProps & { alignCenter: IDialogProps["alignCenter"]; keyboard: IDialogProps["keyboard"]; } & { - nodeRef: React.MutableRefObject; + visible?: boolean; + wrapperRef: React.MutableRefObject; + rootRef: React.MutableRefObject; + /** 内容 */ + children: React.ReactNode; + style?: React.CSSProperties; /** 蒙层点击 */ onClick: (e: React.MouseEventHandler) => void; /** esc按键关闭时 */ onInternalClose: (e: React.KeyboardEvent) => void; - /** 内容 */ - children: React.ReactNode; - style?: React.CSSProperties; }; const DialogOverlay: React.FC = (props) => { - const { mask, zIndex, overlayClass, children, alignCenter, style, nodeRef, keyboard } = props; + const { mask, zIndex, overlayClass, children, alignCenter, style, rootRef, wrapperRef, keyboard, visible } = props; const { getPrefixCls } = useContext(ConfigContext); const ns = useNamespace("dialog", getPrefixCls()); @@ -61,9 +65,10 @@ const DialogOverlay: React.FC = (props) => { return ( <> -
+
= (props) => { onKeyDown={onWrapperKeyDown} style={overlayDialogStyle} > - {children} + {children}
= (props) => { onMouseDown={onMousedown} onMouseUp={onMouseup} onClick={onClick} + ref={wrapperRef} tabIndex={-1} onKeyDown={onWrapperKeyDown} style={overlayDialogStyle} > - {children} + {children}
diff --git a/packages/components/dialog/dialog-wrap.tsx b/packages/components/dialog/dialog-wrap.tsx index 864b719..8548626 100644 --- a/packages/components/dialog/dialog-wrap.tsx +++ b/packages/components/dialog/dialog-wrap.tsx @@ -8,7 +8,7 @@ import Portal from "../_internal/portal"; import Dialog from "./dialog"; const DialogWrap: React.FC = (props) => { - const { open, getContainer, destroyOnClose = true, afterClose, lockScroll = true } = props; + const { open, getContainer, destroyOnClose = false, afterClose, lockScroll = true } = props; const [animatedVisible, setAnimatedVisible] = useState(open); diff --git a/packages/components/dialog/dialog.tsx b/packages/components/dialog/dialog.tsx index 6fe9a88..d0efd82 100644 --- a/packages/components/dialog/dialog.tsx +++ b/packages/components/dialog/dialog.tsx @@ -8,6 +8,8 @@ import { CSSTransition } from "react-transition-group"; import { ConfigContext } from "../config-provider"; import { useNamespace, useZIndex } from "@camelia/core/hooks"; import { Close as CloseIcon } from "fish-icons"; +import { contains } from "@camelia/core"; + import Visible from "../_internal/visible"; import type { IDialogProps } from "./type"; import DialogOverlay from "./dialog-overlay"; @@ -17,6 +19,7 @@ const Dialog: React.FC = (props) => { top, width, open, + transitionName, mask = true, keyboard = true, alignCenter = false, @@ -45,7 +48,9 @@ const Dialog: React.FC = (props) => { const { currentZIndex } = useZIndex(); - const nodeRef = useRef(null); + const rootRef = useRef(null); + const wrapperRef = useRef(); + const contentRef = useRef(); // dialog Style const dialogStyle = useMemo(() => { @@ -56,10 +61,17 @@ const Dialog: React.FC = (props) => { if (width) { style[`width`] = typeof width == "string" ? width : `${width}px`; } + style[`outline`] = "none"; + // show? style[`display`] = animatedVisible ? "block" : "none"; return style; }, [top, width, animatedVisible]); + // 动画类名 + const motionName = useMemo(() => { + return transitionName || `dialog-fade`; + }, [transitionName]); + // 点击蒙层时 const onMaskClick = (e: any) => { if (closeOnClickMask) { @@ -81,6 +93,23 @@ const Dialog: React.FC = (props) => { } }; + useEffect(() => { + if (open) { + setAnimatedVisible(true); + } + }, [open]); + + function focusDialogContent() { + if (!contains(wrapperRef.current, document.activeElement)) { + contentRef.current?.focus({ preventScroll: true }); + } + } + + const onEnter = () => { + // Try to focus + focusDialogContent(); + }; + // 弹窗元素已从 DOM 中移除时调用 const onExited = () => { // 等待动画结束才才改变状态 @@ -92,24 +121,21 @@ const Dialog: React.FC = (props) => { } }; - useEffect(() => { - if (open) { - setAnimatedVisible(true); - } - }, [open]); - return ( = (props) => { >
diff --git a/packages/components/dialog/memo-children.tsx b/packages/components/dialog/memo-children.tsx new file mode 100644 index 0000000..34e386d --- /dev/null +++ b/packages/components/dialog/memo-children.tsx @@ -0,0 +1,15 @@ +/* + * @Date: 2024-09-08 09:47:51 + * @Description: Modify here please + */ +import * as React from "react"; + +export type MemoChildrenProps = { + shouldUpdate: boolean; + children: React.ReactNode; +}; + +export default React.memo( + ({ children }: MemoChildrenProps) => children as React.ReactElement, + (_, { shouldUpdate }) => !shouldUpdate +); diff --git a/packages/components/dialog/type.ts b/packages/components/dialog/type.ts index 53fc197..e26ce64 100644 --- a/packages/components/dialog/type.ts +++ b/packages/components/dialog/type.ts @@ -29,7 +29,9 @@ export interface IDialogProps extends IOverlayProps, IDialogContentProps { lockScroll?: boolean; /** 是否支持键盘 esc 关闭 */ keyboard?: boolean; - /** 动画持续时间,当你想修改自带的动画效果时,这个很有用 */ + /** 执行的动画的类名 */ + transitionName?: string; + /** 执行动画的时长,当你需要自定义动画时,这个是很有用的 */ duration?: number; /** 弹窗body内容部分,语义化结构className */ className?: string; diff --git a/packages/components/tooltip/popup.tsx b/packages/components/tooltip/popup.tsx index efbcf03..2772c87 100644 --- a/packages/components/tooltip/popup.tsx +++ b/packages/components/tooltip/popup.tsx @@ -46,7 +46,7 @@ const TooltipPopup = React.forwardRef((pro // 动画类名 const motionName = useMemo(() => { - return transitionName || `${getPrefixCls()}-zoom-in-top`; + return transitionName || `${getPrefixCls()}-zoom-big-fast`; }, [transitionName]); // When the element has been removed from the DOM diff --git a/packages/core/utils/dom.ts b/packages/core/utils/dom.ts index 3e1a78a..412e252 100644 --- a/packages/core/utils/dom.ts +++ b/packages/core/utils/dom.ts @@ -37,3 +37,25 @@ export const getScrollBarWidth = (): number => { export function canUseDom() { return !!(typeof window !== "undefined" && window.document && window.document.createElement); } + +export function contains(root: Node | null | undefined, n?: Node) { + if (!root) { + return false; + } + + // Use native if support + if (root.contains) { + return root.contains(n); + } + + // `document.contains` not support with IE11 + let node = n; + while (node) { + if (node === root) { + return true; + } + node = node.parentNode; + } + + return false; +} From 267941f74c601ccf651623ebf0a3c7c0dcf28b8b Mon Sep 17 00:00:00 2001 From: "PL196\\40380" <403802162@qq.com> Date: Sun, 8 Sep 2024 19:48:06 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .docs/docs/libraries/popover.md | 104 ++++++++++++++++ .docs/docs/libraries/tooltip.md | 10 +- .docs/style/components.scss | 5 +- .docs/style/index.scss | 1 + .docs/style/page.scss | 7 ++ packages/camelia/version.ts | 2 +- .../components/_styles/common/transition.scss | 44 +++++++ packages/components/index.ts | 5 +- packages/components/popover/index.tsx | 111 ++++++++++++++++++ packages/components/popover/style/index.scss | 37 ++++++ packages/components/popover/style/index.ts | 7 ++ packages/components/tooltip/popup-type.ts | 2 +- packages/components/tooltip/popup.tsx | 2 +- packages/components/tooltip/style/index.scss | 4 +- packages/components/tooltip/tooltip-type.ts | 17 ++- packages/components/tooltip/tooltip.tsx | 27 +++-- packages/components/tooltip/trigger-type.ts | 7 +- packages/components/tooltip/trigger.tsx | 39 ++++-- packages/shared/use-click-away/index.ts | 1 - 19 files changed, 396 insertions(+), 36 deletions(-) create mode 100644 .docs/docs/libraries/popover.md create mode 100644 packages/components/popover/index.tsx create mode 100644 packages/components/popover/style/index.scss create mode 100644 packages/components/popover/style/index.ts diff --git a/.docs/docs/libraries/popover.md b/.docs/docs/libraries/popover.md new file mode 100644 index 0000000..5395b8f --- /dev/null +++ b/.docs/docs/libraries/popover.md @@ -0,0 +1,104 @@ +--- +order: 4 +group: + title: 数据展示 + order: 3 +--- + +# Popover 气泡卡片 + +Popover 是在 Tooltip 基础上开发出来的。 因此对于重复属性,请参考 Tooltip 的文档,在此文档中不做详尽解释。 + +## 基础用法 +1. trigger 属性被用来决定 popover 的触发方式,支持的触发方式: `hover` `click` 如果你想手动控制它,可以设置 visible 属性。 +2. 最简单的用法,浮层的大小由内容区域决定。 + +```tsx +import React from 'react'; +import { Button, Popover } from 'camelia'; + +const content = ( +
+

There is a lot of content

+

There is a lot of content

+
+); + +const App: React.FC = () => ( + <> + + + + + + + +); + +export default App; +``` + +## 虚拟触发 + +1. 可以由虚拟元素触发,这个功能就很适合使用在触发元素和展示内容元素是分开的场景 +2. 结合visible,你可以做的主动控制Popover + +```tsx +import React, { useState, useRef } from 'react'; +import { Button, Popover } from 'camelia'; +import { useClickAway } from "camelia/shared"; + +const content = ( +
+

There is a lot of content

+

There is a lot of content

+
+); + +const App: React.FC = () => { + const [clicked, setClicked] = useState(false); + const [enable, setEnable] = useState(false); + + const virtualRef = useRef(null) + const popoverRef = useRef(null) + + // 点击弹窗外面主动取消 + useClickAway(() => { + setClicked(false) + }, popoverRef.current?.popupRef, { + enable + }); + + return( + <> + + { + setEnable(v) + }} + /> + + ) +} + +export default App; +``` + +## API + +| 参数 | 说明 | 类型 | 默认值 | 版本 | +| ------- | -------- | ---------------------------- | ------ | ---- | +| content | 卡片内容 | ReactNode \| () => ReactNode | - | | +| title | 卡片标题 | ReactNode \| () => ReactNode | - | | + +更多属性请参考 [Tooltip](http://cameliya.cn/libraries/tooltip#api)。 + +## 注意 + +请确保 `Popover` 的子元素能接受 `onMouseEnter` `onMouseLeave` `onClick` 事件。 diff --git a/.docs/docs/libraries/tooltip.md b/.docs/docs/libraries/tooltip.md index a05bdaf..40c9a4e 100644 --- a/.docs/docs/libraries/tooltip.md +++ b/.docs/docs/libraries/tooltip.md @@ -101,21 +101,23 @@ export default App; | transitionName | [动画名称, 请查看描述](https://reactcommunity.org/react-transition-group/css-transition) | string | — | | duration | 执行动画的时长(以毫秒为单位)的 | number | 200 | | disabled | 是否禁止 | boolean | false | -| destroyTooltipOnHide | 关闭后是否销毁 Tooltip | boolean | false | +| destroyTooltipOnHide | 关闭后是否销毁 Tooltip | boolean | true | | overlayClassName | 卡片类名 | string | — | | overlayStyle | 卡片style | CSSProperties | — | | zIndex | 设置 Tooltip 的 z-index | number | — | | visible | 受控模式,来控制它的显示与关闭 | boolean | — | | getPopupContainer | 浮层渲染父节点,默认渲染到 body 上 | (triggerNode:HTMLElement) => void | — | | showArrow | tooltip 的内容是否有箭头 | boolean | true | -| onShow | 显示时的回调 | () => void | — | -| onHide | 隐藏时的回调 | () => void | — | +| virtualTriggering | 用来标识虚拟触发是否被启用 | boolean | false | +| virtualRef | 标识虚拟触发时的触发元素 | MutableRefObject< HTMLElement > | — | +| onOpenChange | 显示隐藏的回调 | (open: boolean) => void | — | ### Methods | 名称 | 说明 | 类型 | | ------------ | -------------------------------------------------------------------------------- | ---------------------------- | | tooltipRef | tooltip Component | object | +| popupRef | 获取popup的ref, 可能有些业务场景是想要获取popup节点的 | object | | onOpen | 控制显示状态 | Function | | onClose | 控制隐藏状态, 可以传递一个time来覆盖hideAfterTime,为0时立马关闭弹窗 | Function `(time) => void` | -| updatePopup | 使用更新位置 | Function +| updatePopup | 使用更新位置 | Function | diff --git a/.docs/style/components.scss b/.docs/style/components.scss index f6f87ed..294dc19 100644 --- a/.docs/style/components.scss +++ b/.docs/style/components.scss @@ -4,10 +4,11 @@ @use "../../packages/components/checkbox/style/index.scss" as *; @use "../../packages/components/dialog/style/index.scss" as *; @use "../../packages/components/image/style/index.scss" as *; + @use "../../packages/components/input/style/index.scss" as *; + @use "../../packages/components/img-captcha/style/index.scss" as *; @use "../../packages/components/message/style/index.scss" as *; + @use "../../packages/components/popover/style/index.scss" as *; @use "../../packages/components/prompt/style/index.scss" as *; - @use "../../packages/components/img-captcha/style/index.scss" as *; - @use "../../packages/components/input/style/index.scss" as *; @use "../../packages/components/tag/style/index.scss" as *; @use "../../packages/components/tooltip/style/index.scss" as *; @use "../../packages/components/_internal/wave/style/index.scss" as *; diff --git a/.docs/style/index.scss b/.docs/style/index.scss index 49c6d72..d880eef 100644 --- a/.docs/style/index.scss +++ b/.docs/style/index.scss @@ -6,3 +6,4 @@ // 引入页面组件的样式 @import "./page.scss"; + diff --git a/.docs/style/page.scss b/.docs/style/page.scss index 8aa26a5..fd02af7 100644 --- a/.docs/style/page.scss +++ b/.docs/style/page.scss @@ -1,5 +1,12 @@ @use "../.dumi/var.scss" as *; +.layout-init-box { + p { + margin: 0; + } +} + + .site-t2vb78 .icon-item { :hover { background-color: #2b2b2c; diff --git a/packages/camelia/version.ts b/packages/camelia/version.ts index 333a7e4..f14110c 100644 --- a/packages/camelia/version.ts +++ b/packages/camelia/version.ts @@ -6,4 +6,4 @@ * "当你在使用 pnpm 处理多包依赖时,如果本地包的版本与远程版本完全匹配, * 则 pnpm 会默认使用本地包覆盖远程包,无论远程包的版本是更新的还是旧的。" */ -export const version = "1.1.3"; +export const version = "1.1.4"; diff --git a/packages/components/_styles/common/transition.scss b/packages/components/_styles/common/transition.scss index 94bc560..7d7c574 100644 --- a/packages/components/_styles/common/transition.scss +++ b/packages/components/_styles/common/transition.scss @@ -78,6 +78,50 @@ } } +/* +* 放大顶部 +*/ +.#{$namespace}-zoom-in-top-enter-active { + animation-name: #{$namespace}-zoom-in-top-in; + animation-duration: 0.2s; + animation-timing-function: cubic-bezier(0.23, 1, 0.32, 1); + transform-origin: center top; + &[data-popper-placement^="top"] { + transform-origin: center bottom; + } +} +.#{$namespace}-zoom-in-top-exit-active { + animation-name: #{$namespace}-zoom-in-top-out; + animation-duration: 0.2s; + animation-timing-function: cubic-bezier(0.755, 0.05, 0.855, 0.06); + transform-origin: center top; + &[data-popper-placement^="top"] { + transform-origin: center bottom; + } +} +@keyframes #{$namespace}-zoom-in-top-in { + 0% { + transform: scaleY(0.8); + opacity: 0; + } + + 100% { + transform: scaleY(1); + opacity: 1; + } +} +@keyframes #{$namespace}-zoom-in-top-out { + 0% { + transform: scaleY(1); + opacity: 1; + } + + 100% { + transform: scaleY(0.8); + opacity: 0; + } +} + /* * 淡入按比例放大 --- select */ diff --git a/packages/components/index.ts b/packages/components/index.ts index 8bdf630..f84d355 100644 --- a/packages/components/index.ts +++ b/packages/components/index.ts @@ -28,7 +28,10 @@ export { default as Dialog } from "./dialog"; export type { IDialogProps } from "./dialog"; export { default as Tooltip } from "./tooltip"; -export type { ITooltipProps } from "./tooltip"; +export type { ITooltipProps, ITooltipRef } from "./tooltip"; + +export { default as Popover } from "./popover"; +export type { IPopoverProps } from "./popover"; // Plugin export { default as message } from "./message"; diff --git a/packages/components/popover/index.tsx b/packages/components/popover/index.tsx new file mode 100644 index 0000000..69b2366 --- /dev/null +++ b/packages/components/popover/index.tsx @@ -0,0 +1,111 @@ +/* + * @Date: 2024-09-08 13:59:29 + * @Description: Modify here please + */ +import React, { useContext, useMemo, cloneElement, createRef } from "react"; +import { ConfigContext } from "../config-provider"; +import { useNamespace, KeyCode, composeRef } from "@camelia/core"; +import type { ITooltipProps, ITooltipRef } from "../tooltip"; +import Tooltip from "../tooltip"; + +export interface IPopoverProps extends ITooltipProps { + title?: React.ReactNode | (() => React.ReactNode); + content?: React.ReactNode | (() => React.ReactNode); +} + +interface OverlayProps { + prefixCls?: string; + title?: React.ReactNode; + content?: React.ReactNode; +} + +const getRenderNode = (propValue?: T): React.ReactNode => { + if (!propValue) { + return null; + } + + return typeof propValue === "function" ? propValue() : propValue; +}; + +const Overlay: React.FC = ({ title, content, prefixCls }) => { + if (!title && !content) { + return null; + } + return ( + <> + {title &&
{title}
} + {content &&
{content}
} + + ); +}; + +const Popover = React.forwardRef((props, ref) => { + const { + title, + content, + virtualRef, + overlayClassName, + virtualTriggering, + placement = "top", + effect = "", + trigger = "hover", + children, + destroyTooltipOnHide = false, + overlayStyle = {}, + ...otherProps + } = props; + + const { getPrefixCls } = useContext(ConfigContext); + const ns = useNamespace("popover", getPrefixCls()); + + const internalRef = createRef(); + const PopoverRef = composeRef(ref, internalRef); + + // 动画类名 + const motionName = useMemo(() => { + return otherProps.transitionName || `${getPrefixCls()}-zoom-big`; + }, [otherProps.transitionName]); + + const titleNode = getRenderNode(title); + const contentNode = getRenderNode(content); + + const onKeyDown = (e: React.KeyboardEvent) => { + if (e.keyCode === KeyCode.ESC) { + internalRef.current.onClose(); + } + }; + + const triggerChild = + React.isValidElement(children) && !virtualTriggering + ? cloneElement(children as any, { + onKeyDown: (e: React.KeyboardEvent) => { + if (React.isValidElement(children)) { + children?.props.onKeyDown?.(e); + } + onKeyDown(e); + } + }) + : null; + + return ( + : null} + transitionName={motionName} + > + {triggerChild} + + ); +}); + +export default Popover; diff --git a/packages/components/popover/style/index.scss b/packages/components/popover/style/index.scss new file mode 100644 index 0000000..1722e1a --- /dev/null +++ b/packages/components/popover/style/index.scss @@ -0,0 +1,37 @@ +@use "../../_styles/core/mixins" as *; +@use "../../_styles/common/var" as *; +@use "../../_styles/core/css-var" as *; +@use "../../_styles/core/function" as *; +@use "../../_styles/core/config" as *; + +@include b(popover) { + --box-shadow-secondary: 0 6px 16px 0 rgba(0, 0, 0, 0.08), 0 3px 6px -4px rgba(0, 0, 0, 0.12), + 0 9px 28px 8px rgba(0, 0, 0, 0.05); + + &.#{$namespace}-popper { + border-radius: 0px; + } + .#{$namespace}-popper__content { + border-radius: 10px; + background-clip: padding-box; + background: #fff; + box-shadow: var(--box-shadow-secondary); + padding: 12px; + font-family: getCssVar("font-family"); + } + + .#{$namespace}-popper__arrow::before { + border: 1px solid #e4e7ed; + background: #fff; + } + + @include e(title) { + margin-bottom: 8px; + color: rgba(0, 0, 0, 0.88); + font-weight: 600; + } + + @include e(inner-content) { + color: rgba(0, 0, 0, 0.88); + } +} diff --git a/packages/components/popover/style/index.ts b/packages/components/popover/style/index.ts new file mode 100644 index 0000000..3a1012a --- /dev/null +++ b/packages/components/popover/style/index.ts @@ -0,0 +1,7 @@ +/* + * @Date: 2024-09-08 14:33:32 + * @Description: Modify here please + */ +import "../../_styles/base.scss"; +import "../../tooltip/style"; +import "./index.scss"; diff --git a/packages/components/tooltip/popup-type.ts b/packages/components/tooltip/popup-type.ts index 5ba9131..648543b 100644 --- a/packages/components/tooltip/popup-type.ts +++ b/packages/components/tooltip/popup-type.ts @@ -48,6 +48,6 @@ export interface ITooltipPopupProps { } export interface ITooltipPopupRef { - /** updatePopper */ + /** update Popper */ updatePopper: () => void; } diff --git a/packages/components/tooltip/popup.tsx b/packages/components/tooltip/popup.tsx index 2772c87..ac0f340 100644 --- a/packages/components/tooltip/popup.tsx +++ b/packages/components/tooltip/popup.tsx @@ -69,7 +69,7 @@ const TooltipPopup = React.forwardRef((pro return getParent(props.getPopupContainer, triggerRef.current); }, [props.getPopupContainer, triggerRef.current]); - const contentClass = [props.overlayClassName, ns.b(), ns.is(effect)]; + const contentClass = [ns.b(), ns.is(effect), props.overlayClassName]; useImperativeHandle(ref, () => ({ updatePopper diff --git a/packages/components/tooltip/style/index.scss b/packages/components/tooltip/style/index.scss index 13ea8c5..fed8502 100644 --- a/packages/components/tooltip/style/index.scss +++ b/packages/components/tooltip/style/index.scss @@ -28,12 +28,12 @@ position: absolute; width: 10px; height: 10px; - z-index: -1; + z-index: 1; &:before { position: absolute; width: 10px; height: 10px; - z-index: -1; + z-index: 1; content: " "; transform: rotate(45deg); background: #303133; diff --git a/packages/components/tooltip/tooltip-type.ts b/packages/components/tooltip/tooltip-type.ts index e875e76..7e26793 100644 --- a/packages/components/tooltip/tooltip-type.ts +++ b/packages/components/tooltip/tooltip-type.ts @@ -9,9 +9,11 @@ import type { ITooltipPopupProps } from "./popup-type"; import type { TooltipWrapInjectionContext } from "./utils"; export interface ITooltipProps - extends ITooltipWrapProps, - Omit, - Omit { + extends Omit, + Omit, + Omit { + /** !!! This is a trigger node, which does not need to be passed because it can be passed after the virtual node */ + children?: React.ReactNode; /** 主动控制,不在受trigger的值影响 */ visible?: boolean | null; /** 提示文字节点 */ @@ -22,12 +24,21 @@ export interface ITooltipProps hideAfterTime?: number; /** 出现延迟,以毫秒为单位 */ showAfterTime?: number; + /** 显示隐藏的回调 */ + onOpenChange?: (open: boolean) => void; } export interface ITooltipRef { + /** tooltip Component */ tooltipRef: MutableRefObject; + /** 获取popup的ref */ + popupRef: MutableRefObject; + /** open Popup */ onOpen: (time?: number) => void; + /** close Popup */ onClose: (time?: number) => void; + /** update Popup */ updatePopup: () => void; + /** 验证当前焦点目标是--提示内容中的节点 */ isFocusInsideContent: (event?: FocusEvent) => void; } diff --git a/packages/components/tooltip/tooltip.tsx b/packages/components/tooltip/tooltip.tsx index d43865c..eff95ea 100644 --- a/packages/components/tooltip/tooltip.tsx +++ b/packages/components/tooltip/tooltip.tsx @@ -22,12 +22,12 @@ const Tooltip = React.forwardRef((props, ref) => { role = "tooltip", trigger = "hover", effect = "dark", - destroyTooltipOnHide = false, + destroyTooltipOnHide = true, showArrow = true, placement = "top", strategy = "absolute", disabled, - children, + children: TriggerNode, title, offset, overlay, @@ -101,14 +101,19 @@ const Tooltip = React.forwardRef((props, ref) => { // pop show const onContentShow = () => { - restProps.onShow?.(); - // Start monitoring external clicks - start(); + restProps.onOpenChange?.(true); + /** + * Start monitoring external clicks + * !! if: When actively controlling pop outside, do not turn on monitoring + */ + if (!stopWhenControlledOrDisabled()) { + start(); + } }; // pop hide const onContentHide = () => { - restProps.onHide?.(); + restProps.onOpenChange?.(false); }; const updatePopup = () => { @@ -131,7 +136,7 @@ const Tooltip = React.forwardRef((props, ref) => { * it actively uninstalls events */ useEffect(() => { - if (!open) { + if (!open && !stopWhenControlledOrDisabled()) { stop?.(); } }, [open]); @@ -152,6 +157,8 @@ const Tooltip = React.forwardRef((props, ref) => { useImperativeHandle(ref, () => ({ /** tooltip component */ tooltipRef, + /** 获取popup的ref */ + popupRef: tooltipRef.current?.popupRef, /** open Popup */ onOpen, /** close Popup */ @@ -178,6 +185,8 @@ const Tooltip = React.forwardRef((props, ref) => { { updatePopup(); }} @@ -185,7 +194,7 @@ const Tooltip = React.forwardRef((props, ref) => { onMouseLeave={onMouseleave} onClick={onClick} > - {children} + {TriggerNode} ((props, ref) => { zIndex={restProps.zIndex} transitionName={restProps.transitionName} duration={restProps.duration} - overlayClassName={classNames(restProps.overlayClassName, restProps.internalClassName || ns.b())} + overlayClassName={classNames(restProps.internalClassName || ns.b(), restProps.overlayClassName)} overlayStyle={restProps.overlayStyle} getPopupContainer={restProps.getPopupContainer} onMouseEnter={onMouseenter} diff --git a/packages/components/tooltip/trigger-type.ts b/packages/components/tooltip/trigger-type.ts index af7711c..b6775ee 100644 --- a/packages/components/tooltip/trigger-type.ts +++ b/packages/components/tooltip/trigger-type.ts @@ -3,9 +3,14 @@ * @Description: Modify here please */ export interface ITooltipTriggerProps { - children: React.ReactNode; + /** !!! This is a trigger node, which does not need to be passed because it can be passed after the virtual node */ + children?: React.ReactNode; /** trigger */ trigger?: "hover" | "click"; + /** Used to identify whether virtual triggering is enabled */ + virtualTriggering?: boolean; + /** Identify the triggering elements during virtual triggering */ + virtualRef?: React.MutableRefObject; onMouseEnter?: (e: any) => void; onMouseLeave?: (e: any) => void; onClick?: (e: any) => void; diff --git a/packages/components/tooltip/trigger.tsx b/packages/components/tooltip/trigger.tsx index b0d6263..2df54c7 100644 --- a/packages/components/tooltip/trigger.tsx +++ b/packages/components/tooltip/trigger.tsx @@ -2,7 +2,7 @@ * @Date: 2024-08-25 14:09:59 * @Description: Modify here please */ -import React, { useContext, cloneElement } from "react"; +import React, { useContext, cloneElement, useEffect } from "react"; import ResizeObserver from "rc-resize-observer"; import { TooltipContext } from "./utils"; import type { ITooltipTriggerProps } from "./trigger-type"; @@ -13,10 +13,12 @@ function isFragment(child: React.ReactNode): boolean { } const TooltipRigger: React.FC void; open: boolean }> = (props) => { - const { children, open, onTargetResize, ...restProps } = props; + const { children, open, onTargetResize, virtualRef, virtualTriggering, ...restProps } = props; const { triggerRef } = useContext(TooltipContext); - const child = React.isValidElement(children) && !isFragment(children) ? children : {children}; + const isExistNode = React.isValidElement(children); + + const child = isExistNode && !isFragment(children) ? children : {children}; // Transfer props to cloneProps for use by the target node const passedProps: Record = {}; @@ -31,15 +33,32 @@ const TooltipRigger: React.FC voi }); // Child Node - const triggerNode = cloneElement(child, { - ...passedProps, - ref: triggerRef - }); + const triggerNode = isExistNode + ? cloneElement(child, { + ...passedProps, + ref: triggerRef + }) + : null; + + /** + * Replace trigger, To achieve triggering of different virtual elements + * Unfortunately, it can only actively use “visible” outside to control Popup + */ + useEffect(() => { + // replace trigger + if (virtualRef?.current && virtualTriggering) { + triggerRef.current = virtualRef?.current; + } + }, [virtualRef?.current, virtualTriggering]); return ( - - {triggerNode} - + <> + {isExistNode ? ( + + {triggerNode} + + ) : null} + ); }; diff --git a/packages/shared/use-click-away/index.ts b/packages/shared/use-click-away/index.ts index 7ab33a6..e893f41 100644 --- a/packages/shared/use-click-away/index.ts +++ b/packages/shared/use-click-away/index.ts @@ -40,7 +40,6 @@ export function useClickAway( if (!enable) { return; } - const handler = (event: any) => { const targets = Array.isArray(target) ? target : [target]; if ( From 8b2f01e8f27a706ca713c3eb14dbff37924fb481 Mon Sep 17 00:00:00 2001 From: "PL196\\40380" <403802162@qq.com> Date: Sun, 8 Sep 2024 19:53:49 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/components/tooltip/composables/use-popup.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/components/tooltip/composables/use-popup.ts b/packages/components/tooltip/composables/use-popup.ts index 70a5afc..c253bc0 100644 --- a/packages/components/tooltip/composables/use-popup.ts +++ b/packages/components/tooltip/composables/use-popup.ts @@ -19,7 +19,7 @@ import { useZIndex } from "@camelia/core/hooks"; import { TooltipContext } from "../utils"; import type { ITooltipPopupProps } from "../popup-type"; -export const usePopup = (props: ITooltipPopupProps) => { +export const usePopup = (props: ITooltipPopupProps): any => { const { open, disabled, zIndex, placement, strategy, offset, overlayStyle, showArrow } = props; const { popupRef, triggerRef, role } = useContext(TooltipContext);