@versini/ui-datagrid 4.2.13 → 5.0.0
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/README.md +28 -30
- package/dist/7.js +1 -1
- package/dist/8.js +1 -1
- package/dist/DataGrid/DataGrid.d.ts +1 -1
- package/dist/DataGrid/DataGridTypes.d.ts +8 -0
- package/dist/DataGrid/index.js +102 -5
- package/dist/DataGrid/useHeightAnimation.d.ts +24 -0
- package/dist/DataGridBody/index.js +1 -1
- package/dist/DataGridCell/index.js +1 -1
- package/dist/DataGridCellSort/index.js +1 -1
- package/dist/DataGridConstants/index.js +1 -1
- package/dist/DataGridFooter/index.js +1 -1
- package/dist/DataGridHeader/index.js +1 -1
- package/dist/DataGridInfinite/index.js +1 -1
- package/dist/DataGridRow/index.js +1 -1
- package/dist/DataGridSorting/index.js +1 -1
- package/dist/u.js +1 -1
- package/dist/y.js +1 -1
- package/package.json +3 -7
- package/dist/DataGridAnimated/AnimatedWrapper.d.ts +0 -50
- package/dist/DataGridAnimated/index.d.ts +0 -4
- package/dist/DataGridAnimated/index.js +0 -92
- package/dist/DataGridAnimated/useAnimatedHeight.d.ts +0 -49
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ A data grid component library for React built with div-based ARIA grid layout fo
|
|
|
6
6
|
- **Flexible Layout**: Div-based structure allows for complex styling
|
|
7
7
|
- **CSS Grid Columns**: Define column widths with CSS Grid track sizes
|
|
8
8
|
- **Row Virtualization**: Windowing for large datasets — only viewport rows stay in the DOM
|
|
9
|
-
- **Animated Height**: Smooth height transitions when content changes
|
|
9
|
+
- **Animated Height**: Smooth height transitions when content changes (`animateHeight`)
|
|
10
10
|
- **Sorting**: Built-in sorting utilities and sortable column headers
|
|
11
11
|
- **Sticky Header/Footer**: With optional blur effects
|
|
12
12
|
- **Loading States**: Built-in loading overlay with spinner
|
|
@@ -38,12 +38,6 @@ import { DataGridCellSort } from "@versini/ui-datagrid/cell-sort";
|
|
|
38
38
|
// Virtualized body for large datasets
|
|
39
39
|
import { DataGridInfiniteBody } from "@versini/ui-datagrid/infinite";
|
|
40
40
|
|
|
41
|
-
// Animated height wrapper
|
|
42
|
-
import {
|
|
43
|
-
AnimatedWrapper,
|
|
44
|
-
useAnimatedHeight
|
|
45
|
-
} from "@versini/ui-datagrid/animated";
|
|
46
|
-
|
|
47
41
|
// Sorting utilities
|
|
48
42
|
import {
|
|
49
43
|
sortByDate,
|
|
@@ -144,39 +138,42 @@ import { DataGridHeader } from "@versini/ui-datagrid/header";
|
|
|
144
138
|
import { DataGridRow } from "@versini/ui-datagrid/row";
|
|
145
139
|
import { DataGridCell } from "@versini/ui-datagrid/cell";
|
|
146
140
|
import { DataGridInfiniteBody } from "@versini/ui-datagrid/infinite";
|
|
147
|
-
import { AnimatedWrapper } from "@versini/ui-datagrid/animated";
|
|
148
141
|
|
|
149
142
|
function MyInfiniteTable({ data }) {
|
|
150
143
|
const [visibleCount, setVisibleCount] = useState(0);
|
|
151
144
|
|
|
152
145
|
return (
|
|
153
|
-
<
|
|
154
|
-
<
|
|
155
|
-
<
|
|
156
|
-
<
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
</DataGridHeader>
|
|
146
|
+
<DataGrid maxHeight="500px" stickyHeader animateHeight>
|
|
147
|
+
<DataGridHeader caption={`Showing ${visibleCount} of ${data.length}`}>
|
|
148
|
+
<DataGridRow>
|
|
149
|
+
<DataGridCell>Name</DataGridCell>
|
|
150
|
+
<DataGridCell>Date</DataGridCell>
|
|
151
|
+
</DataGridRow>
|
|
152
|
+
</DataGridHeader>
|
|
161
153
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
</AnimatedWrapper>
|
|
154
|
+
<DataGridInfiniteBody
|
|
155
|
+
data={data}
|
|
156
|
+
estimatedRowHeight={48}
|
|
157
|
+
onVisibleCountChange={(count) => setVisibleCount(count)}
|
|
158
|
+
>
|
|
159
|
+
{(item) => (
|
|
160
|
+
<DataGridRow key={item.id}>
|
|
161
|
+
<DataGridCell>{item.name}</DataGridCell>
|
|
162
|
+
<DataGridCell>{item.date}</DataGridCell>
|
|
163
|
+
</DataGridRow>
|
|
164
|
+
)}
|
|
165
|
+
</DataGridInfiniteBody>
|
|
166
|
+
</DataGrid>
|
|
176
167
|
);
|
|
177
168
|
}
|
|
178
169
|
```
|
|
179
170
|
|
|
171
|
+
### Animated height
|
|
172
|
+
|
|
173
|
+
`animateHeight` animates the grid's height over 300ms whenever its content grows or shrinks, and keeps its border, shadow and rounded corners intact while it does. It follows the grid's measured content height, so there is no dependency value to pass. A width change, such as a window resize, is not animated, and neither is the grid mounting: a loading line swapped for a freshly mounted grid appears without a transition. It is skipped when the user prefers reduced motion. It is meant for grids bounded by `maxHeight` or a sticky header/footer; a page-scroll `DataGridInfiniteBody` changes height as rows are measured while scrolling.
|
|
174
|
+
|
|
175
|
+
Migrating from `AnimatedWrapper` (removed in v5): replace `<AnimatedWrapper dependency={n}><DataGrid …></AnimatedWrapper>` with `<DataGrid animateHeight …>`. The duration is fixed at 300ms, and there is no replacement for `duration`, `enabled`, `className`, the standalone `useAnimatedHeight` hook or its `isAnimating` value.
|
|
176
|
+
|
|
180
177
|
`DataGridInfiniteBody` keeps `columns` (including intrinsic `auto` tracks) working because it renders rows in normal grid flow between two full-span spacer elements (not absolutely positioned). It windows against the DataGrid's own scroll container — created when you set `maxHeight` or a sticky header/footer — and otherwise against the page (window scroll). To scroll inside your own bounded container, set `maxHeight` on the `DataGrid` rather than relying on an outer wrapper.
|
|
181
178
|
|
|
182
179
|
Notes:
|
|
@@ -335,6 +332,7 @@ function MySortableTable({ data }) {
|
|
|
335
332
|
| Prop | Type | Default | Description |
|
|
336
333
|
| ------------------ | ----------------------------------------------- | ---------- | -------------------------------------- |
|
|
337
334
|
| `mode` | `'dark' \| 'light' \| 'system' \| 'alt-system'` | `'system'` | Theme mode |
|
|
335
|
+
| `animateHeight` | `boolean` | `false` | Animate height when content changes |
|
|
338
336
|
| `compact` | `boolean` | `false` | Reduced padding |
|
|
339
337
|
| `stickyHeader` | `boolean` | `false` | Fixed header while scrolling |
|
|
340
338
|
| `stickyFooter` | `boolean` | `false` | Fixed footer while scrolling |
|
package/dist/7.js
CHANGED
package/dist/8.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { DataGridProps } from "./DataGridTypes.js";
|
|
2
|
-
export declare const DataGrid: ({ className, wrapperClassName, children, mode, compact, stickyHeader, stickyFooter, blurEffect, maxHeight, loading, columns, ...rest }: DataGridProps) => import("react").JSX.Element;
|
|
2
|
+
export declare const DataGrid: ({ animateHeight, className, wrapperClassName, children, mode, compact, stickyHeader, stickyFooter, blurEffect, maxHeight, loading, columns, ...rest }: DataGridProps) => import("react").JSX.Element;
|
|
@@ -16,6 +16,14 @@ export type DataGridProps = {
|
|
|
16
16
|
* The content of the DataGrid (DataGridHeader, DataGridBody, DataGridFooter).
|
|
17
17
|
*/
|
|
18
18
|
children: React.ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* If true, the data grid animates its height whenever its content grows or
|
|
21
|
+
* shrinks, e.g. when rows are filtered or swapped. The grid's own border,
|
|
22
|
+
* shadow and rounded corners stay intact throughout. A change in width, such
|
|
23
|
+
* as a window resize, is not animated.
|
|
24
|
+
* @default false
|
|
25
|
+
*/
|
|
26
|
+
animateHeight?: boolean;
|
|
19
27
|
/**
|
|
20
28
|
* CSS class to apply to the data grid.
|
|
21
29
|
*/
|
package/dist/DataGrid/index.js
CHANGED
|
@@ -1,14 +1,94 @@
|
|
|
1
1
|
/*!
|
|
2
|
-
@versini/ui-datagrid
|
|
2
|
+
@versini/ui-datagrid v5.0.0
|
|
3
3
|
© 2026 gizmette.com
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
-
import { useCallback, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { BlurEffects } from "../7.js";
|
|
9
9
|
import { getDataGridClasses } from "../y.js";
|
|
10
10
|
import { DataGridContext } from "../8.js";
|
|
11
|
-
const
|
|
11
|
+
const HEIGHT_ANIMATION_DURATION = 300;
|
|
12
|
+
function prefersReducedMotion() {
|
|
13
|
+
return Boolean(window.matchMedia?.("(prefers-reduced-motion: reduce)").matches);
|
|
14
|
+
}
|
|
15
|
+
function useHeightAnimation(wrapper, grid, enabled) {
|
|
16
|
+
useLayoutEffect(()=>{
|
|
17
|
+
if (!enabled || !wrapper || !grid) return;
|
|
18
|
+
const scroller = grid.parentElement;
|
|
19
|
+
const clipped = scroller && scroller !== wrapper ? [
|
|
20
|
+
wrapper,
|
|
21
|
+
scroller
|
|
22
|
+
] : [
|
|
23
|
+
wrapper
|
|
24
|
+
];
|
|
25
|
+
let wrapperHeight = wrapper.offsetHeight;
|
|
26
|
+
let gridHeight = grid.offsetHeight;
|
|
27
|
+
let gridWidth = grid.offsetWidth;
|
|
28
|
+
let animation;
|
|
29
|
+
let frame = 0;
|
|
30
|
+
const unlock = ()=>{
|
|
31
|
+
for (const el of clipped)el.style.removeProperty("overflow-y");
|
|
32
|
+
};
|
|
33
|
+
const settle = ()=>{
|
|
34
|
+
unlock();
|
|
35
|
+
wrapperHeight = wrapper.offsetHeight;
|
|
36
|
+
if (animation) {
|
|
37
|
+
animation = void 0;
|
|
38
|
+
frame = requestAnimationFrame(()=>observer.observe(wrapper));
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
const observer = new ResizeObserver((entries)=>{
|
|
42
|
+
if (!entries.some((entry)=>entry.target === grid)) {
|
|
43
|
+
wrapperHeight = wrapper.offsetHeight;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const nextGridHeight = grid.offsetHeight;
|
|
47
|
+
const nextGridWidth = grid.offsetWidth;
|
|
48
|
+
const resized = nextGridWidth !== gridWidth;
|
|
49
|
+
const heightChanged = nextGridHeight !== gridHeight;
|
|
50
|
+
gridHeight = nextGridHeight;
|
|
51
|
+
gridWidth = nextGridWidth;
|
|
52
|
+
if (!heightChanged || resized && !animation) {
|
|
53
|
+
wrapperHeight = wrapper.offsetHeight;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const from = animation ? wrapper.offsetHeight : wrapperHeight;
|
|
57
|
+
animation?.cancel();
|
|
58
|
+
const to = wrapper.offsetHeight;
|
|
59
|
+
if (from === to || prefersReducedMotion()) return void settle();
|
|
60
|
+
if (!animation) observer.unobserve(wrapper);
|
|
61
|
+
for (const el of clipped)el.style.overflowY = "hidden";
|
|
62
|
+
const current = wrapper.animate({
|
|
63
|
+
height: [
|
|
64
|
+
`${from}px`,
|
|
65
|
+
`${to}px`
|
|
66
|
+
]
|
|
67
|
+
}, {
|
|
68
|
+
duration: HEIGHT_ANIMATION_DURATION,
|
|
69
|
+
easing: "ease-out"
|
|
70
|
+
});
|
|
71
|
+
const settleIfCurrent = ()=>animation === current && settle();
|
|
72
|
+
current.onfinish = settleIfCurrent;
|
|
73
|
+
current.oncancel = settleIfCurrent;
|
|
74
|
+
animation = current;
|
|
75
|
+
});
|
|
76
|
+
observer.observe(grid);
|
|
77
|
+
observer.observe(wrapper);
|
|
78
|
+
return ()=>{
|
|
79
|
+
observer.disconnect();
|
|
80
|
+
cancelAnimationFrame(frame);
|
|
81
|
+
animation?.cancel();
|
|
82
|
+
animation = void 0;
|
|
83
|
+
unlock();
|
|
84
|
+
};
|
|
85
|
+
}, [
|
|
86
|
+
enabled,
|
|
87
|
+
wrapper,
|
|
88
|
+
grid
|
|
89
|
+
]);
|
|
90
|
+
}
|
|
91
|
+
const DataGrid = ({ animateHeight = false, className, wrapperClassName, children, mode = "system", compact = false, stickyHeader = false, stickyFooter = false, blurEffect = BlurEffects.NONE, maxHeight, loading = false, columns, ...rest })=>{
|
|
12
92
|
const [headerCount, setHeaderCount] = useState(0);
|
|
13
93
|
const [footerCount, setFooterCount] = useState(0);
|
|
14
94
|
const [headerHeight, setHeaderHeight] = useState(0);
|
|
@@ -40,6 +120,23 @@ const DataGrid = ({ className, wrapperClassName, children, mode = "system", comp
|
|
|
40
120
|
const scrollContainerRef = useCallback((el)=>{
|
|
41
121
|
setScrollContainer(el);
|
|
42
122
|
}, []);
|
|
123
|
+
const [wrapperElement, setWrapperElement] = useState(null);
|
|
124
|
+
const [gridElement, setGridElement] = useState(null);
|
|
125
|
+
const wrapperIsScroller = hasInternalScroller && !hasSticky;
|
|
126
|
+
const setWrapper = useCallback((el)=>{
|
|
127
|
+
if (animateHeight) setWrapperElement(el);
|
|
128
|
+
if (wrapperIsScroller) setScrollContainer(el);
|
|
129
|
+
}, [
|
|
130
|
+
animateHeight,
|
|
131
|
+
wrapperIsScroller
|
|
132
|
+
]);
|
|
133
|
+
const setGrid = useCallback((el)=>{
|
|
134
|
+
gridRef.current = el;
|
|
135
|
+
if (animateHeight) setGridElement(el);
|
|
136
|
+
}, [
|
|
137
|
+
animateHeight
|
|
138
|
+
]);
|
|
139
|
+
useHeightAnimation(wrapperElement, gridElement, animateHeight);
|
|
43
140
|
const classes = useMemo(()=>getDataGridClasses({
|
|
44
141
|
mode,
|
|
45
142
|
className,
|
|
@@ -110,7 +207,7 @@ const DataGrid = ({ className, wrapperClassName, children, mode = "system", comp
|
|
|
110
207
|
gridTemplateColumns: columns.join(" ")
|
|
111
208
|
} : void 0;
|
|
112
209
|
const gridContent = jsx("div", {
|
|
113
|
-
ref:
|
|
210
|
+
ref: setGrid,
|
|
114
211
|
role: "grid",
|
|
115
212
|
"aria-labelledby": captionId,
|
|
116
213
|
"aria-rowcount": rowCount,
|
|
@@ -143,7 +240,7 @@ const DataGrid = ({ className, wrapperClassName, children, mode = "system", comp
|
|
|
143
240
|
]
|
|
144
241
|
}),
|
|
145
242
|
jsx("div", {
|
|
146
|
-
ref:
|
|
243
|
+
ref: setWrapper,
|
|
147
244
|
className: classes.wrapper,
|
|
148
245
|
style: wrapperStyle,
|
|
149
246
|
children: hasSticky ? jsx("div", {
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export declare const HEIGHT_ANIMATION_DURATION = 300;
|
|
2
|
+
/**
|
|
3
|
+
* Animates the wrapper's height whenever the grid inside it changes height.
|
|
4
|
+
*
|
|
5
|
+
* The wrapper is the element that carries the grid's rounded corners, shadow
|
|
6
|
+
* and any ring passed through `wrapperClassName`. Clipping it with its own
|
|
7
|
+
* `overflow` never clips its own wrapper-shadow, which is why the animation
|
|
8
|
+
* lives here and not on an element around the grid: an outer clip would cut the
|
|
9
|
+
* border off for the length of the animation.
|
|
10
|
+
*
|
|
11
|
+
* The grid's height drives the animation because it never depends on the
|
|
12
|
+
* wrapper's: animating the wrapper cannot feed back into the measurement. Both
|
|
13
|
+
* elements are passed in rather than read from refs, because the grid element
|
|
14
|
+
* is replaced when a sticky header or footer registers after the first render,
|
|
15
|
+
* and the observer has to follow it. The wrapper is observed too, only to keep
|
|
16
|
+
* its resting height current when it changes on its own, such as a new
|
|
17
|
+
* max-height, and it is unobserved while it animates: resizing an element from
|
|
18
|
+
* inside its own observer callback is what raises "ResizeObserver loop
|
|
19
|
+
* completed with undelivered notifications". A width change re-wraps text and
|
|
20
|
+
* changes the height too, but that is a resize, not new content, so it is not
|
|
21
|
+
* animated.
|
|
22
|
+
*
|
|
23
|
+
*/
|
|
24
|
+
export declare function useHeightAnimation(wrapper: HTMLElement | null, grid: HTMLElement | null, enabled: boolean): void;
|
package/dist/u.js
CHANGED
package/dist/y.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@versini/ui-datagrid",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"author": "Arno Versini",
|
|
6
6
|
"publishConfig": {
|
|
@@ -13,10 +13,6 @@
|
|
|
13
13
|
},
|
|
14
14
|
"type": "module",
|
|
15
15
|
"exports": {
|
|
16
|
-
"./animated": {
|
|
17
|
-
"types": "./dist/DataGridAnimated/index.d.ts",
|
|
18
|
-
"import": "./dist/DataGridAnimated/index.js"
|
|
19
|
-
},
|
|
20
16
|
"./body": {
|
|
21
17
|
"types": "./dist/DataGridBody/index.d.ts",
|
|
22
18
|
"import": "./dist/DataGridBody/index.js"
|
|
@@ -64,12 +60,12 @@
|
|
|
64
60
|
],
|
|
65
61
|
"devDependencies": {
|
|
66
62
|
"@testing-library/jest-dom": "7.0.1",
|
|
67
|
-
"@versini/ui-button": "15.3.
|
|
63
|
+
"@versini/ui-button": "15.3.10",
|
|
68
64
|
"@versini/ui-styles": "9.7.9"
|
|
69
65
|
},
|
|
70
66
|
"dependencies": {
|
|
71
67
|
"@tanstack/react-virtual": "3.14.13",
|
|
72
|
-
"@versini/ui-icons": "4.
|
|
68
|
+
"@versini/ui-icons": "4.30.0",
|
|
73
69
|
"clsx": "2.1.1",
|
|
74
70
|
"tailwindcss": "4.3.3"
|
|
75
71
|
},
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
export type AnimatedWrapperProps = {
|
|
2
|
-
/**
|
|
3
|
-
* The content to wrap (typically a DataGrid).
|
|
4
|
-
*/
|
|
5
|
-
children: React.ReactNode;
|
|
6
|
-
/**
|
|
7
|
-
* Value that triggers animation when changed. Typically the count of items or
|
|
8
|
-
* a key that changes.
|
|
9
|
-
*/
|
|
10
|
-
dependency: unknown;
|
|
11
|
-
/**
|
|
12
|
-
* Animation duration in milliseconds.
|
|
13
|
-
* @default 300
|
|
14
|
-
*/
|
|
15
|
-
duration?: number;
|
|
16
|
-
/**
|
|
17
|
-
* Whether animation is enabled.
|
|
18
|
-
* @default true
|
|
19
|
-
*/
|
|
20
|
-
enabled?: boolean;
|
|
21
|
-
/**
|
|
22
|
-
* Additional class name for the wrapper.
|
|
23
|
-
*/
|
|
24
|
-
className?: string;
|
|
25
|
-
};
|
|
26
|
-
/**
|
|
27
|
-
* Wrapper component that animates height changes when content changes.
|
|
28
|
-
*
|
|
29
|
-
* @example
|
|
30
|
-
* ```tsx
|
|
31
|
-
* const [visibleCount, setVisibleCount] = useState(0);
|
|
32
|
-
*
|
|
33
|
-
* return (
|
|
34
|
-
* <AnimatedWrapper dependency={visibleCount}>
|
|
35
|
-
* <DataGrid maxHeight="400px" stickyHeader>
|
|
36
|
-
* <DataGridInfiniteBody
|
|
37
|
-
* data={largeData}
|
|
38
|
-
* onVisibleCountChange={(count) => setVisibleCount(count)}
|
|
39
|
-
* >
|
|
40
|
-
* {(row) => (
|
|
41
|
-
* <DataGridRow key={row.id}>...</DataGridRow>
|
|
42
|
-
* )}
|
|
43
|
-
* </DataGridInfiniteBody>
|
|
44
|
-
* </DataGrid>
|
|
45
|
-
* </AnimatedWrapper>
|
|
46
|
-
* );
|
|
47
|
-
* ```
|
|
48
|
-
*
|
|
49
|
-
*/
|
|
50
|
-
export declare const AnimatedWrapper: ({ children, dependency, duration, enabled, className, }: AnimatedWrapperProps) => import("react").JSX.Element;
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
export type { AnimatedWrapperProps } from "./AnimatedWrapper.js";
|
|
2
|
-
export { AnimatedWrapper } from "./AnimatedWrapper.js";
|
|
3
|
-
export type { UseAnimatedHeightOptions, UseAnimatedHeightReturn, } from "./useAnimatedHeight.js";
|
|
4
|
-
export { useAnimatedHeight } from "./useAnimatedHeight.js";
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/*!
|
|
2
|
-
@versini/ui-datagrid v4.2.13
|
|
3
|
-
© 2026 gizmette.com
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { jsx } from "react/jsx-runtime";
|
|
7
|
-
import { useLayoutEffect, useRef, useState } from "react";
|
|
8
|
-
const DEFAULT_ANIMATION_DURATION = 300;
|
|
9
|
-
function useAnimatedHeight(dependency, options) {
|
|
10
|
-
const { duration = DEFAULT_ANIMATION_DURATION, enabled = true } = options ?? {};
|
|
11
|
-
const ref = useRef(null);
|
|
12
|
-
const [animationState, setAnimationState] = useState({
|
|
13
|
-
height: "auto",
|
|
14
|
-
isAnimating: false
|
|
15
|
-
});
|
|
16
|
-
const lastHeightRef = useRef(0);
|
|
17
|
-
const prevDependencyRef = useRef(dependency);
|
|
18
|
-
const animationFrameRef = useRef(0);
|
|
19
|
-
const timeoutRef = useRef(null);
|
|
20
|
-
useLayoutEffect(()=>{
|
|
21
|
-
if (!ref.current || !enabled) return;
|
|
22
|
-
if (0 === lastHeightRef.current) {
|
|
23
|
-
lastHeightRef.current = ref.current.offsetHeight;
|
|
24
|
-
prevDependencyRef.current = dependency;
|
|
25
|
-
setAnimationState((state)=>state.isAnimating ? {
|
|
26
|
-
height: "auto",
|
|
27
|
-
isAnimating: false
|
|
28
|
-
} : state);
|
|
29
|
-
return;
|
|
30
|
-
}
|
|
31
|
-
if (prevDependencyRef.current === dependency) return;
|
|
32
|
-
prevDependencyRef.current = dependency;
|
|
33
|
-
const previousHeight = lastHeightRef.current;
|
|
34
|
-
const newHeight = ref.current.scrollHeight;
|
|
35
|
-
lastHeightRef.current = newHeight;
|
|
36
|
-
if (previousHeight === newHeight || 0 === previousHeight) return void setAnimationState((state)=>state.isAnimating ? {
|
|
37
|
-
height: "auto",
|
|
38
|
-
isAnimating: false
|
|
39
|
-
} : state);
|
|
40
|
-
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
41
|
-
if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
|
|
42
|
-
setAnimationState({
|
|
43
|
-
height: previousHeight,
|
|
44
|
-
isAnimating: true
|
|
45
|
-
});
|
|
46
|
-
animationFrameRef.current = requestAnimationFrame(()=>{
|
|
47
|
-
animationFrameRef.current = requestAnimationFrame(()=>{
|
|
48
|
-
setAnimationState({
|
|
49
|
-
height: newHeight,
|
|
50
|
-
isAnimating: true
|
|
51
|
-
});
|
|
52
|
-
timeoutRef.current = setTimeout(()=>{
|
|
53
|
-
setAnimationState({
|
|
54
|
-
height: "auto",
|
|
55
|
-
isAnimating: false
|
|
56
|
-
});
|
|
57
|
-
}, duration);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
return ()=>{
|
|
61
|
-
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
|
62
|
-
if (animationFrameRef.current) cancelAnimationFrame(animationFrameRef.current);
|
|
63
|
-
};
|
|
64
|
-
}, [
|
|
65
|
-
dependency,
|
|
66
|
-
duration,
|
|
67
|
-
enabled
|
|
68
|
-
]);
|
|
69
|
-
const style = enabled ? {
|
|
70
|
-
height: "auto" === animationState.height ? "auto" : `${animationState.height}px`,
|
|
71
|
-
overflow: animationState.isAnimating ? "hidden" : void 0,
|
|
72
|
-
transition: animationState.isAnimating ? `height ${duration}ms ease-out` : void 0
|
|
73
|
-
} : {};
|
|
74
|
-
return {
|
|
75
|
-
ref,
|
|
76
|
-
style,
|
|
77
|
-
isAnimating: animationState.isAnimating
|
|
78
|
-
};
|
|
79
|
-
}
|
|
80
|
-
const AnimatedWrapper = ({ children, dependency, duration, enabled = true, className })=>{
|
|
81
|
-
const { ref, style } = useAnimatedHeight(dependency, {
|
|
82
|
-
duration,
|
|
83
|
-
enabled
|
|
84
|
-
});
|
|
85
|
-
return jsx("div", {
|
|
86
|
-
ref: ref,
|
|
87
|
-
style: style,
|
|
88
|
-
className: className,
|
|
89
|
-
children: children
|
|
90
|
-
});
|
|
91
|
-
};
|
|
92
|
-
export { AnimatedWrapper, useAnimatedHeight };
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
export type UseAnimatedHeightOptions = {
|
|
2
|
-
/**
|
|
3
|
-
* Animation duration in milliseconds.
|
|
4
|
-
* @default 300
|
|
5
|
-
*/
|
|
6
|
-
duration?: number;
|
|
7
|
-
/**
|
|
8
|
-
* Whether animation is enabled.
|
|
9
|
-
* @default true
|
|
10
|
-
*/
|
|
11
|
-
enabled?: boolean;
|
|
12
|
-
};
|
|
13
|
-
export type UseAnimatedHeightReturn<T extends HTMLElement> = {
|
|
14
|
-
/**
|
|
15
|
-
* Ref to attach to the container element.
|
|
16
|
-
*/
|
|
17
|
-
ref: React.RefObject<T | null>;
|
|
18
|
-
/**
|
|
19
|
-
* Style object to apply to the container.
|
|
20
|
-
*/
|
|
21
|
-
style: React.CSSProperties;
|
|
22
|
-
/**
|
|
23
|
-
* Whether an animation is currently in progress.
|
|
24
|
-
*/
|
|
25
|
-
isAnimating: boolean;
|
|
26
|
-
};
|
|
27
|
-
/**
|
|
28
|
-
* Hook that provides smooth height animations when content changes.
|
|
29
|
-
*
|
|
30
|
-
* Uses FLIP technique: captures height before render via a persistent ref, then
|
|
31
|
-
* animates from old height to new height after the DOM updates.
|
|
32
|
-
*
|
|
33
|
-
* @param dependency - Value that triggers animation when changed.
|
|
34
|
-
* @param options - Configuration options.
|
|
35
|
-
*
|
|
36
|
-
* @example
|
|
37
|
-
* ```tsx
|
|
38
|
-
* const items = useMemo(() => data.slice(0, visibleCount), [data, visibleCount]);
|
|
39
|
-
* const { ref, style } = useAnimatedHeight(items.length);
|
|
40
|
-
*
|
|
41
|
-
* return (
|
|
42
|
-
* <div ref={ref} style={style}>
|
|
43
|
-
* {items.map((item) => <Row key={item.id} {...item} />)}
|
|
44
|
-
* </div>
|
|
45
|
-
* );
|
|
46
|
-
* ```
|
|
47
|
-
*
|
|
48
|
-
*/
|
|
49
|
-
export declare function useAnimatedHeight<T, E extends HTMLElement = HTMLDivElement>(dependency: T, options?: UseAnimatedHeightOptions): UseAnimatedHeightReturn<E>;
|