@wordpress/grid 0.1.1-next.v.202606191442.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,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,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
|
+
}
|