+
{letter}
))}
diff --git a/packages/demo/src/pages/home/index.tsx b/packages/demo/src/pages/home/index.tsx
index 3595944..ffc371a 100644
--- a/packages/demo/src/pages/home/index.tsx
+++ b/packages/demo/src/pages/home/index.tsx
@@ -25,7 +25,7 @@ const Home: React.FC = () => {
Pivot Design
{navigatorList.map((nav) => (
-
+
{nav.text}
))}
diff --git a/packages/design/index.ts b/packages/design/index.ts
index 013613e..0cdf80c 100644
--- a/packages/design/index.ts
+++ b/packages/design/index.ts
@@ -15,7 +15,9 @@ import Input from './components/Input';
import Card from './components/Card';
import Skeleton from './components/Skeleton';
import Popover from './components/Popover/';
-export { Button, Icon, Input, Card, Skeleton, Popover };
+import Transition from './components/Transition';
+
+export { Button, Icon, Input, Card, Skeleton, Popover, Transition };
export const arrayMove = (array: any[], from: number, to: number) => {
const resArray = array.slice();
From 4bcc9a81a67be777016ed15545df6d1989348e6c Mon Sep 17 00:00:00 2001
From: huhu-198 <1985989625@qq.com>
Date: Mon, 7 Aug 2023 03:00:00 +0800
Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E8=B0=83=E6=95=B4=E4=BC=98?=
=?UTF-8?q?=E5=8C=96=E6=A0=B7=E5=BC=8F?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
packages/design/components/Card/index.scss | 3 +-
packages/design/components/Popover/index.scss | 1 +
.../design/components/Transition/index.scss | 30 ++
.../design/components/Transition/index.tsx | 302 ++++++++++++++++++
.../design/components/utils/usePrevious.ts | 19 ++
.../components/utils/useUnmountedRef.ts | 15 +
6 files changed, 369 insertions(+), 1 deletion(-)
create mode 100644 packages/design/components/Transition/index.scss
create mode 100644 packages/design/components/Transition/index.tsx
create mode 100644 packages/design/components/utils/usePrevious.ts
create mode 100644 packages/design/components/utils/useUnmountedRef.ts
diff --git a/packages/design/components/Card/index.scss b/packages/design/components/Card/index.scss
index 16a91f4..cef1b7e 100644
--- a/packages/design/components/Card/index.scss
+++ b/packages/design/components/Card/index.scss
@@ -68,7 +68,7 @@ $cardmax: calc(var(--card-maxLength, 100%) * 20px);
font-size: 11px;
line-height: 17px;
text-align: left;
- color: #cbcbcb;
+ color: #333333;
vertical-align: center;
}
}
@@ -102,6 +102,7 @@ $cardmax: calc(var(--card-maxLength, 100%) * 20px);
max-height: $cardmin;
text-align: justify;
position: relative;
+ color: #222222;
&::before {
content: '';
float: right;
diff --git a/packages/design/components/Popover/index.scss b/packages/design/components/Popover/index.scss
index 6b7d166..9c31bcf 100644
--- a/packages/design/components/Popover/index.scss
+++ b/packages/design/components/Popover/index.scss
@@ -1,6 +1,7 @@
@import '../constants.scss';
.#{$prefix}-trigger {
cursor: pointer;
+ color: #000000;
}
.#{$prefix}-popover {
diff --git a/packages/design/components/Transition/index.scss b/packages/design/components/Transition/index.scss
new file mode 100644
index 0000000..9c31bcf
--- /dev/null
+++ b/packages/design/components/Transition/index.scss
@@ -0,0 +1,30 @@
+@import '../constants.scss';
+.#{$prefix}-trigger {
+ cursor: pointer;
+ color: #000000;
+}
+
+.#{$prefix}-popover {
+ position: absolute;
+ max-width: 200px;
+ z-index: 2;
+ color: white;
+ background-color: rgba($color: #000000, $alpha: 0.9);
+ padding: 9px 10px;
+ border-radius: 6px;
+ transition: opacity 0.3s;
+ font-size: 14px;
+
+ ::before {
+ content: '';
+ position: absolute;
+ left: var(--arrowX);
+ top: var(--arrowY);
+ display: var(--arrowShow);
+ width: 10px;
+ height: 10px;
+ transform: rotate(45deg);
+ z-index: -1;
+ background-color: black;
+ }
+}
diff --git a/packages/design/components/Transition/index.tsx b/packages/design/components/Transition/index.tsx
new file mode 100644
index 0000000..4f430f3
--- /dev/null
+++ b/packages/design/components/Transition/index.tsx
@@ -0,0 +1,302 @@
+import React, { useEffect, useRef, useState } from 'react';
+import useUnmountedRef from '../utils/useUnmountedRef';
+import usePrevious from '../utils/usePrevious';
+const TransitionGroupContext = React.createContext(null);
+
+export type timeoutType = number | { enter?: number; exit?: number; appear?: number };
+
+export interface TransitionPropTypes {
+ /**
+ * 用来控制进场、出场状态切换
+ * 默认为 false
+ */
+ in?: boolean;
+ /**
+ * 子组件,是一个函数或者ReactNode,
+ * 如果为函数时其接受参数为刚刚介绍到的entering、entered 、exiting、exited 四个状态值
+ */
+ children?: React.ReactNode | ((status: string) => React.ReactNode);
+ /**
+ * 动画执行时间
+ */
+ timeout: timeoutType;
+ /**
+ * 首次挂载是是否展示动画
+ */
+ appear?: boolean;
+ /**
+ * 是否展示进场动画
+ */
+ enterAnimation?: boolean;
+ /**
+ * 是否展示出场动画
+ */
+ exitAnimation?: boolean;
+ /**
+ * exit状态时是否卸载组件
+ */
+ unmountOnExit?: boolean;
+ /**
+ * 初始化时是否卸载组件
+ */
+ mountOnEnter?: boolean;
+ /**
+ * 进场动画执行前调用
+ */
+ onEnter?: (node?: Element, isAppearing?: boolean) => void;
+ /**
+ * 进场动画执行中调用
+ */
+ onEntering?: (node?: Element, isAppearing?: boolean) => void;
+ /**
+ * 进场动画执行完毕调用
+ */
+ onEntered?: (node?: Element, isAppearing?: boolean) => void;
+ /**
+ * 退场动画开始执行时调用
+ */
+ onExit?: (node?: Element) => void;
+ /**
+ * 退场动画执行中时调用
+ */
+ onExiting?: (node?: Element) => void;
+ /**
+ * 退场动画执行完毕调用
+ */
+ onExited?: (node?: Element) => void;
+}
+
+export enum StatusEnum {
+ UNMOUNTED = 'unmounted',
+ EXITED = 'exited',
+ ENTERING = 'entering',
+ ENTERED = 'entered',
+ EXITING = 'exiting',
+}
+
+const Transition: React.FC = ({
+ children,
+ onEnter = () => {},
+ onEntering = () => {},
+ onEntered = () => {},
+ onExit = () => {},
+ onExiting = () => {},
+ onExited = () => {},
+ in: _in = true, // 获取外部传入的in,代表组件是否展示
+ timeout: preTimeout,
+ appear = false, // 第一次加载时,是否需要进场动画
+ enterAnimation = true,
+ exitAnimation = true,
+ mountOnEnter = false,
+ unmountOnExit = false,
+ ...childProps
+}) => {
+ // 获取上下文
+ const initialStatus = useRef(null); // 初始状态
+ const appearStatus = useRef(null); // 初始状态
+
+ // const { initialStatus, appearStatus } = getInitStatus(_in, appear);
+
+ const [status, setStatus] = useState(initStatus()); // 设置初始化状态
+
+ const unmountedRef = useUnmountedRef(); // 判断当前组件是否已被卸载
+ const inRef = useRef(null); // 用于props的in
+ const isMountingRef = useRef(false); // 用于props的in
+
+ const nextStatusRef = useRef(null); // 预设下一个状态,便于取消之前已失效的safeSetStatus
+
+ const preStatus = usePrevious(status);
+
+ const timeouts = getFormateTimeouts(preTimeout); // 规范化后的时间,包括进场时间、退场时间、首次挂载进场时间
+
+ // 控制 Transition 的子组件在首次挂载的时候是否执行进场动画
+ useEffect(() => {
+ let isMounting = false;
+ if (initialStatus.current === StatusEnum.EXITED && appearStatus.current === StatusEnum.ENTERING) {
+ isMounting = appear;
+ isMountingRef.current = appear;
+ }
+ updateStatus(appearStatus.current, isMounting);
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, []);
+
+ // in 变化时组件状态
+ useEffect(() => {
+ // 初次挂载时,不执行
+ if (isMountingRef.current) {
+ return;
+ }
+
+ if (status === StatusEnum.UNMOUNTED) {
+ if (_in) {
+ setStatus(StatusEnum.EXITED); // 从卸载 unmounted 转为 exits
+ }
+ return;
+ }
+
+ // 状态为exited且需要卸载
+ if (
+ inRef.current === _in &&
+ status === StatusEnum.EXITED &&
+ (preStatus === StatusEnum.ENTERED || preStatus === StatusEnum.EXITING) &&
+ unmountOnExit
+ ) {
+ setStatus(StatusEnum.UNMOUNTED);
+ return;
+ }
+
+ let nextStatus = null; // status 下一个要变化的状态
+
+ // 若in的值改变
+ if (inRef.current !== _in) {
+ if (_in) {
+ // 变为「加载中」,执行加载动画
+ if (status !== StatusEnum.ENTERING && status !== StatusEnum.ENTERED) {
+ nextStatus = StatusEnum.ENTERING;
+ }
+ } else {
+ // 变为「退场中」,执行加载动画
+ if (status === StatusEnum.ENTERING || status === StatusEnum.ENTERED) {
+ nextStatus = StatusEnum.EXITING;
+ }
+ }
+ }
+ inRef.current = _in;
+ updateStatus(nextStatus); // 更新status状态,若为null可能是要卸载
+ }, [_in, status]);
+
+ function initStatus(): StatusEnum | null {
+ let _initialStatus = StatusEnum.EXITED; // 初始状态,默认 exited
+ let _appearStatus = null; // 首次出现的状态,即 initialStatus 需要切换的下一个状态
+ if (_in) {
+ if (appear) {
+ // 首次挂载需要进场动画
+ _initialStatus = StatusEnum.EXITED;
+ _appearStatus = StatusEnum.ENTERING;
+ } else {
+ _initialStatus = StatusEnum.ENTERED;
+ }
+ } else {
+ if (unmountOnExit || mountOnEnter) {
+ _initialStatus = StatusEnum.UNMOUNTED; // 初始时未挂载
+ } else {
+ _initialStatus = StatusEnum.EXITED;
+ }
+ }
+ appearStatus.current = _appearStatus;
+ initialStatus.current = _initialStatus;
+
+ return _initialStatus;
+ }
+
+ /** 更新 Status 和动画 */
+ const updateStatus = (nextStatus: StatusEnum | null, isMounting = false) => {
+ if (nextStatus !== null) {
+ if (nextStatus === StatusEnum.ENTERING) {
+ performEnter(isMounting); // 执行进场
+ } else if (nextStatus === StatusEnum.EXITING) {
+ performExit(); // 执行退场
+ }
+ }
+ };
+
+ /** 设置status并执行回调,确保在组件卸载后或组件状态变更后不会执行回调 */
+ const safeSetStatus = (newStatus: StatusEnum | null, callback?: { (): void }) => {
+ nextStatusRef.current = newStatus; // 先预设下一个状态,保证每次执行的callback都是最新的
+ setStatus(newStatus);
+
+ // 如果组件未被卸载
+ if (unmountedRef && callback) {
+ callback();
+ }
+ };
+
+ /** 进场动画 isMounting表示是否为首次挂载 */
+ const performEnter = (isMounting = false) => {
+ const enterTimeout = isMounting && appear ? timeouts.appear : timeouts.enter;
+
+ onEnter?.();
+ if (!enterAnimation) {
+ // 不执行进场动画,直接跳转到entered状态
+ safeSetStatus(StatusEnum.ENTERED, () => {
+ onEntered?.();
+ if (isMounting) isMountingRef.current = false;
+ });
+ return;
+ }
+
+ // 先更新状态为ENTERING,然后在指定时间后更新状态为 StatusEnum.ENTERED
+ safeSetStatus(StatusEnum.ENTERING, () => {
+ onEntering?.();
+ onTransitionEnd(enterTimeout, StatusEnum.ENTERING, () => {
+ safeSetStatus(StatusEnum.ENTERED, () => {
+ onEntered?.();
+ if (isMounting) isMountingRef.current = false;
+ });
+ });
+ });
+ };
+
+ /** 执行退场相关操作 */
+ const performExit = () => {
+ onExit?.();
+
+ if (!exitAnimation) {
+ // 不执行出场动画,直接跳转到exited状态
+ safeSetStatus(StatusEnum.EXITED, () => {
+ onExited?.();
+ if (unmountOnExit) updateStatus(null); // 卸载组件
+ });
+ return;
+ }
+
+ // 先更新状态为EXITING、然后在指定时间后将状态更新为 StatusEnum.EXITED
+ safeSetStatus(StatusEnum.EXITING, () => {
+ onExiting?.();
+ onTransitionEnd(timeouts.exit, StatusEnum.EXITING, () => {
+ safeSetStatus(StatusEnum.EXITED, () => {
+ onExited?.();
+ if (unmountOnExit) updateStatus(null); // 卸载组件
+ });
+ });
+ });
+ };
+
+ /** 在指定时间后执行callback */
+ const onTransitionEnd = (timeout: number | null, nextStatus: StatusEnum, callback: any) => {
+ if (timeout !== null && !unmountedRef.current) {
+ setTimeout(() => {
+ if (nextStatus !== nextStatusRef.current) return; // 如果状态已经改变,不执行callback
+ callback();
+ }, timeout);
+ }
+ };
+
+ /** 统一转化timeout格式 */
+ function getFormateTimeouts(timeout: timeoutType | null) {
+ let exit, enter, appear;
+ exit = enter = appear = timeout;
+
+ if (timeout != null && typeof timeout !== 'number') {
+ exit = timeout?.exit;
+ enter = timeout?.enter;
+ appear = timeout?.appear !== undefined ? timeout?.appear : enter;
+ }
+ return { exit, enter, appear } as { enter: number; exit: number; appear: number };
+ }
+
+ // 卸载状态
+ if (status === StatusEnum.UNMOUNTED) {
+ return null;
+ }
+
+ return (
+
+ {typeof children === 'function'
+ ? children(status as StatusEnum)
+ : React.cloneElement(React.Children.only(children) as React.ReactElement, childProps)}
+
+ );
+};
+
+export default Transition;
diff --git a/packages/design/components/utils/usePrevious.ts b/packages/design/components/utils/usePrevious.ts
new file mode 100644
index 0000000..d5dce84
--- /dev/null
+++ b/packages/design/components/utils/usePrevious.ts
@@ -0,0 +1,19 @@
+import { useRef } from 'react';
+
+export type ShouldUpdateFunc = (prev: T | undefined, next: T) => boolean;
+
+const defaultShouldUpdate = (a?: T, b?: T) => !Object.is(a, b);
+
+function usePrevious(state: T, shouldUpdate: ShouldUpdateFunc = defaultShouldUpdate): T | undefined {
+ const prevRef = useRef();
+ const curRef = useRef();
+
+ if (shouldUpdate(curRef.current, state)) {
+ prevRef.current = curRef.current;
+ curRef.current = state;
+ }
+
+ return prevRef.current;
+}
+
+export default usePrevious;
diff --git a/packages/design/components/utils/useUnmountedRef.ts b/packages/design/components/utils/useUnmountedRef.ts
new file mode 100644
index 0000000..e9cd13b
--- /dev/null
+++ b/packages/design/components/utils/useUnmountedRef.ts
@@ -0,0 +1,15 @@
+import { useEffect, useRef } from 'react';
+
+/* 获取当前组件是否还挂载的ref */
+const useUnmountedRef = () => {
+ const unmountedRef = useRef(false);
+ useEffect(() => {
+ unmountedRef.current = false; // 默认为 true
+ return () => {
+ unmountedRef.current = true; // 组件被卸载后,返回 false
+ };
+ }, []);
+ return unmountedRef;
+};
+
+export default useUnmountedRef;