@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.
Files changed (158) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/LICENSE.md +788 -0
  3. package/README.md +534 -0
  4. package/build/dashboard-grid/grid-item.cjs +308 -0
  5. package/build/dashboard-grid/grid-item.cjs.map +7 -0
  6. package/build/dashboard-grid/index.cjs +591 -0
  7. package/build/dashboard-grid/index.cjs.map +7 -0
  8. package/build/dashboard-grid/resolve-fill-widths.cjs +189 -0
  9. package/build/dashboard-grid/resolve-fill-widths.cjs.map +7 -0
  10. package/build/dashboard-grid/types.cjs +19 -0
  11. package/build/dashboard-grid/types.cjs.map +7 -0
  12. package/build/dashboard-lanes/index.cjs +558 -0
  13. package/build/dashboard-lanes/index.cjs.map +7 -0
  14. package/build/dashboard-lanes/lane-placement.cjs +110 -0
  15. package/build/dashboard-lanes/lane-placement.cjs.map +7 -0
  16. package/build/dashboard-lanes/lanes-item.cjs +295 -0
  17. package/build/dashboard-lanes/lanes-item.cjs.map +7 -0
  18. package/build/dashboard-lanes/types.cjs +19 -0
  19. package/build/dashboard-lanes/types.cjs.map +7 -0
  20. package/build/dashboard-lanes/use-lane-placement.cjs +206 -0
  21. package/build/dashboard-lanes/use-lane-placement.cjs.map +7 -0
  22. package/build/index.cjs +34 -0
  23. package/build/index.cjs.map +7 -0
  24. package/build/shared/drag-overlay-drop-animation.cjs +70 -0
  25. package/build/shared/drag-overlay-drop-animation.cjs.map +7 -0
  26. package/build/shared/grid-item-key.cjs +31 -0
  27. package/build/shared/grid-item-key.cjs.map +7 -0
  28. package/build/shared/grid-overlay.cjs +187 -0
  29. package/build/shared/grid-overlay.cjs.map +7 -0
  30. package/build/shared/item-exit-overlay.cjs +150 -0
  31. package/build/shared/item-exit-overlay.cjs.map +7 -0
  32. package/build/shared/resize-handle.cjs +224 -0
  33. package/build/shared/resize-handle.cjs.map +7 -0
  34. package/build/shared/resize-snap.cjs +47 -0
  35. package/build/shared/resize-snap.cjs.map +7 -0
  36. package/build/shared/types.cjs +19 -0
  37. package/build/shared/types.cjs.map +7 -0
  38. package/build/shared/use-item-exit-animation.cjs +148 -0
  39. package/build/shared/use-item-exit-animation.cjs.map +7 -0
  40. package/build/shared/use-layout-shift-animation.cjs +167 -0
  41. package/build/shared/use-layout-shift-animation.cjs.map +7 -0
  42. package/build-module/dashboard-grid/grid-item.mjs +273 -0
  43. package/build-module/dashboard-grid/grid-item.mjs.map +7 -0
  44. package/build-module/dashboard-grid/index.mjs +579 -0
  45. package/build-module/dashboard-grid/index.mjs.map +7 -0
  46. package/build-module/dashboard-grid/resolve-fill-widths.mjs +164 -0
  47. package/build-module/dashboard-grid/resolve-fill-widths.mjs.map +7 -0
  48. package/build-module/dashboard-grid/types.mjs +1 -0
  49. package/build-module/dashboard-grid/types.mjs.map +7 -0
  50. package/build-module/dashboard-lanes/index.mjs +547 -0
  51. package/build-module/dashboard-lanes/index.mjs.map +7 -0
  52. package/build-module/dashboard-lanes/lane-placement.mjs +85 -0
  53. package/build-module/dashboard-lanes/lane-placement.mjs.map +7 -0
  54. package/build-module/dashboard-lanes/lanes-item.mjs +260 -0
  55. package/build-module/dashboard-lanes/lanes-item.mjs.map +7 -0
  56. package/build-module/dashboard-lanes/types.mjs +1 -0
  57. package/build-module/dashboard-lanes/types.mjs.map +7 -0
  58. package/build-module/dashboard-lanes/use-lane-placement.mjs +181 -0
  59. package/build-module/dashboard-lanes/use-lane-placement.mjs.map +7 -0
  60. package/build-module/index.mjs +8 -0
  61. package/build-module/index.mjs.map +7 -0
  62. package/build-module/shared/drag-overlay-drop-animation.mjs +47 -0
  63. package/build-module/shared/drag-overlay-drop-animation.mjs.map +7 -0
  64. package/build-module/shared/grid-item-key.mjs +6 -0
  65. package/build-module/shared/grid-item-key.mjs.map +7 -0
  66. package/build-module/shared/grid-overlay.mjs +152 -0
  67. package/build-module/shared/grid-overlay.mjs.map +7 -0
  68. package/build-module/shared/item-exit-overlay.mjs +125 -0
  69. package/build-module/shared/item-exit-overlay.mjs.map +7 -0
  70. package/build-module/shared/resize-handle.mjs +193 -0
  71. package/build-module/shared/resize-handle.mjs.map +7 -0
  72. package/build-module/shared/resize-snap.mjs +21 -0
  73. package/build-module/shared/resize-snap.mjs.map +7 -0
  74. package/build-module/shared/types.mjs +1 -0
  75. package/build-module/shared/types.mjs.map +7 -0
  76. package/build-module/shared/use-item-exit-animation.mjs +128 -0
  77. package/build-module/shared/use-item-exit-animation.mjs.map +7 -0
  78. package/build-module/shared/use-layout-shift-animation.mjs +140 -0
  79. package/build-module/shared/use-layout-shift-animation.mjs.map +7 -0
  80. package/build-types/dashboard-grid/grid-item.d.ts +3 -0
  81. package/build-types/dashboard-grid/grid-item.d.ts.map +1 -0
  82. package/build-types/dashboard-grid/index.d.ts +35 -0
  83. package/build-types/dashboard-grid/index.d.ts.map +1 -0
  84. package/build-types/dashboard-grid/resolve-fill-widths.d.ts +26 -0
  85. package/build-types/dashboard-grid/resolve-fill-widths.d.ts.map +1 -0
  86. package/build-types/dashboard-grid/stories/index.story.d.ts +98 -0
  87. package/build-types/dashboard-grid/stories/index.story.d.ts.map +1 -0
  88. package/build-types/dashboard-grid/types.d.ts +232 -0
  89. package/build-types/dashboard-grid/types.d.ts.map +1 -0
  90. package/build-types/dashboard-lanes/index.d.ts +40 -0
  91. package/build-types/dashboard-lanes/index.d.ts.map +1 -0
  92. package/build-types/dashboard-lanes/lane-placement.d.ts +126 -0
  93. package/build-types/dashboard-lanes/lane-placement.d.ts.map +1 -0
  94. package/build-types/dashboard-lanes/lanes-item.d.ts +52 -0
  95. package/build-types/dashboard-lanes/lanes-item.d.ts.map +1 -0
  96. package/build-types/dashboard-lanes/stories/index.story.d.ts +64 -0
  97. package/build-types/dashboard-lanes/stories/index.story.d.ts.map +1 -0
  98. package/build-types/dashboard-lanes/types.d.ts +151 -0
  99. package/build-types/dashboard-lanes/types.d.ts.map +1 -0
  100. package/build-types/dashboard-lanes/use-lane-placement.d.ts +74 -0
  101. package/build-types/dashboard-lanes/use-lane-placement.d.ts.map +1 -0
  102. package/build-types/index.d.ts +6 -0
  103. package/build-types/index.d.ts.map +1 -0
  104. package/build-types/shared/drag-overlay-drop-animation.d.ts +13 -0
  105. package/build-types/shared/drag-overlay-drop-animation.d.ts.map +1 -0
  106. package/build-types/shared/grid-item-key.d.ts +6 -0
  107. package/build-types/shared/grid-item-key.d.ts.map +1 -0
  108. package/build-types/shared/grid-overlay.d.ts +19 -0
  109. package/build-types/shared/grid-overlay.d.ts.map +1 -0
  110. package/build-types/shared/item-exit-overlay.d.ts +20 -0
  111. package/build-types/shared/item-exit-overlay.d.ts.map +1 -0
  112. package/build-types/shared/resize-handle.d.ts +23 -0
  113. package/build-types/shared/resize-handle.d.ts.map +1 -0
  114. package/build-types/shared/resize-snap.d.ts +41 -0
  115. package/build-types/shared/resize-snap.d.ts.map +1 -0
  116. package/build-types/shared/types.d.ts +144 -0
  117. package/build-types/shared/types.d.ts.map +1 -0
  118. package/build-types/shared/use-item-exit-animation.d.ts +37 -0
  119. package/build-types/shared/use-item-exit-animation.d.ts.map +1 -0
  120. package/build-types/shared/use-layout-shift-animation.d.ts +77 -0
  121. package/build-types/shared/use-layout-shift-animation.d.ts.map +1 -0
  122. package/package.json +80 -0
  123. package/src/dashboard-grid/grid-item.module.css +94 -0
  124. package/src/dashboard-grid/grid-item.tsx +205 -0
  125. package/src/dashboard-grid/grid.module.css +134 -0
  126. package/src/dashboard-grid/index.tsx +713 -0
  127. package/src/dashboard-grid/resolve-fill-widths.ts +224 -0
  128. package/src/dashboard-grid/stories/index.story.tsx +930 -0
  129. package/src/dashboard-grid/test/keyboard-activation.test.tsx +76 -0
  130. package/src/dashboard-grid/test/resolve-fill-widths.test.ts +250 -0
  131. package/src/dashboard-grid/types.ts +271 -0
  132. package/src/dashboard-lanes/index.tsx +629 -0
  133. package/src/dashboard-lanes/lane-placement.ts +245 -0
  134. package/src/dashboard-lanes/lanes-item.module.css +93 -0
  135. package/src/dashboard-lanes/lanes-item.tsx +236 -0
  136. package/src/dashboard-lanes/lanes.module.css +152 -0
  137. package/src/dashboard-lanes/stories/index.story.tsx +518 -0
  138. package/src/dashboard-lanes/test/keyboard-activation.test.tsx +71 -0
  139. package/src/dashboard-lanes/test/lane-placement.test.ts +442 -0
  140. package/src/dashboard-lanes/test/use-lane-placement.test.tsx +358 -0
  141. package/src/dashboard-lanes/types.ts +176 -0
  142. package/src/dashboard-lanes/use-lane-placement.ts +313 -0
  143. package/src/index.ts +17 -0
  144. package/src/shared/actionable-area-slot.module.css +16 -0
  145. package/src/shared/drag-overlay-drop-animation.ts +66 -0
  146. package/src/shared/grid-item-key.ts +5 -0
  147. package/src/shared/grid-overlay.module.css +82 -0
  148. package/src/shared/grid-overlay.tsx +93 -0
  149. package/src/shared/item-exit-animation.module.css +49 -0
  150. package/src/shared/item-exit-overlay.tsx +57 -0
  151. package/src/shared/layout-shift-animation.module.css +16 -0
  152. package/src/shared/resize-handle.module.css +88 -0
  153. package/src/shared/resize-handle.tsx +163 -0
  154. package/src/shared/resize-snap.ts +63 -0
  155. package/src/shared/test/resize-snap.test.ts +35 -0
  156. package/src/shared/types.ts +164 -0
  157. package/src/shared/use-item-exit-animation.ts +199 -0
  158. 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
+ }