@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,313 @@
1
+ /**
2
+ * WordPress dependencies
3
+ */
4
+ import { useState, useLayoutEffect, useMemo } from '@wordpress/element';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import { computeLanePlacements } from './lane-placement';
10
+ import { GRID_ITEM_DATA_KEY } from '../shared/grid-item-key';
11
+
12
+ const DEFAULT_ROW_UNIT = 4;
13
+
14
+ function supportsGridLanes(): boolean {
15
+ if ( typeof CSS === 'undefined' || ! CSS.supports ) {
16
+ return false;
17
+ }
18
+ return CSS.supports( 'display', 'grid-lanes' );
19
+ }
20
+
21
+ function clampSpan( span: number | undefined ): number {
22
+ if ( typeof span !== 'number' || ! Number.isFinite( span ) ) {
23
+ return 1;
24
+ }
25
+ return Math.max( 1, Math.floor( span ) );
26
+ }
27
+
28
+ /**
29
+ * Logical item passed to the hook. The renderer is responsible for
30
+ * mounting a DOM node with `data-wp-grid-item-key={ item.key }` for each
31
+ * entry; the hook will measure that node and produce inline styles.
32
+ */
33
+ export type LaneItemInput = {
34
+ key: string;
35
+ span?: number;
36
+ lane?: number;
37
+ };
38
+
39
+ export type UseLanePlacementInput = {
40
+ items: ReadonlyArray< LaneItemInput >;
41
+ lanes: number;
42
+ gap: number;
43
+ flowTolerance: number;
44
+ /**
45
+ * Snap unit for `grid-row-start` / `grid-row-end: span N` math.
46
+ * Smaller values produce sharper placement at the cost of more
47
+ * implicit rows. Defaults to 4 (px).
48
+ */
49
+ rowUnit?: number;
50
+ };
51
+
52
+ export type UseLanePlacementResult = {
53
+ /**
54
+ * Inline styles to apply to each item, keyed by item key. On
55
+ * native (`display: grid-lanes`), entries carry only
56
+ * `gridColumn: span N`; the browser handles row placement. While
57
+ * polyfilling, entries also carry explicit `grid-column-start` /
58
+ * `grid-row-*` values.
59
+ */
60
+ itemStyles: Map< string, React.CSSProperties >;
61
+
62
+ /**
63
+ * `false` when the host browser supports `display: grid-lanes`
64
+ * natively. The hook avoids mounting any observers in that case.
65
+ */
66
+ isPolyfilled: boolean;
67
+ };
68
+
69
+ /**
70
+ * Hook that measures item heights and resolves their placement when
71
+ * `display: grid-lanes` is unavailable, falling through to a no-op
72
+ * pass when the host browser supports the feature natively.
73
+ *
74
+ * Usage from the renderer:
75
+ *
76
+ * ```tsx
77
+ * const [ container, setContainer ] = useState< HTMLDivElement | null >( null );
78
+ * const { itemStyles } = useLanePlacement( container, {
79
+ * items: layout,
80
+ * lanes: columns,
81
+ * gap: gapPx,
82
+ * flowTolerance: 16,
83
+ * } );
84
+ *
85
+ * return (
86
+ * <div ref={ setContainer } style={ { display: 'grid-lanes', ... } }>
87
+ * { items.map( ( item ) => (
88
+ * <div
89
+ * key={ item.key }
90
+ * data-wp-grid-item-key={ item.key }
91
+ * style={ itemStyles.get( item.key ) }
92
+ * >
93
+ * { ... }
94
+ * </div>
95
+ * ) ) }
96
+ * </div>
97
+ * );
98
+ * ```
99
+ *
100
+ * @param container HTMLElement (or null pre-mount) hosting the items.
101
+ * @param input Logical items, lane count, gap, and tuning.
102
+ * @return Per-item styles plus the `isPolyfilled` flag.
103
+ */
104
+ export function useLanePlacement(
105
+ container: HTMLElement | null,
106
+ input: UseLanePlacementInput
107
+ ): UseLanePlacementResult {
108
+ // Detect once at mount. SSR returns `true` (CSS undefined); the
109
+ // client-first render returns the real value. Either path produces
110
+ // the same DOM until the polyfill effect runs (both emit
111
+ // span-only styles), so there is no hydration mismatch.
112
+ const [ isPolyfilled ] = useState( () => ! supportsGridLanes() );
113
+
114
+ const [ itemStyles, setItemStyles ] = useState<
115
+ Map< string, React.CSSProperties >
116
+ >( () => new Map() );
117
+
118
+ // Native pass-through: items only need their column span; the
119
+ // browser handles row placement. Memoized so a stable items
120
+ // array yields a stable Map identity.
121
+ const nativeStyles = useMemo( () => {
122
+ const map = new Map< string, React.CSSProperties >();
123
+ for ( const item of input.items ) {
124
+ map.set( item.key, {
125
+ gridColumn: `span ${ clampSpan( item.span ) }`,
126
+ } );
127
+ }
128
+ return map;
129
+ }, [ input.items ] );
130
+
131
+ // Stable signature of items for deps. Keys, spans, and explicit
132
+ // lanes are the only fields that influence observer wiring or
133
+ // placement, so we hash exactly those.
134
+ const itemsSignature = useMemo( () => {
135
+ return input.items
136
+ .map(
137
+ ( item ) =>
138
+ `${ item.key }/${ item.span ?? 1 }/${ item.lane ?? '' }`
139
+ )
140
+ .join( '\0' );
141
+ }, [ input.items ] );
142
+
143
+ // Stable array identity while placement-relevant fields match
144
+ // `itemsSignature`, so the layout effect is not torn down on every
145
+ // parent re-render that passes a fresh `items` reference.
146
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- `itemsSignature` encodes keys/spans/lanes; `input.items` reference often changes without placement changes.
147
+ const itemsForPlacement = useMemo( () => input.items, [ itemsSignature ] );
148
+
149
+ const { lanes, gap, flowTolerance, rowUnit } = input;
150
+
151
+ useLayoutEffect( () => {
152
+ if ( ! isPolyfilled || ! container ) {
153
+ return;
154
+ }
155
+ if ( typeof ResizeObserver === 'undefined' ) {
156
+ return;
157
+ }
158
+
159
+ const heights = new Map< string, number >();
160
+ const observed = new Set< Element >();
161
+ let cancelled = false;
162
+ let rafId: number | null = null;
163
+
164
+ const recompute = () => {
165
+ if ( rafId !== null || cancelled ) {
166
+ return;
167
+ }
168
+ // One layout per frame even when ResizeObserver and
169
+ // MutationObserver fire in the same tick.
170
+ rafId = requestAnimationFrame( () => {
171
+ rafId = null;
172
+ if ( cancelled ) {
173
+ return;
174
+ }
175
+ const itemsWithHeight = itemsForPlacement.map( ( item ) => ( {
176
+ key: item.key,
177
+ span: clampSpan( item.span ),
178
+ lane: item.lane,
179
+ height: heights.get( item.key ) ?? 0,
180
+ } ) );
181
+ const result = computeLanePlacements( {
182
+ items: itemsWithHeight,
183
+ lanes,
184
+ gap,
185
+ flowTolerance,
186
+ } );
187
+ const effectiveRowUnit = Math.max(
188
+ 1,
189
+ rowUnit ?? DEFAULT_ROW_UNIT
190
+ );
191
+ const next = new Map< string, React.CSSProperties >();
192
+ for ( const item of itemsForPlacement ) {
193
+ const placement = result.placements.get( item.key );
194
+ if ( ! placement ) {
195
+ continue;
196
+ }
197
+ const height = heights.get( item.key ) ?? 0;
198
+ const rowStart =
199
+ Math.floor( placement.top / effectiveRowUnit ) + 1;
200
+ const rowSpan = Math.max(
201
+ 1,
202
+ Math.ceil( height / effectiveRowUnit )
203
+ );
204
+ next.set( item.key, {
205
+ gridColumnStart: placement.lane + 1,
206
+ gridColumnEnd: `span ${ placement.span }`,
207
+ gridRowStart: rowStart,
208
+ gridRowEnd: `span ${ rowSpan }`,
209
+ } );
210
+ }
211
+ setItemStyles( next );
212
+ } );
213
+ };
214
+
215
+ const resizeObserver = new ResizeObserver( ( entries ) => {
216
+ let changed = false;
217
+ for ( const entry of entries ) {
218
+ const key = ( entry.target as HTMLElement ).getAttribute(
219
+ GRID_ITEM_DATA_KEY
220
+ );
221
+ if ( ! key ) {
222
+ continue;
223
+ }
224
+ const newHeight = entry.contentRect.height;
225
+ if ( heights.get( key ) !== newHeight ) {
226
+ heights.set( key, newHeight );
227
+ changed = true;
228
+ }
229
+ }
230
+ if ( changed ) {
231
+ recompute();
232
+ }
233
+ } );
234
+
235
+ const refreshObserved = () => {
236
+ const current = container.querySelectorAll(
237
+ `[${ GRID_ITEM_DATA_KEY }]`
238
+ );
239
+ for ( const element of current ) {
240
+ if ( ! observed.has( element ) ) {
241
+ observed.add( element );
242
+ resizeObserver.observe( element );
243
+ const key = element.getAttribute( GRID_ITEM_DATA_KEY );
244
+ if ( key ) {
245
+ const rect = (
246
+ element as HTMLElement
247
+ ).getBoundingClientRect();
248
+ heights.set( key, rect.height );
249
+ }
250
+ }
251
+ }
252
+ for ( const element of observed ) {
253
+ if ( ! container.contains( element ) ) {
254
+ resizeObserver.unobserve( element );
255
+ observed.delete( element );
256
+ }
257
+ }
258
+ };
259
+
260
+ // Children may mount, unmount, or change `data-wp-grid-item-key`
261
+ // after the container exists (drag reorders, additions). The
262
+ // mutation observer keeps the observed set in sync.
263
+ const mutationObserver =
264
+ typeof MutationObserver !== 'undefined'
265
+ ? new MutationObserver( () => {
266
+ refreshObserved();
267
+ recompute();
268
+ } )
269
+ : null;
270
+ if ( mutationObserver ) {
271
+ mutationObserver.observe( container, {
272
+ childList: true,
273
+ subtree: true,
274
+ attributes: true,
275
+ attributeFilter: [ GRID_ITEM_DATA_KEY ],
276
+ } );
277
+ }
278
+
279
+ refreshObserved();
280
+ recompute();
281
+
282
+ return () => {
283
+ cancelled = true;
284
+ if ( rafId !== null ) {
285
+ cancelAnimationFrame( rafId );
286
+ }
287
+ resizeObserver.disconnect();
288
+ mutationObserver?.disconnect();
289
+ };
290
+ }, [
291
+ container,
292
+ isPolyfilled,
293
+ lanes,
294
+ gap,
295
+ flowTolerance,
296
+ rowUnit,
297
+ itemsForPlacement,
298
+ ] );
299
+
300
+ if ( ! isPolyfilled ) {
301
+ return { itemStyles: nativeStyles, isPolyfilled: false };
302
+ }
303
+ if ( itemStyles.size === 0 ) {
304
+ // Pre-measurement frame: emit native-shape styles so items
305
+ // appear in their default span rather than collapsing to 1
306
+ // column at the top-left.
307
+ return {
308
+ itemStyles: nativeStyles as Map< string, React.CSSProperties >,
309
+ isPolyfilled: true,
310
+ };
311
+ }
312
+ return { itemStyles, isPolyfilled: true };
313
+ }
package/src/index.ts ADDED
@@ -0,0 +1,17 @@
1
+ export { DashboardGrid } from './dashboard-grid';
2
+ export { DashboardLanes } from './dashboard-lanes';
3
+
4
+ export type {
5
+ DashboardGridLayoutItem,
6
+ DashboardGridProps,
7
+ } from './dashboard-grid/types';
8
+ export type {
9
+ DashboardLanesLayoutItem,
10
+ DashboardLanesProps,
11
+ } from './dashboard-lanes/types';
12
+ export type {
13
+ DragPreviewRenderProps,
14
+ GridOverlayRenderProps,
15
+ ResizeDelta,
16
+ ResizeHandleRenderProps,
17
+ } from './shared/types';
@@ -0,0 +1,16 @@
1
+ .actionable-area-slot {
2
+ opacity: 1;
3
+ }
4
+
5
+ @media (prefers-reduced-motion: no-preference) {
6
+ .actionable-area-slot {
7
+ transition:
8
+ opacity var(--wpds-motion-duration-md)
9
+ var(--wpds-motion-easing-subtle);
10
+ }
11
+ }
12
+
13
+ :global([data-wp-grid-resizing]) .actionable-area-slot {
14
+ opacity: 0;
15
+ pointer-events: none;
16
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import {
5
+ defaultDropAnimation,
6
+ defaultDropAnimationSideEffects,
7
+ } from '@dnd-kit/core';
8
+ import type { DropAnimation } from '@dnd-kit/core';
9
+
10
+ /** Matches `--wpds-motion-duration-md` on the drag preview frame exit. */
11
+ export const DROP_ANIMATION_DURATION_MS = 200;
12
+
13
+ /** Matches `--wpds-motion-easing-balanced` on the drag preview frame exit. */
14
+ const DROP_ANIMATION_EASING = 'cubic-bezier(0.4, 0, 0.2, 1)';
15
+
16
+ /**
17
+ * Composes @dnd-kit/core’s default overlay drop translation with preview
18
+ * exit keyframes (via side effects). When the pointer never moves, @dnd-kit
19
+ * skips the drop animation and these side effects do not run.
20
+ *
21
+ * @param dragPreviewFrameClassName Hashed class for `.drag-preview-frame`.
22
+ * @param exitingFrameClassName Hashed class for the exit state.
23
+ */
24
+ export function createDashboardDragDropAnimation(
25
+ dragPreviewFrameClassName: string,
26
+ exitingFrameClassName: string
27
+ ): DropAnimation {
28
+ return {
29
+ ...defaultDropAnimation,
30
+ duration: DROP_ANIMATION_DURATION_MS,
31
+ easing: DROP_ANIMATION_EASING,
32
+ sideEffects( args ) {
33
+ const cleanupDefault = defaultDropAnimationSideEffects( {
34
+ styles: {
35
+ active: {
36
+ opacity: '0',
37
+ },
38
+ },
39
+ } )( args );
40
+
41
+ const frame = args.dragOverlay.node.getElementsByClassName(
42
+ dragPreviewFrameClassName
43
+ )[ 0 ] as HTMLElement | undefined;
44
+
45
+ if ( frame ) {
46
+ frame
47
+ .getAnimations()
48
+ .forEach( ( animation ) => animation.cancel() );
49
+ const lift = frame.firstElementChild;
50
+ if ( lift instanceof HTMLElement ) {
51
+ lift.getAnimations().forEach( ( animation ) =>
52
+ animation.cancel()
53
+ );
54
+ }
55
+ frame.classList.add( exitingFrameClassName );
56
+ }
57
+
58
+ return () => {
59
+ cleanupDefault?.();
60
+ if ( frame ) {
61
+ frame.classList.remove( exitingFrameClassName );
62
+ }
63
+ };
64
+ },
65
+ };
66
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Data attribute grid tiles declare so layout-shift animation can map
3
+ * measured DOM nodes back to logical item keys.
4
+ */
5
+ export const GRID_ITEM_DATA_KEY = 'data-wp-grid-item-key';
@@ -0,0 +1,82 @@
1
+ .overlay {
2
+ position: absolute;
3
+ inset: 0;
4
+ display: grid;
5
+ gap: var(--wp-grid-gap, var(--wpds-dimension-gap-xl));
6
+ pointer-events: none;
7
+ opacity: 0;
8
+ visibility: hidden;
9
+ }
10
+
11
+ .overlay.is-active {
12
+ opacity: 1;
13
+ visibility: visible;
14
+ }
15
+
16
+ /*
17
+ * Alpha wave: row-marker tiles (or column tracks on lanes) reveal in a
18
+ * diagonal sweep from the top-left using staggered delays.
19
+ */
20
+ @media not (prefers-reduced-motion) {
21
+ .overlay.is-active .row,
22
+ .overlay.is-active:not(.has-rows) .column {
23
+ opacity: 0;
24
+ transform: scale(0.64);
25
+ animation:
26
+ grid-overlay-tile-in
27
+ var(--wpds-motion-duration-md) var(--wpds-motion-easing-subtle)
28
+ both;
29
+ animation-delay: calc(var(--wp-grid-overlay-wave-delay-step, 28ms) * (var(--wp-grid-overlay-column-index, 0) + var(--wp-grid-overlay-row-index, 0)));
30
+ }
31
+
32
+ @keyframes grid-overlay-tile-in {
33
+ to {
34
+ opacity: 1;
35
+ transform: scale(1);
36
+ }
37
+ }
38
+
39
+ .overlay:not(.is-active) {
40
+ transition:
41
+ opacity var(--wpds-motion-duration-sm)
42
+ var(--wpds-motion-easing-subtle),
43
+ visibility 0s linear var(--wpds-motion-duration-sm);
44
+ }
45
+
46
+ .overlay:not(.is-active) .row,
47
+ .overlay:not(.is-active) .column {
48
+ animation: none;
49
+ transform: none;
50
+ }
51
+ }
52
+
53
+ @media (prefers-reduced-motion: reduce) {
54
+ .overlay {
55
+ transition:
56
+ opacity var(--wpds-motion-duration-sm)
57
+ var(--wpds-motion-easing-subtle),
58
+ visibility 0s linear var(--wpds-motion-duration-sm);
59
+ }
60
+
61
+ .overlay.is-active {
62
+ transition:
63
+ opacity var(--wpds-motion-duration-sm)
64
+ var(--wpds-motion-easing-subtle),
65
+ visibility 0s linear 0s;
66
+ }
67
+ }
68
+
69
+ .column {
70
+ display: flex;
71
+ flex-direction: column;
72
+ gap: var(--wp-grid-gap, var(--wpds-dimension-gap-xl));
73
+ min-width: 0;
74
+ }
75
+
76
+ .row {
77
+ flex: 0 0 var(--wp-grid-overlay-row-height);
78
+ height: var(--wp-grid-overlay-row-height);
79
+ box-sizing: border-box;
80
+ border-radius: var(--wpds-border-radius-lg);
81
+ background-color: var(--wp-grid-overlay-tile-bg, var(--wpds-color-background-surface-neutral-weak));
82
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import clsx from 'clsx';
5
+
6
+ /**
7
+ * WordPress dependencies
8
+ */
9
+ import { useEffect, useState } from '@wordpress/element';
10
+
11
+ /**
12
+ * Internal dependencies
13
+ */
14
+ import type { GridOverlayRenderProps } from './types';
15
+ import styles from './grid-overlay.module.css';
16
+
17
+ /**
18
+ * Default edit-mode overlay: one column per track, each holding `rows`
19
+ * row-marker tiles when `rowHeight` is uniform. Used by both surfaces
20
+ * and replaceable via `renderGridOverlay`. Reveals with a diagonal
21
+ * wave on activate and releases paint cost while inactive.
22
+ *
23
+ * @param props Render props supplied by the surface.
24
+ * @param props.columns Column tracks to mirror.
25
+ * @param props.rowHeight Uniform row height in pixels; omitted for
26
+ * content-sized rows, which skip row markers.
27
+ * @param props.rows Row tracks per column; omitted when unknown.
28
+ * @param props.isActive When `false`, the overlay fades out.
29
+ */
30
+ export function GridOverlay( {
31
+ columns,
32
+ rowHeight,
33
+ rows,
34
+ isActive,
35
+ }: GridOverlayRenderProps ) {
36
+ const showRows =
37
+ typeof rowHeight === 'number' && typeof rows === 'number' && rows > 0;
38
+ // Bump the key when edit mode activates so CSS animations restart on
39
+ // each enter (the overlay stays mounted across toggles).
40
+ const [ waveKey, setWaveKey ] = useState( 0 );
41
+ useEffect( () => {
42
+ if ( isActive ) {
43
+ setWaveKey( ( key ) => key + 1 );
44
+ }
45
+ }, [ isActive ] );
46
+ const style: React.CSSProperties = {
47
+ gridTemplateColumns: `repeat(${ columns }, minmax(0, 1fr))`,
48
+ ...( showRows
49
+ ? ( {
50
+ '--wp-grid-overlay-row-height': `${ rowHeight }px`,
51
+ } as React.CSSProperties )
52
+ : {} ),
53
+ };
54
+
55
+ return (
56
+ <div
57
+ key={ waveKey }
58
+ aria-hidden
59
+ className={ clsx(
60
+ styles.overlay,
61
+ isActive && styles[ 'is-active' ],
62
+ showRows && styles[ 'has-rows' ]
63
+ ) }
64
+ style={ style }
65
+ >
66
+ { Array.from( { length: columns }, ( _column, columnIndex ) => (
67
+ <div
68
+ key={ columnIndex }
69
+ className={ styles.column }
70
+ style={
71
+ {
72
+ '--wp-grid-overlay-column-index': columnIndex,
73
+ '--wp-grid-overlay-row-index': 0,
74
+ } as React.CSSProperties
75
+ }
76
+ >
77
+ { showRows &&
78
+ Array.from( { length: rows }, ( _row, rowIndex ) => (
79
+ <div
80
+ key={ rowIndex }
81
+ className={ styles.row }
82
+ style={
83
+ {
84
+ '--wp-grid-overlay-row-index': rowIndex,
85
+ } as React.CSSProperties
86
+ }
87
+ />
88
+ ) ) }
89
+ </div>
90
+ ) ) }
91
+ </div>
92
+ );
93
+ }
@@ -0,0 +1,49 @@
1
+ /*
2
+ * Absolutely positioned clone of a removed tile. Siblings reflow via
3
+ * FLIP while this overlay scales down and fades out.
4
+ */
5
+ .exit-overlay {
6
+ position: absolute;
7
+ pointer-events: none;
8
+ z-index: 2;
9
+ overflow: hidden;
10
+ transform-origin: center center;
11
+ opacity: 1;
12
+ transform: scale(1);
13
+ }
14
+
15
+ @media not (prefers-reduced-motion: reduce) {
16
+ .exit-overlay {
17
+ animation:
18
+ wp-grid-item-exit-opacity var(--wpds-motion-duration-md)
19
+ var(--wpds-motion-easing-subtle) forwards,
20
+ wp-grid-item-exit-scale var(--wpds-motion-duration-md)
21
+ var(--wpds-motion-easing-balanced) forwards;
22
+ }
23
+ }
24
+
25
+ @keyframes wp-grid-item-exit-opacity {
26
+ from {
27
+ opacity: 1;
28
+ }
29
+
30
+ to {
31
+ opacity: 0;
32
+ }
33
+ }
34
+
35
+ @keyframes wp-grid-item-exit-scale {
36
+ from {
37
+ transform: scale(1);
38
+ }
39
+
40
+ to {
41
+ transform: scale(0.88);
42
+ }
43
+ }
44
+
45
+ @media (prefers-reduced-motion: reduce) {
46
+ .exit-overlay {
47
+ display: none;
48
+ }
49
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ import { GRID_ITEM_DATA_KEY } from './grid-item-key';
5
+ import exitStyles from './item-exit-animation.module.css';
6
+ import type { RectSnapshot } from './use-layout-shift-animation';
7
+
8
+ export type ItemExitOverlayRect = Pick<
9
+ RectSnapshot,
10
+ 'left' | 'top' | 'width' | 'height'
11
+ >;
12
+
13
+ export type ItemExitOverlayProps = {
14
+ itemKey: string;
15
+ rect: ItemExitOverlayRect;
16
+ children: React.ReactNode;
17
+ onAnimationEnd: () => void;
18
+ };
19
+
20
+ /**
21
+ * Ghost tile shown at the removed item's last position while siblings
22
+ * reflow. Not a sortable grid cell — only visual exit feedback.
23
+ *
24
+ * @param root0 Component props.
25
+ * @param root0.itemKey Layout key of the removed tile.
26
+ * @param root0.rect Last bounds relative to the grid surface.
27
+ * @param root0.children Cached tile content to render in the ghost.
28
+ * @param root0.onAnimationEnd Called when the exit animation finishes.
29
+ */
30
+ export function ItemExitOverlay( {
31
+ itemKey,
32
+ rect,
33
+ children,
34
+ onAnimationEnd,
35
+ }: ItemExitOverlayProps ) {
36
+ return (
37
+ <div
38
+ className={ exitStyles[ 'exit-overlay' ] }
39
+ style={ {
40
+ left: rect.left,
41
+ top: rect.top,
42
+ width: rect.width,
43
+ height: rect.height,
44
+ } }
45
+ { ...{ [ GRID_ITEM_DATA_KEY ]: itemKey } }
46
+ data-wp-grid-item-exiting=""
47
+ onAnimationEnd={ ( event ) => {
48
+ if ( event.target !== event.currentTarget ) {
49
+ return;
50
+ }
51
+ onAnimationEnd();
52
+ } }
53
+ >
54
+ { children }
55
+ </div>
56
+ );
57
+ }