@wordpress/grid 0.1.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/CHANGELOG.md +33 -0
- package/LICENSE.md +788 -0
- package/README.md +534 -0
- package/build/dashboard-grid/grid-item.cjs +308 -0
- package/build/dashboard-grid/grid-item.cjs.map +7 -0
- package/build/dashboard-grid/index.cjs +591 -0
- package/build/dashboard-grid/index.cjs.map +7 -0
- package/build/dashboard-grid/resolve-fill-widths.cjs +189 -0
- package/build/dashboard-grid/resolve-fill-widths.cjs.map +7 -0
- package/build/dashboard-grid/types.cjs +19 -0
- package/build/dashboard-grid/types.cjs.map +7 -0
- package/build/dashboard-lanes/index.cjs +558 -0
- package/build/dashboard-lanes/index.cjs.map +7 -0
- package/build/dashboard-lanes/lane-placement.cjs +110 -0
- package/build/dashboard-lanes/lane-placement.cjs.map +7 -0
- package/build/dashboard-lanes/lanes-item.cjs +295 -0
- package/build/dashboard-lanes/lanes-item.cjs.map +7 -0
- package/build/dashboard-lanes/types.cjs +19 -0
- package/build/dashboard-lanes/types.cjs.map +7 -0
- package/build/dashboard-lanes/use-lane-placement.cjs +206 -0
- package/build/dashboard-lanes/use-lane-placement.cjs.map +7 -0
- package/build/index.cjs +34 -0
- package/build/index.cjs.map +7 -0
- package/build/shared/drag-overlay-drop-animation.cjs +70 -0
- package/build/shared/drag-overlay-drop-animation.cjs.map +7 -0
- package/build/shared/grid-item-key.cjs +31 -0
- package/build/shared/grid-item-key.cjs.map +7 -0
- package/build/shared/grid-overlay.cjs +187 -0
- package/build/shared/grid-overlay.cjs.map +7 -0
- package/build/shared/item-exit-overlay.cjs +150 -0
- package/build/shared/item-exit-overlay.cjs.map +7 -0
- package/build/shared/resize-handle.cjs +224 -0
- package/build/shared/resize-handle.cjs.map +7 -0
- package/build/shared/resize-snap.cjs +47 -0
- package/build/shared/resize-snap.cjs.map +7 -0
- package/build/shared/types.cjs +19 -0
- package/build/shared/types.cjs.map +7 -0
- package/build/shared/use-item-exit-animation.cjs +148 -0
- package/build/shared/use-item-exit-animation.cjs.map +7 -0
- package/build/shared/use-layout-shift-animation.cjs +167 -0
- package/build/shared/use-layout-shift-animation.cjs.map +7 -0
- package/build-module/dashboard-grid/grid-item.mjs +273 -0
- package/build-module/dashboard-grid/grid-item.mjs.map +7 -0
- package/build-module/dashboard-grid/index.mjs +579 -0
- package/build-module/dashboard-grid/index.mjs.map +7 -0
- package/build-module/dashboard-grid/resolve-fill-widths.mjs +164 -0
- package/build-module/dashboard-grid/resolve-fill-widths.mjs.map +7 -0
- package/build-module/dashboard-grid/types.mjs +1 -0
- package/build-module/dashboard-grid/types.mjs.map +7 -0
- package/build-module/dashboard-lanes/index.mjs +547 -0
- package/build-module/dashboard-lanes/index.mjs.map +7 -0
- package/build-module/dashboard-lanes/lane-placement.mjs +85 -0
- package/build-module/dashboard-lanes/lane-placement.mjs.map +7 -0
- package/build-module/dashboard-lanes/lanes-item.mjs +260 -0
- package/build-module/dashboard-lanes/lanes-item.mjs.map +7 -0
- package/build-module/dashboard-lanes/types.mjs +1 -0
- package/build-module/dashboard-lanes/types.mjs.map +7 -0
- package/build-module/dashboard-lanes/use-lane-placement.mjs +181 -0
- package/build-module/dashboard-lanes/use-lane-placement.mjs.map +7 -0
- package/build-module/index.mjs +8 -0
- package/build-module/index.mjs.map +7 -0
- package/build-module/shared/drag-overlay-drop-animation.mjs +47 -0
- package/build-module/shared/drag-overlay-drop-animation.mjs.map +7 -0
- package/build-module/shared/grid-item-key.mjs +6 -0
- package/build-module/shared/grid-item-key.mjs.map +7 -0
- package/build-module/shared/grid-overlay.mjs +152 -0
- package/build-module/shared/grid-overlay.mjs.map +7 -0
- package/build-module/shared/item-exit-overlay.mjs +125 -0
- package/build-module/shared/item-exit-overlay.mjs.map +7 -0
- package/build-module/shared/resize-handle.mjs +193 -0
- package/build-module/shared/resize-handle.mjs.map +7 -0
- package/build-module/shared/resize-snap.mjs +21 -0
- package/build-module/shared/resize-snap.mjs.map +7 -0
- package/build-module/shared/types.mjs +1 -0
- package/build-module/shared/types.mjs.map +7 -0
- package/build-module/shared/use-item-exit-animation.mjs +128 -0
- package/build-module/shared/use-item-exit-animation.mjs.map +7 -0
- package/build-module/shared/use-layout-shift-animation.mjs +140 -0
- package/build-module/shared/use-layout-shift-animation.mjs.map +7 -0
- package/build-types/dashboard-grid/grid-item.d.ts +3 -0
- package/build-types/dashboard-grid/grid-item.d.ts.map +1 -0
- package/build-types/dashboard-grid/index.d.ts +35 -0
- package/build-types/dashboard-grid/index.d.ts.map +1 -0
- package/build-types/dashboard-grid/resolve-fill-widths.d.ts +26 -0
- package/build-types/dashboard-grid/resolve-fill-widths.d.ts.map +1 -0
- package/build-types/dashboard-grid/stories/index.story.d.ts +98 -0
- package/build-types/dashboard-grid/stories/index.story.d.ts.map +1 -0
- package/build-types/dashboard-grid/types.d.ts +232 -0
- package/build-types/dashboard-grid/types.d.ts.map +1 -0
- package/build-types/dashboard-lanes/index.d.ts +40 -0
- package/build-types/dashboard-lanes/index.d.ts.map +1 -0
- package/build-types/dashboard-lanes/lane-placement.d.ts +126 -0
- package/build-types/dashboard-lanes/lane-placement.d.ts.map +1 -0
- package/build-types/dashboard-lanes/lanes-item.d.ts +52 -0
- package/build-types/dashboard-lanes/lanes-item.d.ts.map +1 -0
- package/build-types/dashboard-lanes/stories/index.story.d.ts +64 -0
- package/build-types/dashboard-lanes/stories/index.story.d.ts.map +1 -0
- package/build-types/dashboard-lanes/types.d.ts +151 -0
- package/build-types/dashboard-lanes/types.d.ts.map +1 -0
- package/build-types/dashboard-lanes/use-lane-placement.d.ts +74 -0
- package/build-types/dashboard-lanes/use-lane-placement.d.ts.map +1 -0
- package/build-types/index.d.ts +6 -0
- package/build-types/index.d.ts.map +1 -0
- package/build-types/shared/drag-overlay-drop-animation.d.ts +13 -0
- package/build-types/shared/drag-overlay-drop-animation.d.ts.map +1 -0
- package/build-types/shared/grid-item-key.d.ts +6 -0
- package/build-types/shared/grid-item-key.d.ts.map +1 -0
- package/build-types/shared/grid-overlay.d.ts +19 -0
- package/build-types/shared/grid-overlay.d.ts.map +1 -0
- package/build-types/shared/item-exit-overlay.d.ts +20 -0
- package/build-types/shared/item-exit-overlay.d.ts.map +1 -0
- package/build-types/shared/resize-handle.d.ts +23 -0
- package/build-types/shared/resize-handle.d.ts.map +1 -0
- package/build-types/shared/resize-snap.d.ts +41 -0
- package/build-types/shared/resize-snap.d.ts.map +1 -0
- package/build-types/shared/types.d.ts +144 -0
- package/build-types/shared/types.d.ts.map +1 -0
- package/build-types/shared/use-item-exit-animation.d.ts +37 -0
- package/build-types/shared/use-item-exit-animation.d.ts.map +1 -0
- package/build-types/shared/use-layout-shift-animation.d.ts +77 -0
- package/build-types/shared/use-layout-shift-animation.d.ts.map +1 -0
- package/package.json +80 -0
- package/src/dashboard-grid/grid-item.module.css +94 -0
- package/src/dashboard-grid/grid-item.tsx +205 -0
- package/src/dashboard-grid/grid.module.css +134 -0
- package/src/dashboard-grid/index.tsx +713 -0
- package/src/dashboard-grid/resolve-fill-widths.ts +224 -0
- package/src/dashboard-grid/stories/index.story.tsx +930 -0
- package/src/dashboard-grid/test/keyboard-activation.test.tsx +76 -0
- package/src/dashboard-grid/test/resolve-fill-widths.test.ts +250 -0
- package/src/dashboard-grid/types.ts +271 -0
- package/src/dashboard-lanes/index.tsx +629 -0
- package/src/dashboard-lanes/lane-placement.ts +245 -0
- package/src/dashboard-lanes/lanes-item.module.css +93 -0
- package/src/dashboard-lanes/lanes-item.tsx +236 -0
- package/src/dashboard-lanes/lanes.module.css +152 -0
- package/src/dashboard-lanes/stories/index.story.tsx +518 -0
- package/src/dashboard-lanes/test/keyboard-activation.test.tsx +71 -0
- package/src/dashboard-lanes/test/lane-placement.test.ts +442 -0
- package/src/dashboard-lanes/test/use-lane-placement.test.tsx +358 -0
- package/src/dashboard-lanes/types.ts +176 -0
- package/src/dashboard-lanes/use-lane-placement.ts +313 -0
- package/src/index.ts +17 -0
- package/src/shared/actionable-area-slot.module.css +16 -0
- package/src/shared/drag-overlay-drop-animation.ts +66 -0
- package/src/shared/grid-item-key.ts +5 -0
- package/src/shared/grid-overlay.module.css +82 -0
- package/src/shared/grid-overlay.tsx +93 -0
- package/src/shared/item-exit-animation.module.css +49 -0
- package/src/shared/item-exit-overlay.tsx +57 -0
- package/src/shared/layout-shift-animation.module.css +16 -0
- package/src/shared/resize-handle.module.css +88 -0
- package/src/shared/resize-handle.tsx +163 -0
- package/src/shared/resize-snap.ts +63 -0
- package/src/shared/test/resize-snap.test.ts +35 -0
- package/src/shared/types.ts +164 -0
- package/src/shared/use-item-exit-animation.ts +199 -0
- package/src/shared/use-layout-shift-animation.ts +284 -0
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
useCallback,
|
|
6
|
+
useLayoutEffect,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
} from '@wordpress/element';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Internal dependencies
|
|
13
|
+
*/
|
|
14
|
+
import type { ItemExitOverlayRect } from './item-exit-overlay';
|
|
15
|
+
import type { RectSnapshot } from './use-layout-shift-animation';
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Last-resort cleanup if `animationend` never fires (the overlay's
|
|
19
|
+
* `onAnimationEnd` is the primary path). Kept well above the motion
|
|
20
|
+
* token durations so the timeout can never clip the exit animation.
|
|
21
|
+
*/
|
|
22
|
+
const EXIT_SAFETY_TIMEOUT_MS = 1000;
|
|
23
|
+
|
|
24
|
+
export type ExitingGridItem = {
|
|
25
|
+
key: string;
|
|
26
|
+
rect: ItemExitOverlayRect;
|
|
27
|
+
child: React.ReactElement;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type UseItemExitAnimationOptions = {
|
|
31
|
+
container: HTMLElement | null;
|
|
32
|
+
enabled: boolean;
|
|
33
|
+
layoutKeys: ReadonlySet< string >;
|
|
34
|
+
getPositionsBeforeLastChange: () => ReadonlyMap<
|
|
35
|
+
string,
|
|
36
|
+
RectSnapshot
|
|
37
|
+
> | null;
|
|
38
|
+
childrenCacheRef: React.MutableRefObject<
|
|
39
|
+
Map< string, React.ReactElement >
|
|
40
|
+
>;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
type UseItemExitAnimationResult = {
|
|
44
|
+
exitingItems: ExitingGridItem[];
|
|
45
|
+
hasExitingItems: boolean;
|
|
46
|
+
clearExitingItem: ( key: string ) => void;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
function prefersReducedMotion(): boolean {
|
|
50
|
+
return (
|
|
51
|
+
typeof window !== 'undefined' &&
|
|
52
|
+
window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* When `layout` loses keys in edit mode, keeps a short-lived overlay at
|
|
58
|
+
* the removed tile's last position (scale + fade) while siblings FLIP.
|
|
59
|
+
*
|
|
60
|
+
* @param root0 Hook options.
|
|
61
|
+
* @param root0.container Surface root that contains grid tiles.
|
|
62
|
+
* @param root0.enabled When false, exiting state is cleared.
|
|
63
|
+
* @param root0.layoutKeys Keys in the committed `layout` prop.
|
|
64
|
+
* @param root0.getPositionsBeforeLastChange Container-relative rects before the latest layout commit.
|
|
65
|
+
* @param root0.childrenCacheRef Last rendered children keyed by tile id.
|
|
66
|
+
* @return Exiting overlays and a callback to dismiss one by key.
|
|
67
|
+
*/
|
|
68
|
+
export function useItemExitAnimation( {
|
|
69
|
+
container,
|
|
70
|
+
enabled,
|
|
71
|
+
layoutKeys,
|
|
72
|
+
getPositionsBeforeLastChange,
|
|
73
|
+
childrenCacheRef,
|
|
74
|
+
}: UseItemExitAnimationOptions ): UseItemExitAnimationResult {
|
|
75
|
+
const [ exitingItems, setExitingItems ] = useState< ExitingGridItem[] >(
|
|
76
|
+
[]
|
|
77
|
+
);
|
|
78
|
+
const prevLayoutKeysRef = useRef< Set< string > >( new Set() );
|
|
79
|
+
const exitTimeoutsRef = useRef<
|
|
80
|
+
Map< string, ReturnType< typeof setTimeout > >
|
|
81
|
+
>( new Map() );
|
|
82
|
+
|
|
83
|
+
const clearExitingItem = useCallback(
|
|
84
|
+
( key: string ) => {
|
|
85
|
+
const timeout = exitTimeoutsRef.current.get( key );
|
|
86
|
+
if ( timeout ) {
|
|
87
|
+
clearTimeout( timeout );
|
|
88
|
+
exitTimeoutsRef.current.delete( key );
|
|
89
|
+
}
|
|
90
|
+
setExitingItems( ( current ) =>
|
|
91
|
+
current.filter( ( item ) => item.key !== key )
|
|
92
|
+
);
|
|
93
|
+
childrenCacheRef.current.delete( key );
|
|
94
|
+
},
|
|
95
|
+
[ childrenCacheRef ]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const scheduleExitComplete = useCallback(
|
|
99
|
+
( key: string ) => {
|
|
100
|
+
if ( exitTimeoutsRef.current.has( key ) ) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const timeout = setTimeout( () => {
|
|
104
|
+
exitTimeoutsRef.current.delete( key );
|
|
105
|
+
clearExitingItem( key );
|
|
106
|
+
}, EXIT_SAFETY_TIMEOUT_MS );
|
|
107
|
+
exitTimeoutsRef.current.set( key, timeout );
|
|
108
|
+
},
|
|
109
|
+
[ clearExitingItem ]
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
useLayoutEffect( () => {
|
|
113
|
+
if ( ! enabled || ! container ) {
|
|
114
|
+
prevLayoutKeysRef.current = new Set( layoutKeys );
|
|
115
|
+
for ( const timeout of exitTimeoutsRef.current.values() ) {
|
|
116
|
+
clearTimeout( timeout );
|
|
117
|
+
}
|
|
118
|
+
exitTimeoutsRef.current.clear();
|
|
119
|
+
setExitingItems( [] );
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const prevKeys = prevLayoutKeysRef.current;
|
|
124
|
+
const removed: string[] = [];
|
|
125
|
+
for ( const key of prevKeys ) {
|
|
126
|
+
if ( ! layoutKeys.has( key ) ) {
|
|
127
|
+
removed.push( key );
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
prevLayoutKeysRef.current = new Set( layoutKeys );
|
|
131
|
+
|
|
132
|
+
if ( removed.length === 0 ) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const lastPositions = getPositionsBeforeLastChange();
|
|
137
|
+
if ( ! lastPositions ) {
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const nextExiting: ExitingGridItem[] = [];
|
|
142
|
+
for ( const key of removed ) {
|
|
143
|
+
const position = lastPositions.get( key );
|
|
144
|
+
const child = childrenCacheRef.current.get( key );
|
|
145
|
+
if ( ! position || ! child ) {
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
nextExiting.push( {
|
|
149
|
+
key,
|
|
150
|
+
rect: position,
|
|
151
|
+
child,
|
|
152
|
+
} );
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if ( nextExiting.length === 0 ) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if ( prefersReducedMotion() ) {
|
|
160
|
+
// Siblings snap into place via the layout-shift hook; skip the
|
|
161
|
+
// exit ghost (and its synchronous mount) entirely.
|
|
162
|
+
for ( const { key } of nextExiting ) {
|
|
163
|
+
childrenCacheRef.current.delete( key );
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// A state update inside a layout effect is flushed before paint,
|
|
169
|
+
// so the ghost mounts in the same frame the tile is removed.
|
|
170
|
+
setExitingItems( ( current ) => [ ...current, ...nextExiting ] );
|
|
171
|
+
|
|
172
|
+
for ( const { key } of nextExiting ) {
|
|
173
|
+
scheduleExitComplete( key );
|
|
174
|
+
}
|
|
175
|
+
}, [
|
|
176
|
+
container,
|
|
177
|
+
enabled,
|
|
178
|
+
getPositionsBeforeLastChange,
|
|
179
|
+
layoutKeys,
|
|
180
|
+
childrenCacheRef,
|
|
181
|
+
scheduleExitComplete,
|
|
182
|
+
] );
|
|
183
|
+
|
|
184
|
+
useLayoutEffect( () => {
|
|
185
|
+
const exitTimeouts = exitTimeoutsRef.current;
|
|
186
|
+
return () => {
|
|
187
|
+
for ( const timeout of exitTimeouts.values() ) {
|
|
188
|
+
clearTimeout( timeout );
|
|
189
|
+
}
|
|
190
|
+
exitTimeouts.clear();
|
|
191
|
+
};
|
|
192
|
+
}, [] );
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
exitingItems,
|
|
196
|
+
hasExitingItems: exitingItems.length > 0,
|
|
197
|
+
clearExitingItem,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WordPress dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { useCallback, useLayoutEffect, useRef } from '@wordpress/element';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Internal dependencies
|
|
8
|
+
*/
|
|
9
|
+
import { GRID_ITEM_DATA_KEY } from './grid-item-key';
|
|
10
|
+
|
|
11
|
+
/* `left`/`top` are relative to the grid container, not the viewport. */
|
|
12
|
+
export type RectSnapshot = {
|
|
13
|
+
left: number;
|
|
14
|
+
top: number;
|
|
15
|
+
width: number;
|
|
16
|
+
height: number;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type UseLayoutShiftAnimationOptions = {
|
|
20
|
+
/**
|
|
21
|
+
* Surface root that contains grid tiles.
|
|
22
|
+
*/
|
|
23
|
+
container: HTMLElement | null;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* When false, snapshots are cleared and no transforms run.
|
|
27
|
+
*/
|
|
28
|
+
enabled: boolean;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Serialized layout/placement state. The hook runs FLIP when this
|
|
32
|
+
* value changes while `enabled` is true.
|
|
33
|
+
*/
|
|
34
|
+
layoutFingerprint: string;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Item key to skip (the tile being dragged or resized).
|
|
38
|
+
*/
|
|
39
|
+
excludeItemKey?: string | null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
type UseLayoutShiftAnimationResult = {
|
|
43
|
+
/**
|
|
44
|
+
* Capture tile positions synchronously **before** a layout update
|
|
45
|
+
* (call immediately before `setTemporaryLayout` / similar).
|
|
46
|
+
*/
|
|
47
|
+
captureLayoutSnapshot: () => void;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Container-relative rects from the last committed paint (settled, no
|
|
51
|
+
* FLIP invert transforms).
|
|
52
|
+
*/
|
|
53
|
+
getLastPositions: () => ReadonlyMap< string, RectSnapshot > | null;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Tile positions immediately before the latest layout commit. Used
|
|
57
|
+
* by item-exit animation when keys drop out of `layout`.
|
|
58
|
+
*/
|
|
59
|
+
getPositionsBeforeLastChange: () => ReadonlyMap<
|
|
60
|
+
string,
|
|
61
|
+
RectSnapshot
|
|
62
|
+
> | null;
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function queryGridItems( container: HTMLElement ): HTMLElement[] {
|
|
66
|
+
return Array.from(
|
|
67
|
+
container.querySelectorAll< HTMLElement >(
|
|
68
|
+
`[${ GRID_ITEM_DATA_KEY }]:not([data-wp-grid-item-exiting])`
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readItemKey( element: HTMLElement ): string | null {
|
|
74
|
+
return element.getAttribute( GRID_ITEM_DATA_KEY );
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function snapshotPositions(
|
|
78
|
+
container: HTMLElement
|
|
79
|
+
): Map< string, RectSnapshot > {
|
|
80
|
+
// Measure relative to the container so positions stay valid even if the
|
|
81
|
+
// page scroll shifts between capture and use (e.g. the document reflowing
|
|
82
|
+
// shorter after a tile is removed).
|
|
83
|
+
const base = container.getBoundingClientRect();
|
|
84
|
+
const positions = new Map< string, RectSnapshot >();
|
|
85
|
+
for ( const element of queryGridItems( container ) ) {
|
|
86
|
+
const key = readItemKey( element );
|
|
87
|
+
if ( ! key ) {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
const { left, top, width, height } = element.getBoundingClientRect();
|
|
91
|
+
positions.set( key, {
|
|
92
|
+
left: left - base.left,
|
|
93
|
+
top: top - base.top,
|
|
94
|
+
width,
|
|
95
|
+
height,
|
|
96
|
+
} );
|
|
97
|
+
}
|
|
98
|
+
return positions;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function clearLayoutShiftStyles( element: HTMLElement ): void {
|
|
102
|
+
element.style.removeProperty( 'transform' );
|
|
103
|
+
element.style.removeProperty( 'transition' );
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function playLayoutShift(
|
|
107
|
+
element: HTMLElement,
|
|
108
|
+
deltaX: number,
|
|
109
|
+
deltaY: number
|
|
110
|
+
): void {
|
|
111
|
+
if ( deltaX === 0 && deltaY === 0 ) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Invert: show the tile where it was before the layout change.
|
|
116
|
+
element.style.transition = 'none';
|
|
117
|
+
element.style.transform = `translate(${ deltaX }px, ${ deltaY }px)`;
|
|
118
|
+
void element.offsetHeight;
|
|
119
|
+
|
|
120
|
+
// Play on the next frame so the inverted transform paints before
|
|
121
|
+
// the transition back to the committed grid position.
|
|
122
|
+
requestAnimationFrame( () => {
|
|
123
|
+
element.style.removeProperty( 'transition' );
|
|
124
|
+
element.style.transform = '';
|
|
125
|
+
|
|
126
|
+
const onTransitionEnd = ( event: TransitionEvent ) => {
|
|
127
|
+
if ( event.propertyName !== 'transform' ) {
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
element.removeEventListener( 'transitionend', onTransitionEnd );
|
|
131
|
+
clearLayoutShiftStyles( element );
|
|
132
|
+
};
|
|
133
|
+
element.addEventListener( 'transitionend', onTransitionEnd );
|
|
134
|
+
} );
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Animates sibling tiles when grid layout reflows during drag or resize
|
|
139
|
+
* using a FLIP transform (see `layout-shift-animation.module.css`).
|
|
140
|
+
*
|
|
141
|
+
* @param root0 Hook options.
|
|
142
|
+
* @param root0.container Surface root that contains grid tiles.
|
|
143
|
+
* @param root0.enabled When false, snapshots are cleared and no transforms run.
|
|
144
|
+
* @param root0.layoutFingerprint Serialized layout/placement state.
|
|
145
|
+
* @param root0.excludeItemKey Item key to skip (the tile being dragged or resized).
|
|
146
|
+
* @return Snapshot capture callback for use before layout updates.
|
|
147
|
+
*/
|
|
148
|
+
export function useLayoutShiftAnimation( {
|
|
149
|
+
container,
|
|
150
|
+
enabled,
|
|
151
|
+
layoutFingerprint,
|
|
152
|
+
excludeItemKey = null,
|
|
153
|
+
}: UseLayoutShiftAnimationOptions ): UseLayoutShiftAnimationResult {
|
|
154
|
+
const snapshotBeforeChangeRef = useRef< Map<
|
|
155
|
+
string,
|
|
156
|
+
RectSnapshot
|
|
157
|
+
> | null >( null );
|
|
158
|
+
const lastRenderedPositionsRef = useRef< Map<
|
|
159
|
+
string,
|
|
160
|
+
RectSnapshot
|
|
161
|
+
> | null >( null );
|
|
162
|
+
const positionsBeforeLastChangeRef = useRef< Map<
|
|
163
|
+
string,
|
|
164
|
+
RectSnapshot
|
|
165
|
+
> | null >( null );
|
|
166
|
+
|
|
167
|
+
const captureLayoutSnapshot = useCallback( () => {
|
|
168
|
+
if ( container ) {
|
|
169
|
+
snapshotBeforeChangeRef.current = snapshotPositions( container );
|
|
170
|
+
}
|
|
171
|
+
}, [ container ] );
|
|
172
|
+
|
|
173
|
+
useLayoutEffect( () => {
|
|
174
|
+
if ( ! container || ! enabled ) {
|
|
175
|
+
snapshotBeforeChangeRef.current = null;
|
|
176
|
+
lastRenderedPositionsRef.current = null;
|
|
177
|
+
positionsBeforeLastChangeRef.current = null;
|
|
178
|
+
if ( container ) {
|
|
179
|
+
for ( const element of queryGridItems( container ) ) {
|
|
180
|
+
clearLayoutShiftStyles( element );
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
for ( const element of queryGridItems( container ) ) {
|
|
187
|
+
clearLayoutShiftStyles( element );
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const previous =
|
|
191
|
+
snapshotBeforeChangeRef.current ?? lastRenderedPositionsRef.current;
|
|
192
|
+
snapshotBeforeChangeRef.current = null;
|
|
193
|
+
|
|
194
|
+
positionsBeforeLastChangeRef.current = previous
|
|
195
|
+
? new Map( previous )
|
|
196
|
+
: null;
|
|
197
|
+
|
|
198
|
+
// Record settled grid positions for the next FLIP. Must run before
|
|
199
|
+
// invert transforms — measuring after `playLayoutShift` would bake
|
|
200
|
+
// translate offsets into the baseline and skew the next animation.
|
|
201
|
+
lastRenderedPositionsRef.current = snapshotPositions( container );
|
|
202
|
+
|
|
203
|
+
if ( previous ) {
|
|
204
|
+
const base = container.getBoundingClientRect();
|
|
205
|
+
for ( const element of queryGridItems( container ) ) {
|
|
206
|
+
const key = readItemKey( element );
|
|
207
|
+
if ( ! key || key === excludeItemKey ) {
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const old = previous.get( key );
|
|
211
|
+
if ( ! old ) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const { left, top } = element.getBoundingClientRect();
|
|
215
|
+
const deltaX = old.left - ( left - base.left );
|
|
216
|
+
const deltaY = old.top - ( top - base.top );
|
|
217
|
+
playLayoutShift( element, deltaX, deltaY );
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}, [ container, enabled, layoutFingerprint, excludeItemKey ] );
|
|
221
|
+
|
|
222
|
+
const getLastPositions = useCallback( () => {
|
|
223
|
+
return lastRenderedPositionsRef.current;
|
|
224
|
+
}, [] );
|
|
225
|
+
|
|
226
|
+
const getPositionsBeforeLastChange = useCallback( () => {
|
|
227
|
+
return positionsBeforeLastChangeRef.current;
|
|
228
|
+
}, [] );
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
captureLayoutSnapshot,
|
|
232
|
+
getLastPositions,
|
|
233
|
+
getPositionsBeforeLastChange,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Stable fingerprint for {@link useLayoutShiftAnimation}. Width/height
|
|
239
|
+
* values may be numbers or layout keywords (`'fill'`, `'full'`).
|
|
240
|
+
*
|
|
241
|
+
* @param layout Layout items to serialize.
|
|
242
|
+
* @return Fingerprint string.
|
|
243
|
+
*/
|
|
244
|
+
export function getLayoutFingerprint(
|
|
245
|
+
layout: ReadonlyArray< {
|
|
246
|
+
key: string;
|
|
247
|
+
width?: number | string;
|
|
248
|
+
height?: number;
|
|
249
|
+
order?: number;
|
|
250
|
+
lane?: number;
|
|
251
|
+
} >
|
|
252
|
+
): string {
|
|
253
|
+
return layout
|
|
254
|
+
.map(
|
|
255
|
+
( item ) =>
|
|
256
|
+
`${ item.key }:${ String( item.width ?? '' ) }:${
|
|
257
|
+
item.height ?? 1
|
|
258
|
+
}:${ item.order ?? '' }:${ item.lane ?? '' }`
|
|
259
|
+
)
|
|
260
|
+
.join( '|' );
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Placement fingerprint for lanes polyfill / explicit grid positions.
|
|
265
|
+
*
|
|
266
|
+
* @param itemStyles Per-item inline placement styles.
|
|
267
|
+
* @return Fingerprint string.
|
|
268
|
+
*/
|
|
269
|
+
export function getPlacementFingerprint(
|
|
270
|
+
itemStyles: Map< string, React.CSSProperties >
|
|
271
|
+
): string {
|
|
272
|
+
return [ ...itemStyles.entries() ]
|
|
273
|
+
.sort( ( [ a ], [ b ] ) => a.localeCompare( b ) )
|
|
274
|
+
.map( ( [ key, style ] ) => {
|
|
275
|
+
const column = style.gridColumn ?? '';
|
|
276
|
+
const columnStart = style.gridColumnStart ?? '';
|
|
277
|
+
const rowStart = style.gridRowStart ?? '';
|
|
278
|
+
const rowEnd = style.gridRowEnd ?? '';
|
|
279
|
+
return `${ key }:${ String( column ) }:${ String(
|
|
280
|
+
columnStart
|
|
281
|
+
) }:${ String( rowStart ) }:${ String( rowEnd ) }`;
|
|
282
|
+
} )
|
|
283
|
+
.join( '|' );
|
|
284
|
+
}
|