@codehz/auto-transition 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +84 -0
- package/dist/index.d.mts +111 -0
- package/dist/index.mjs +207 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 codehz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# @codehz/auto-transition
|
|
2
|
+
|
|
3
|
+
一个轻量级的 React 组件库,旨在为容器内的子元素提供自动的**进入 (Enter)**、**退出 (Exit)** 和**移动 (Move)** 动画。
|
|
4
|
+
|
|
5
|
+
它通过拦截底层的 DOM 操作(如 `appendChild`、`removeChild`)来实现动画,无需开发者手动管理复杂的动画状态。
|
|
6
|
+
|
|
7
|
+
## 主要功能
|
|
8
|
+
|
|
9
|
+
- **全自动动画**:自动识别子元素的添加、删除和位置变化并应用动画。
|
|
10
|
+
- **高性能**:基于原生 Web Animations API 实现,确保流畅的 160fps 体验。
|
|
11
|
+
- **布局感知**:自动计算元素在容器内的相对位置,支持平滑的位移和缩放过渡。
|
|
12
|
+
- **高度可定制**:支持通过插件系统自定义动画效果。
|
|
13
|
+
- **无侵入性**:支持 `asChild` 属性(类似 Radix UI),可无缝集成到现有布局中。
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
该项目依赖于 React 19+。
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install @codehz/auto-transition
|
|
21
|
+
# 或者使用 bun
|
|
22
|
+
bun add @codehz/auto-transition
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## 快速上手
|
|
26
|
+
|
|
27
|
+
只需将需要动画的列表或元素包裹在 `AutoTransition` 中即可:
|
|
28
|
+
|
|
29
|
+
```tsx
|
|
30
|
+
import { AutoTransition } from "@codehz/auto-transition";
|
|
31
|
+
import { useState } from "react";
|
|
32
|
+
|
|
33
|
+
function ListExample() {
|
|
34
|
+
const [items, setItems] = useState([1, 2, 3]);
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<AutoTransition as="ul" className="grid gap-2">
|
|
38
|
+
{items.map((id) => (
|
|
39
|
+
<li key={id} onClick={() => setItems(items.filter((i) => i !== id))}>
|
|
40
|
+
项目 {id} (点击删除)
|
|
41
|
+
</li>
|
|
42
|
+
))}
|
|
43
|
+
<button onClick={() => setItems([...items, Date.now()])}>添加项目</button>
|
|
44
|
+
</AutoTransition>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## API 参考
|
|
50
|
+
|
|
51
|
+
### `AutoTransition` 组件 Props
|
|
52
|
+
|
|
53
|
+
| 属性 | 类型 | 默认值 | 说明 |
|
|
54
|
+
| :--------- | :----------------- | :----------- | :-------------------------------------------------- |
|
|
55
|
+
| `as` | `ElementType` | `div` | 容器渲染成的 HTML 标签或组件。 |
|
|
56
|
+
| `asChild` | `boolean` | `false` | 是否使用 `Slot` 模式,将 props 转发给唯一的子元素。 |
|
|
57
|
+
| `plugin` | `TransitionPlugin` | 内置默认动画 | 用于自定义进入、退出和移动动画的插件对象。 |
|
|
58
|
+
| `children` | `ReactNode` | - | 需要应用动画的子元素。 |
|
|
59
|
+
| `ref` | `Ref<HTMLElement>` | - | 转发给容器 DOM 元素的引用。 |
|
|
60
|
+
|
|
61
|
+
### `TransitionPlugin` 接口
|
|
62
|
+
|
|
63
|
+
你可以通过实现此接口来自定义动画:
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
export type TransitionPlugin = {
|
|
67
|
+
// 元素插入容器时触发
|
|
68
|
+
enter?(el: Element): Animation;
|
|
69
|
+
// 元素从容器移除时触发,rect 为移除时的位置大小
|
|
70
|
+
exit?(el: Element, rect: Rect): Animation;
|
|
71
|
+
// 元素在容器内位置或大小变化时触发
|
|
72
|
+
move?(el: Element, current: Rect, previous: Rect): Animation;
|
|
73
|
+
};
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 默认动画行为
|
|
77
|
+
|
|
78
|
+
- **Enter**: 透明度从 0 到 1 (250ms ease-out)。
|
|
79
|
+
- **Exit**: 保持当前位置和大小,透明度从 1 到 0 (250ms ease-in),动画结束后从 DOM 移除。
|
|
80
|
+
- **Move**: 使用 FLIP 进行位移和缩放补偿,实现平滑的布局切换 (250ms ease-in)。
|
|
81
|
+
|
|
82
|
+
## 许可证
|
|
83
|
+
|
|
84
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { ComponentPropsWithoutRef, ElementType, ForwardedRef, ReactElement, ReactNode } from "react";
|
|
2
|
+
import * as react_jsx_runtime0 from "react/jsx-runtime";
|
|
3
|
+
|
|
4
|
+
//#region src/AutoTransition.d.ts
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A rectangle describing an element's position and size relative to the measured
|
|
8
|
+
* parent used by `AutoTransition` for layout calculations.
|
|
9
|
+
*
|
|
10
|
+
* - `x`/`y` are the left/top offsets relative to the measurement parent's content box.
|
|
11
|
+
* - `width`/`height` are the element's layout size in pixels.
|
|
12
|
+
*/
|
|
13
|
+
type Rect = {
|
|
14
|
+
x: number;
|
|
15
|
+
y: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Simple size pair used for resize transitions (width/height in pixels).
|
|
21
|
+
*/
|
|
22
|
+
type Dimensions = {
|
|
23
|
+
width: number;
|
|
24
|
+
height: number;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* Common props for `AutoTransition`.
|
|
28
|
+
*
|
|
29
|
+
* @template T - Element type to render as (e.g., "div", "ul").
|
|
30
|
+
*/
|
|
31
|
+
type AutoTransitionBaseProps<T extends ElementType | undefined> = {
|
|
32
|
+
as?: T;
|
|
33
|
+
transition?: TransitionPlugin;
|
|
34
|
+
ref?: ForwardedRef<HTMLElement>;
|
|
35
|
+
};
|
|
36
|
+
type AutoTransitionProps<T extends ElementType | undefined> = T extends ElementType ? AutoTransitionBaseProps<T> & Omit<ComponentPropsWithoutRef<T>, keyof AutoTransitionBaseProps<T>> & {
|
|
37
|
+
children?: ReactNode;
|
|
38
|
+
} : AutoTransitionBaseProps<T> & {
|
|
39
|
+
children: ReactElement;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* AutoTransition
|
|
43
|
+
*
|
|
44
|
+
* A small container component that provides automatic enter/exit/move
|
|
45
|
+
* animations for its child `Element` nodes. The component intercepts
|
|
46
|
+
* low-level DOM operations (`appendChild`, `insertBefore`, `removeChild`)
|
|
47
|
+
* performed on the container and plays animations (via the Web Animations
|
|
48
|
+
* API) before applying DOM changes such as removing an element.
|
|
49
|
+
*
|
|
50
|
+
* If a `transition` plugin is not provided, AutoTransition applies its
|
|
51
|
+
* default animations:
|
|
52
|
+
* - enter: fade in (opacity 0 -> 1), 250ms ease-out
|
|
53
|
+
* - exit: keep element size and position while fading out, 250ms ease-in
|
|
54
|
+
* - move: translate + scale from previous rect to new rect, 250ms ease-in
|
|
55
|
+
*
|
|
56
|
+
* Notes:
|
|
57
|
+
* - This component is client-only (relies on DOM measurement & Web Animations API).
|
|
58
|
+
* - It only animates `Element` nodes; text nodes use native DOM operations.
|
|
59
|
+
* - In exit path, the provided animation's finish triggers removal from the DOM.
|
|
60
|
+
*
|
|
61
|
+
* Example usage:
|
|
62
|
+
* ```tsx
|
|
63
|
+
* <AutoTransition as="div" className="grid gap-2">
|
|
64
|
+
* {items.map((it) => (
|
|
65
|
+
* <Card key={it.id}>{it.title}</Card>
|
|
66
|
+
* ))}
|
|
67
|
+
* </AutoTransition>
|
|
68
|
+
*
|
|
69
|
+
* // with custom transition plugin
|
|
70
|
+
* <AutoTransition transition={FloatingPanelTransition} as="div">
|
|
71
|
+
* {isOpen && <PanelContent/>}
|
|
72
|
+
* </AutoTransition>
|
|
73
|
+
* ```
|
|
74
|
+
*
|
|
75
|
+
* @template T - Element type to render as (e.g. "div")
|
|
76
|
+
* @param props - props as defined by `AutoTransitionProps<T>`
|
|
77
|
+
*/
|
|
78
|
+
declare function AutoTransition<T extends ElementType | undefined>({
|
|
79
|
+
as,
|
|
80
|
+
children,
|
|
81
|
+
transition,
|
|
82
|
+
ref: externalRef,
|
|
83
|
+
...rest
|
|
84
|
+
}: AutoTransitionProps<T>): react_jsx_runtime0.JSX.Element;
|
|
85
|
+
/**
|
|
86
|
+
* A plugin interface to provide custom animations for enter/exit/move/resize.
|
|
87
|
+
* Implementations should return a Web Animations API `Animation` instance.
|
|
88
|
+
*
|
|
89
|
+
* - `enter` receives the element being inserted.
|
|
90
|
+
* - `exit` receives the element being removed and its last-known rectangle
|
|
91
|
+
* relative to the measurement parent — useful for leaving the element in
|
|
92
|
+
* place while animating out.
|
|
93
|
+
* - `move` receives the element that's moved and both current/previous rects
|
|
94
|
+
* to allow translation/scale-based transitions.
|
|
95
|
+
* - `resize` receives the element and previous/new dimensions — note: the
|
|
96
|
+
* current component implementation doesn't automatically call `resize`,
|
|
97
|
+
* but implementations may document this hook for future use.
|
|
98
|
+
*/
|
|
99
|
+
type TransitionPlugin = {
|
|
100
|
+
/** Play when an element enters/was inserted into the container. */
|
|
101
|
+
enter?(el: Element): Animation;
|
|
102
|
+
/** Play when an element is removed; `rect` is the element's rect at removal time. */
|
|
103
|
+
exit?(el: Element, rect: Rect): Animation;
|
|
104
|
+
/** Play when an element moves within the container (position or size changes). */
|
|
105
|
+
move?(el: Element, current: Rect, previous: Rect): Animation;
|
|
106
|
+
/** Play when element is resized; not invoked by current implementation. */
|
|
107
|
+
resize?(el: Element, current: Dimensions, previous: Dimensions): Animation;
|
|
108
|
+
};
|
|
109
|
+
//#endregion
|
|
110
|
+
export { AutoTransition, Dimensions, Rect, TransitionPlugin };
|
|
111
|
+
//# sourceMappingURL=index.d.mts.map
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { Slot } from "@radix-ui/react-slot";
|
|
2
|
+
import { startTransition, useCallback, useEffect, useRef } from "react";
|
|
3
|
+
import { jsx } from "react/jsx-runtime";
|
|
4
|
+
|
|
5
|
+
//#region src/microcache.ts
|
|
6
|
+
function microcache(create, cleanup) {
|
|
7
|
+
let cache;
|
|
8
|
+
return (...args) => {
|
|
9
|
+
if (cache) return cache;
|
|
10
|
+
cache = create(...args);
|
|
11
|
+
startTransition(() => {
|
|
12
|
+
const old = cache;
|
|
13
|
+
cache = void 0;
|
|
14
|
+
cleanup?.(old);
|
|
15
|
+
});
|
|
16
|
+
return cache;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
//#endregion
|
|
21
|
+
//#region src/useForkRef.ts
|
|
22
|
+
function useForkRef(...refs) {
|
|
23
|
+
return useCallback((node) => {
|
|
24
|
+
refs.forEach((ref) => {
|
|
25
|
+
if (ref) {
|
|
26
|
+
if (typeof ref === "function") ref(node);
|
|
27
|
+
else if (ref.current !== void 0) ref.current = node;
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
}, refs);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
//#endregion
|
|
34
|
+
//#region src/AutoTransition.tsx
|
|
35
|
+
/**
|
|
36
|
+
* AutoTransition
|
|
37
|
+
*
|
|
38
|
+
* A small container component that provides automatic enter/exit/move
|
|
39
|
+
* animations for its child `Element` nodes. The component intercepts
|
|
40
|
+
* low-level DOM operations (`appendChild`, `insertBefore`, `removeChild`)
|
|
41
|
+
* performed on the container and plays animations (via the Web Animations
|
|
42
|
+
* API) before applying DOM changes such as removing an element.
|
|
43
|
+
*
|
|
44
|
+
* If a `transition` plugin is not provided, AutoTransition applies its
|
|
45
|
+
* default animations:
|
|
46
|
+
* - enter: fade in (opacity 0 -> 1), 250ms ease-out
|
|
47
|
+
* - exit: keep element size and position while fading out, 250ms ease-in
|
|
48
|
+
* - move: translate + scale from previous rect to new rect, 250ms ease-in
|
|
49
|
+
*
|
|
50
|
+
* Notes:
|
|
51
|
+
* - This component is client-only (relies on DOM measurement & Web Animations API).
|
|
52
|
+
* - It only animates `Element` nodes; text nodes use native DOM operations.
|
|
53
|
+
* - In exit path, the provided animation's finish triggers removal from the DOM.
|
|
54
|
+
*
|
|
55
|
+
* Example usage:
|
|
56
|
+
* ```tsx
|
|
57
|
+
* <AutoTransition as="div" className="grid gap-2">
|
|
58
|
+
* {items.map((it) => (
|
|
59
|
+
* <Card key={it.id}>{it.title}</Card>
|
|
60
|
+
* ))}
|
|
61
|
+
* </AutoTransition>
|
|
62
|
+
*
|
|
63
|
+
* // with custom transition plugin
|
|
64
|
+
* <AutoTransition transition={FloatingPanelTransition} as="div">
|
|
65
|
+
* {isOpen && <PanelContent/>}
|
|
66
|
+
* </AutoTransition>
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @template T - Element type to render as (e.g. "div")
|
|
70
|
+
* @param props - props as defined by `AutoTransitionProps<T>`
|
|
71
|
+
*/
|
|
72
|
+
function AutoTransition({ as, children, transition, ref: externalRef, ...rest }) {
|
|
73
|
+
const Component = as ?? Slot;
|
|
74
|
+
const ref = useRef(null);
|
|
75
|
+
useEffect(() => {
|
|
76
|
+
const removed = /* @__PURE__ */ new Set();
|
|
77
|
+
const target = ref.current;
|
|
78
|
+
let measureTarget = target;
|
|
79
|
+
let styles = getComputedStyle(measureTarget);
|
|
80
|
+
while (styles.display === "contents" || styles.position === "static" && measureTarget !== document.body) {
|
|
81
|
+
measureTarget = measureTarget.parentElement;
|
|
82
|
+
styles = getComputedStyle(measureTarget);
|
|
83
|
+
}
|
|
84
|
+
const parentRect = microcache(() => {
|
|
85
|
+
const borderBox = measureTarget.getBoundingClientRect();
|
|
86
|
+
return {
|
|
87
|
+
left: borderBox.left + parseFloat(styles.borderLeftWidth || "0"),
|
|
88
|
+
top: borderBox.top + parseFloat(styles.borderTopWidth || "0")
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
const snapshot = microcache(() => {
|
|
92
|
+
const parent = parentRect();
|
|
93
|
+
const result = /* @__PURE__ */ new Map();
|
|
94
|
+
for (const child of target.children) if (child instanceof Element) {
|
|
95
|
+
const rect = child.getBoundingClientRect();
|
|
96
|
+
result.set(child, {
|
|
97
|
+
x: rect.left - parent.left,
|
|
98
|
+
y: rect.top - parent.top,
|
|
99
|
+
width: rect.width,
|
|
100
|
+
height: rect.height
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}, (old) => {
|
|
105
|
+
for (const child of target.children) if (child instanceof Element) {
|
|
106
|
+
if (removed.has(child)) continue;
|
|
107
|
+
const rect = getRelativePosition(child);
|
|
108
|
+
const oldRect = old.get(child);
|
|
109
|
+
if (!oldRect) continue;
|
|
110
|
+
if (rect.x !== oldRect.x || rect.y !== oldRect.y || rect.width !== oldRect.width || rect.height !== oldRect.height) animateNodeMove(child, rect, oldRect);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
target.removeChild = function removeChild(node) {
|
|
114
|
+
if (node instanceof Element) {
|
|
115
|
+
if (removed.has(node)) return node;
|
|
116
|
+
removed.add(node);
|
|
117
|
+
animateNodeExit(node, snapshot().get(node) ?? getRelativePosition(node));
|
|
118
|
+
return node;
|
|
119
|
+
}
|
|
120
|
+
return Element.prototype.removeChild.call(this, node);
|
|
121
|
+
};
|
|
122
|
+
target.insertBefore = function insertBefore(node, child) {
|
|
123
|
+
snapshot();
|
|
124
|
+
if (!(node instanceof Element)) return Element.prototype.insertBefore.call(this, node, child);
|
|
125
|
+
Element.prototype.insertBefore.call(this, node, child);
|
|
126
|
+
animateNodeEnter(node);
|
|
127
|
+
return node;
|
|
128
|
+
};
|
|
129
|
+
target.appendChild = function appendChild(node) {
|
|
130
|
+
snapshot();
|
|
131
|
+
if (!(node instanceof Element)) return Element.prototype.appendChild.call(this, node);
|
|
132
|
+
Element.prototype.appendChild.call(this, node);
|
|
133
|
+
animateNodeEnter(node);
|
|
134
|
+
return node;
|
|
135
|
+
};
|
|
136
|
+
return () => {
|
|
137
|
+
target.removeChild = Element.prototype.removeChild;
|
|
138
|
+
target.insertBefore = Element.prototype.insertBefore;
|
|
139
|
+
target.appendChild = Element.prototype.appendChild;
|
|
140
|
+
};
|
|
141
|
+
function animateNodeExit(node, rect) {
|
|
142
|
+
let animation;
|
|
143
|
+
if (transition?.exit) animation = transition.exit(node, rect);
|
|
144
|
+
else {
|
|
145
|
+
const width = `${rect.width}px`;
|
|
146
|
+
const height = `${rect.height}px`;
|
|
147
|
+
const translate = `translate(${rect.x}px, ${rect.y}px)`;
|
|
148
|
+
animation = node.animate({
|
|
149
|
+
position: ["absolute", "absolute"],
|
|
150
|
+
opacity: [1, 0],
|
|
151
|
+
top: ["0", "0"],
|
|
152
|
+
left: ["0", "0"],
|
|
153
|
+
transform: [translate, translate],
|
|
154
|
+
width: [width, width],
|
|
155
|
+
height: [height, height],
|
|
156
|
+
margin: ["0", "0"]
|
|
157
|
+
}, {
|
|
158
|
+
duration: 250,
|
|
159
|
+
easing: "ease-in"
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
animation.finished.then(() => node.remove());
|
|
163
|
+
return animation;
|
|
164
|
+
}
|
|
165
|
+
function animateNodeEnter(node) {
|
|
166
|
+
if (transition?.enter) transition.enter(node);
|
|
167
|
+
else node.animate({ opacity: [0, 1] }, {
|
|
168
|
+
duration: 250,
|
|
169
|
+
easing: "ease-out"
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
function animateNodeMove(node, rect, oldRect) {
|
|
173
|
+
if (transition?.move) transition.move(node, rect, oldRect);
|
|
174
|
+
else {
|
|
175
|
+
const dx = oldRect.x - rect.x;
|
|
176
|
+
const dy = oldRect.y - rect.y;
|
|
177
|
+
const sx = oldRect.width / rect.width;
|
|
178
|
+
const sy = oldRect.height / rect.height;
|
|
179
|
+
node.animate({
|
|
180
|
+
transformOrigin: ["0 0", "0 0"],
|
|
181
|
+
transform: [`translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`, `translate(0, 0) scale(1, 1)`]
|
|
182
|
+
}, {
|
|
183
|
+
duration: 250,
|
|
184
|
+
easing: "ease-in"
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
function getRelativePosition(node, parent = parentRect()) {
|
|
189
|
+
const rect = node.getBoundingClientRect();
|
|
190
|
+
return {
|
|
191
|
+
x: rect.left - parent.left,
|
|
192
|
+
y: rect.top - parent.top,
|
|
193
|
+
width: rect.width,
|
|
194
|
+
height: rect.height
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
}, [transition]);
|
|
198
|
+
return /* @__PURE__ */ jsx(Component, {
|
|
199
|
+
ref: useForkRef(ref, externalRef),
|
|
200
|
+
...rest,
|
|
201
|
+
children
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
//#endregion
|
|
206
|
+
export { AutoTransition };
|
|
207
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":["cache: T | undefined","animation: Animation"],"sources":["../src/microcache.ts","../src/useForkRef.ts","../src/AutoTransition.tsx"],"sourcesContent":["import { startTransition } from \"react\";\n\nexport function microcache<T, Args extends unknown[]>(create: (...args: Args) => T, cleanup?: (old: T) => void) {\n let cache: T | undefined;\n return (...args: Args) => {\n if (cache) {\n return cache;\n }\n cache = create(...args);\n startTransition(() => {\n const old = cache!;\n cache = undefined;\n cleanup?.(old);\n });\n return cache;\n };\n}\n","import { useCallback, type Ref } from \"react\";\n\nexport function useForkRef<T>(...refs: (Ref<T> | null | undefined)[]): (node: T | null) => void {\n return useCallback((node: T | null) => {\n refs.forEach((ref) => {\n if (ref) {\n if (typeof ref === \"function\") {\n ref(node);\n } else if (ref.current !== undefined) {\n ref.current = node;\n }\n }\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, refs);\n}\n","import { Slot } from \"@radix-ui/react-slot\";\nimport {\n useEffect,\n useRef,\n type ComponentPropsWithoutRef,\n type ElementType,\n type ForwardedRef,\n type ReactElement,\n type ReactNode,\n} from \"react\";\nimport { microcache } from \"./microcache.ts\";\nimport { useForkRef } from \"./useForkRef.ts\";\n\n/**\n * A rectangle describing an element's position and size relative to the measured\n * parent used by `AutoTransition` for layout calculations.\n *\n * - `x`/`y` are the left/top offsets relative to the measurement parent's content box.\n * - `width`/`height` are the element's layout size in pixels.\n */\nexport type Rect = {\n x: number;\n y: number;\n width: number;\n height: number;\n};\n\n/**\n * Simple size pair used for resize transitions (width/height in pixels).\n */\nexport type Dimensions = {\n width: number;\n height: number;\n};\n\n/**\n * Common props for `AutoTransition`.\n *\n * @template T - Element type to render as (e.g., \"div\", \"ul\").\n */\ntype AutoTransitionBaseProps<T extends ElementType | undefined> = {\n as?: T;\n transition?: TransitionPlugin;\n ref?: ForwardedRef<HTMLElement>;\n};\n\ntype AutoTransitionProps<T extends ElementType | undefined> = T extends ElementType\n ? AutoTransitionBaseProps<T> &\n Omit<ComponentPropsWithoutRef<T>, keyof AutoTransitionBaseProps<T>> & {\n children?: ReactNode;\n }\n : AutoTransitionBaseProps<T> & {\n children: ReactElement;\n };\n\n/**\n * AutoTransition\n *\n * A small container component that provides automatic enter/exit/move\n * animations for its child `Element` nodes. The component intercepts\n * low-level DOM operations (`appendChild`, `insertBefore`, `removeChild`)\n * performed on the container and plays animations (via the Web Animations\n * API) before applying DOM changes such as removing an element.\n *\n * If a `transition` plugin is not provided, AutoTransition applies its\n * default animations:\n * - enter: fade in (opacity 0 -> 1), 250ms ease-out\n * - exit: keep element size and position while fading out, 250ms ease-in\n * - move: translate + scale from previous rect to new rect, 250ms ease-in\n *\n * Notes:\n * - This component is client-only (relies on DOM measurement & Web Animations API).\n * - It only animates `Element` nodes; text nodes use native DOM operations.\n * - In exit path, the provided animation's finish triggers removal from the DOM.\n *\n * Example usage:\n * ```tsx\n * <AutoTransition as=\"div\" className=\"grid gap-2\">\n * {items.map((it) => (\n * <Card key={it.id}>{it.title}</Card>\n * ))}\n * </AutoTransition>\n *\n * // with custom transition plugin\n * <AutoTransition transition={FloatingPanelTransition} as=\"div\">\n * {isOpen && <PanelContent/>}\n * </AutoTransition>\n * ```\n *\n * @template T - Element type to render as (e.g. \"div\")\n * @param props - props as defined by `AutoTransitionProps<T>`\n */\nexport function AutoTransition<T extends ElementType | undefined>({\n as,\n children,\n transition,\n ref: externalRef,\n ...rest\n}: AutoTransitionProps<T>) {\n const Component = as ?? Slot;\n const ref = useRef<HTMLElement>(null);\n useEffect(() => {\n const removed = new Set<Element>();\n const target = ref.current!;\n let measureTarget = target;\n let styles = getComputedStyle(measureTarget);\n while (styles.display === \"contents\" || (styles.position === \"static\" && measureTarget !== document.body)) {\n measureTarget = measureTarget.parentElement!;\n styles = getComputedStyle(measureTarget);\n }\n const parentRect = microcache(() => {\n const borderBox = measureTarget.getBoundingClientRect();\n return {\n left: borderBox.left + parseFloat(styles.borderLeftWidth || \"0\"),\n top: borderBox.top + parseFloat(styles.borderTopWidth || \"0\"),\n };\n });\n const snapshot = microcache(\n () => {\n const parent = parentRect();\n const result = new Map<Element, Rect>();\n for (const child of target.children) {\n if (child instanceof Element) {\n const rect = child.getBoundingClientRect();\n result.set(child, {\n x: rect.left - parent.left,\n y: rect.top - parent.top,\n width: rect.width,\n height: rect.height,\n });\n }\n }\n return result;\n },\n (old) => {\n for (const child of target.children) {\n if (child instanceof Element) {\n if (removed.has(child)) continue;\n const rect = getRelativePosition(child);\n const oldRect = old.get(child);\n if (!oldRect) continue;\n if (\n rect.x !== oldRect.x ||\n rect.y !== oldRect.y ||\n rect.width !== oldRect.width ||\n rect.height !== oldRect.height\n ) {\n animateNodeMove(child, rect, oldRect);\n }\n }\n }\n },\n );\n target.removeChild = function removeChild<T extends Node>(node: T) {\n if (node instanceof Element) {\n if (removed.has(node)) return node;\n removed.add(node);\n const rect = snapshot().get(node) ?? getRelativePosition(node);\n animateNodeExit(node, rect);\n return node;\n }\n return Element.prototype.removeChild.call(this, node) as T;\n };\n target.insertBefore = function insertBefore<T extends Node>(node: T, child: Node | null) {\n snapshot();\n if (!(node instanceof Element)) {\n return Element.prototype.insertBefore.call(this, node, child) as T;\n }\n Element.prototype.insertBefore.call(this, node, child);\n animateNodeEnter(node);\n return node;\n };\n target.appendChild = function appendChild<T extends Node>(node: T) {\n snapshot();\n if (!(node instanceof Element)) {\n return Element.prototype.appendChild.call(this, node) as T;\n }\n Element.prototype.appendChild.call(this, node);\n animateNodeEnter(node);\n return node;\n };\n return () => {\n target.removeChild = Element.prototype.removeChild;\n target.insertBefore = Element.prototype.insertBefore;\n target.appendChild = Element.prototype.appendChild;\n };\n\n function animateNodeExit(node: Element, rect: Rect) {\n let animation: Animation;\n if (transition?.exit) {\n animation = transition.exit(node, rect);\n } else {\n const width = `${rect.width}px`;\n const height = `${rect.height}px`;\n const translate = `translate(${rect.x}px, ${rect.y}px)`;\n animation = node.animate(\n {\n position: [\"absolute\", \"absolute\"],\n opacity: [1, 0],\n top: [\"0\", \"0\"],\n left: [\"0\", \"0\"],\n transform: [translate, translate],\n width: [width, width],\n height: [height, height],\n margin: [\"0\", \"0\"],\n },\n { duration: 250, easing: \"ease-in\" },\n );\n }\n animation.finished.then(() => node.remove());\n return animation;\n }\n\n function animateNodeEnter(node: Element) {\n if (transition?.enter) {\n transition.enter(node);\n } else {\n node.animate({ opacity: [0, 1] }, { duration: 250, easing: \"ease-out\" });\n }\n }\n\n function animateNodeMove(node: Element, rect: Rect, oldRect: Rect) {\n if (transition?.move) {\n transition.move(node, rect, oldRect);\n } else {\n const dx = oldRect.x - rect.x;\n const dy = oldRect.y - rect.y;\n const sx = oldRect.width / rect.width;\n const sy = oldRect.height / rect.height;\n node.animate(\n {\n transformOrigin: [\"0 0\", \"0 0\"],\n transform: [`translate(${dx}px, ${dy}px) scale(${sx}, ${sy})`, `translate(0, 0) scale(1, 1)`],\n },\n { duration: 250, easing: \"ease-in\" },\n );\n }\n }\n\n function getRelativePosition(node: Element, parent = parentRect()): Rect {\n const rect = node.getBoundingClientRect();\n return {\n x: rect.left - parent.left,\n y: rect.top - parent.top,\n width: rect.width,\n height: rect.height,\n };\n }\n }, [transition]);\n const forkedRef = useForkRef(ref, externalRef);\n return (\n <Component ref={forkedRef} {...rest}>\n {children}\n </Component>\n );\n}\n\n/**\n * A plugin interface to provide custom animations for enter/exit/move/resize.\n * Implementations should return a Web Animations API `Animation` instance.\n *\n * - `enter` receives the element being inserted.\n * - `exit` receives the element being removed and its last-known rectangle\n * relative to the measurement parent — useful for leaving the element in\n * place while animating out.\n * - `move` receives the element that's moved and both current/previous rects\n * to allow translation/scale-based transitions.\n * - `resize` receives the element and previous/new dimensions — note: the\n * current component implementation doesn't automatically call `resize`,\n * but implementations may document this hook for future use.\n */\nexport type TransitionPlugin = {\n /** Play when an element enters/was inserted into the container. */\n enter?(el: Element): Animation;\n /** Play when an element is removed; `rect` is the element's rect at removal time. */\n exit?(el: Element, rect: Rect): Animation;\n /** Play when an element moves within the container (position or size changes). */\n move?(el: Element, current: Rect, previous: Rect): Animation;\n /** Play when element is resized; not invoked by current implementation. */\n resize?(el: Element, current: Dimensions, previous: Dimensions): Animation;\n};\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codehz/auto-transition",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "A lightweight React component for automatic enter/exit/move transitions using Web Animations API",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "codehz",
|
|
7
|
+
"module": "dist/index.mjs",
|
|
8
|
+
"type": "module",
|
|
9
|
+
"devDependencies": {
|
|
10
|
+
"@types/bun": "latest",
|
|
11
|
+
"@types/react": "^19.2.7",
|
|
12
|
+
"eslint": "^9.39.2",
|
|
13
|
+
"eslint-config-prettier": "^10.1.8",
|
|
14
|
+
"eslint-plugin-react": "^7.37.5",
|
|
15
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
16
|
+
"husky": "^9.1.7",
|
|
17
|
+
"lint-staged": "^16.2.7",
|
|
18
|
+
"prettier": "^3.7.4",
|
|
19
|
+
"publint": "^0.3.16",
|
|
20
|
+
"tsdown": "^0.18.2",
|
|
21
|
+
"typescript": "^5.9.3",
|
|
22
|
+
"typescript-eslint": "^8.50.1"
|
|
23
|
+
},
|
|
24
|
+
"peerDependencies": {
|
|
25
|
+
"react": "^19.2.3"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@radix-ui/react-slot": "^1.2.4"
|
|
29
|
+
},
|
|
30
|
+
"files": [
|
|
31
|
+
"dist",
|
|
32
|
+
"README.md",
|
|
33
|
+
"LICENSE"
|
|
34
|
+
],
|
|
35
|
+
"exports": {
|
|
36
|
+
".": {
|
|
37
|
+
"types": "./dist/index.d.mts",
|
|
38
|
+
"import": "./dist/index.mjs"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"lint-staged": {
|
|
42
|
+
"*.{js,jsx,ts,tsx}": [
|
|
43
|
+
"eslint --fix",
|
|
44
|
+
"prettier --write"
|
|
45
|
+
],
|
|
46
|
+
"*.{json,md}": [
|
|
47
|
+
"prettier --write"
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"scripts": {
|
|
51
|
+
"prepare": "bunx tsdown --sourcemap --publint && bunx husky",
|
|
52
|
+
"lint": "eslint .",
|
|
53
|
+
"format": "prettier --write ."
|
|
54
|
+
}
|
|
55
|
+
}
|