@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,629 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import {
|
|
5
|
+
DndContext,
|
|
6
|
+
DragOverlay,
|
|
7
|
+
KeyboardSensor,
|
|
8
|
+
PointerSensor,
|
|
9
|
+
useSensor,
|
|
10
|
+
useSensors,
|
|
11
|
+
} from '@dnd-kit/core';
|
|
12
|
+
import {
|
|
13
|
+
arrayMove,
|
|
14
|
+
SortableContext,
|
|
15
|
+
sortableKeyboardCoordinates,
|
|
16
|
+
} from '@dnd-kit/sortable';
|
|
17
|
+
import type { DragMoveEvent, DragStartEvent } from '@dnd-kit/core';
|
|
18
|
+
import clsx from 'clsx';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* WordPress dependencies
|
|
22
|
+
*/
|
|
23
|
+
import { useResizeObserver, useEvent, useMergeRefs } from '@wordpress/compose';
|
|
24
|
+
import {
|
|
25
|
+
forwardRef,
|
|
26
|
+
useMemo,
|
|
27
|
+
Children,
|
|
28
|
+
cloneElement,
|
|
29
|
+
isValidElement,
|
|
30
|
+
useLayoutEffect,
|
|
31
|
+
useRef,
|
|
32
|
+
useState,
|
|
33
|
+
} from '@wordpress/element';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Internal dependencies
|
|
37
|
+
*/
|
|
38
|
+
import { LanesItem } from './lanes-item';
|
|
39
|
+
import { useLanePlacement } from './use-lane-placement';
|
|
40
|
+
import { GridOverlay } from '../shared/grid-overlay';
|
|
41
|
+
import { gridSpanToPixelSize } from '../shared/resize-snap';
|
|
42
|
+
import layoutAnimationStyles from '../shared/layout-shift-animation.module.css';
|
|
43
|
+
import { ItemExitOverlay } from '../shared/item-exit-overlay';
|
|
44
|
+
import {
|
|
45
|
+
getLayoutFingerprint,
|
|
46
|
+
getPlacementFingerprint,
|
|
47
|
+
useLayoutShiftAnimation,
|
|
48
|
+
} from '../shared/use-layout-shift-animation';
|
|
49
|
+
import { useItemExitAnimation } from '../shared/use-item-exit-animation';
|
|
50
|
+
import type { DashboardLanesLayoutItem, DashboardLanesProps } from './types';
|
|
51
|
+
import type { ResizeSnapSize } from '../shared/resize-snap';
|
|
52
|
+
import type { ResizeDelta } from '../shared/types';
|
|
53
|
+
import { createDashboardDragDropAnimation } from '../shared/drag-overlay-drop-animation';
|
|
54
|
+
import styles from './lanes.module.css';
|
|
55
|
+
|
|
56
|
+
const dashboardDragDropAnimation = createDashboardDragDropAnimation(
|
|
57
|
+
styles[ 'drag-preview-frame' ],
|
|
58
|
+
styles.dragPreviewFrameExiting
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Fallback gap in pixels for math that runs before the computed gap
|
|
62
|
+
// can be read from the DOM. Matches the `'xl'` step the surface
|
|
63
|
+
// resolves to in CSS (`--wpds-dimension-gap-xl`); the next layout
|
|
64
|
+
// effect overwrites this with the actual computed value.
|
|
65
|
+
const FALLBACK_GAP_PX = 24;
|
|
66
|
+
|
|
67
|
+
// Default lane cap when no explicit `columns` or `minColumnWidth` is
|
|
68
|
+
// supplied. Layered semantics: `columns` acts as a cap and
|
|
69
|
+
// `minColumnWidth` as a per-tile floor; if neither is set we still
|
|
70
|
+
// need a finite count to render against.
|
|
71
|
+
const DEFAULT_COLUMNS = 6;
|
|
72
|
+
|
|
73
|
+
const NO_SORT_STRATEGY = () => null;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Masonry-style surface aligned with `display: grid-lanes`. Items
|
|
77
|
+
* declare a column span; heights are driven by content; placement
|
|
78
|
+
* follows the source-ordered, shortest-lane algorithm with
|
|
79
|
+
* `flow-tolerance` tiebreaking.
|
|
80
|
+
*
|
|
81
|
+
* On browsers that support `display: grid-lanes` natively, the
|
|
82
|
+
* component emits the spec's CSS and lets the engine handle layout.
|
|
83
|
+
* Otherwise, `useLanePlacement` measures item heights and assigns
|
|
84
|
+
* explicit `grid-column-start` / `grid-row-start` values that
|
|
85
|
+
* approximate the same result inside CSS Grid.
|
|
86
|
+
*
|
|
87
|
+
* Each child's `key` must match an entry in the `layout` array;
|
|
88
|
+
* children without a match render at the end of the surface without
|
|
89
|
+
* explicit placement and fall through the lanes auto-flow.
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* ```jsx
|
|
93
|
+
* <DashboardLanes
|
|
94
|
+
* layout={ [
|
|
95
|
+
* { key: 'a' },
|
|
96
|
+
* { key: 'b', width: 2 },
|
|
97
|
+
* { key: 'c' },
|
|
98
|
+
* ] }
|
|
99
|
+
* columns={ 3 }
|
|
100
|
+
* editMode
|
|
101
|
+
* onChangeLayout={ setLayout }
|
|
102
|
+
* >
|
|
103
|
+
* <Tile key="a">A</Tile>
|
|
104
|
+
* <Tile key="b">B</Tile>
|
|
105
|
+
* <Tile key="c">C</Tile>
|
|
106
|
+
* </DashboardLanes>
|
|
107
|
+
* ```
|
|
108
|
+
*
|
|
109
|
+
* @param props Component props.
|
|
110
|
+
* @param ref Forwarded to the surface's root `<div>`.
|
|
111
|
+
*/
|
|
112
|
+
export const DashboardLanes = forwardRef< HTMLDivElement, DashboardLanesProps >(
|
|
113
|
+
function DashboardLanes( props, ref ) {
|
|
114
|
+
const {
|
|
115
|
+
layout,
|
|
116
|
+
columns,
|
|
117
|
+
children,
|
|
118
|
+
className,
|
|
119
|
+
style,
|
|
120
|
+
flowTolerance = 16,
|
|
121
|
+
rowUnit = 4,
|
|
122
|
+
minColumnWidth,
|
|
123
|
+
editMode = false,
|
|
124
|
+
onChangeLayout,
|
|
125
|
+
onPreviewLayout,
|
|
126
|
+
renderResizeHandle,
|
|
127
|
+
renderDragPreview,
|
|
128
|
+
renderGridOverlay,
|
|
129
|
+
...divProps
|
|
130
|
+
} = props;
|
|
131
|
+
|
|
132
|
+
const [ temporaryLayout, setTemporaryLayout ] = useState<
|
|
133
|
+
DashboardLanesLayoutItem[] | undefined
|
|
134
|
+
>();
|
|
135
|
+
const [ activeId, setActiveId ] = useState< string | null >( null );
|
|
136
|
+
const [ isResizing, setIsResizing ] = useState( false );
|
|
137
|
+
const [ resizeSnapPreview, setResizeSnapPreview ] = useState< {
|
|
138
|
+
id: string;
|
|
139
|
+
snap: ResizeSnapSize;
|
|
140
|
+
} | null >( null );
|
|
141
|
+
const latestLayoutRef = useRef<
|
|
142
|
+
DashboardLanesLayoutItem[] | undefined
|
|
143
|
+
>();
|
|
144
|
+
const lastReorderCursorRef = useRef< {
|
|
145
|
+
x: number;
|
|
146
|
+
y: number;
|
|
147
|
+
} | null >( null );
|
|
148
|
+
const resizeBaselineRef = useRef< number | null >( null );
|
|
149
|
+
const captureLayoutSnapshotRef = useRef< () => void >( () => {} );
|
|
150
|
+
const childrenCacheRef = useRef< Map< string, React.ReactElement > >(
|
|
151
|
+
new Map()
|
|
152
|
+
);
|
|
153
|
+
const activeLayout = temporaryLayout ?? layout;
|
|
154
|
+
|
|
155
|
+
const [ container, setContainer ] = useState< HTMLDivElement | null >(
|
|
156
|
+
null
|
|
157
|
+
);
|
|
158
|
+
const [ containerWidth, setContainerWidth ] = useState( 0 );
|
|
159
|
+
const [ gapPx, setGapPx ] = useState( FALLBACK_GAP_PX );
|
|
160
|
+
const resizeObserverRef = useResizeObserver(
|
|
161
|
+
( [ { contentRect } ] ) => {
|
|
162
|
+
setContainerWidth( contentRect.width );
|
|
163
|
+
}
|
|
164
|
+
);
|
|
165
|
+
const mergedRootRef = useMergeRefs( [
|
|
166
|
+
setContainer,
|
|
167
|
+
resizeObserverRef,
|
|
168
|
+
ref,
|
|
169
|
+
] );
|
|
170
|
+
|
|
171
|
+
// Measure synchronously before paint and snapshot the computed
|
|
172
|
+
// `column-gap` so the placement math tracks the design-system
|
|
173
|
+
// token under any density.
|
|
174
|
+
useLayoutEffect( () => {
|
|
175
|
+
if ( ! container ) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const { width } = container.getBoundingClientRect();
|
|
179
|
+
if ( width > 0 ) {
|
|
180
|
+
setContainerWidth( width );
|
|
181
|
+
}
|
|
182
|
+
const parsed = Number.parseFloat(
|
|
183
|
+
window.getComputedStyle( container ).columnGap
|
|
184
|
+
);
|
|
185
|
+
if ( Number.isFinite( parsed ) && parsed > 0 ) {
|
|
186
|
+
setGapPx( parsed );
|
|
187
|
+
}
|
|
188
|
+
}, [ container ] );
|
|
189
|
+
const effectiveColumns = useMemo( () => {
|
|
190
|
+
if ( ! minColumnWidth ) {
|
|
191
|
+
return columns ?? DEFAULT_COLUMNS;
|
|
192
|
+
}
|
|
193
|
+
const totalWidthPerColumn = minColumnWidth + gapPx;
|
|
194
|
+
const maxFit = Math.max(
|
|
195
|
+
1,
|
|
196
|
+
Math.floor( ( containerWidth + gapPx ) / totalWidthPerColumn )
|
|
197
|
+
);
|
|
198
|
+
return columns !== undefined ? Math.min( columns, maxFit ) : maxFit;
|
|
199
|
+
}, [ minColumnWidth, gapPx, containerWidth, columns ] );
|
|
200
|
+
const columnWidth =
|
|
201
|
+
( containerWidth - ( effectiveColumns - 1 ) * gapPx ) /
|
|
202
|
+
effectiveColumns;
|
|
203
|
+
const minResizeWidthPx = gridSpanToPixelSize(
|
|
204
|
+
1,
|
|
205
|
+
1,
|
|
206
|
+
columnWidth,
|
|
207
|
+
gapPx,
|
|
208
|
+
null
|
|
209
|
+
).widthPx;
|
|
210
|
+
|
|
211
|
+
const layoutMap = useMemo( () => {
|
|
212
|
+
const map = new Map< string, DashboardLanesLayoutItem >();
|
|
213
|
+
activeLayout.forEach( ( item ) => map.set( item.key, item ) );
|
|
214
|
+
return map;
|
|
215
|
+
}, [ activeLayout ] );
|
|
216
|
+
|
|
217
|
+
// Stable-identity key set for the children walk (see grid.tsx).
|
|
218
|
+
const layoutKeys = useMemo(
|
|
219
|
+
() => new Set( layout.map( ( item ) => item.key ) ),
|
|
220
|
+
[ layout ]
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
// Sorted item keys, identity-stable when the resulting sequence
|
|
224
|
+
// is unchanged (avoids invalidating SortableContext).
|
|
225
|
+
const sortedItems = useMemo(
|
|
226
|
+
() =>
|
|
227
|
+
activeLayout
|
|
228
|
+
.map( ( item, index ) => ( { item, index } ) )
|
|
229
|
+
.sort(
|
|
230
|
+
( a, b ) =>
|
|
231
|
+
( a.item.order ?? a.index ) -
|
|
232
|
+
( b.item.order ?? b.index )
|
|
233
|
+
)
|
|
234
|
+
.map( ( { item } ) => item.key ),
|
|
235
|
+
[ activeLayout ]
|
|
236
|
+
);
|
|
237
|
+
const items = sortedItems;
|
|
238
|
+
|
|
239
|
+
// Placement input for the hook: each item with its clamped span
|
|
240
|
+
// in source (sorted) order. `lane` forwards the optional explicit
|
|
241
|
+
// pin from the layout item; the algorithm clamps out-of-range
|
|
242
|
+
// values, so no surface-level guard is needed.
|
|
243
|
+
const placementItems = useMemo( () => {
|
|
244
|
+
return items.map( ( key ) => {
|
|
245
|
+
const item = layoutMap.get( key );
|
|
246
|
+
const width = item?.width;
|
|
247
|
+
const span =
|
|
248
|
+
typeof width === 'number'
|
|
249
|
+
? Math.max( 1, Math.min( width, effectiveColumns ) )
|
|
250
|
+
: 1;
|
|
251
|
+
return { key, span, lane: item?.lane };
|
|
252
|
+
} );
|
|
253
|
+
}, [ items, layoutMap, effectiveColumns ] );
|
|
254
|
+
|
|
255
|
+
const { itemStyles } = useLanePlacement( container, {
|
|
256
|
+
items: placementItems,
|
|
257
|
+
lanes: effectiveColumns,
|
|
258
|
+
gap: gapPx,
|
|
259
|
+
flowTolerance,
|
|
260
|
+
rowUnit,
|
|
261
|
+
} );
|
|
262
|
+
|
|
263
|
+
const [ childrenMap, actionableAreaMap, remaining, renderedByKey ] =
|
|
264
|
+
useMemo( () => {
|
|
265
|
+
const childMap = new Map< string, React.ReactElement >();
|
|
266
|
+
const actionableMap = new Map< string, React.ReactNode >();
|
|
267
|
+
const rest: React.ReactNode[] = [];
|
|
268
|
+
const byKey = new Map< string, React.ReactElement >();
|
|
269
|
+
|
|
270
|
+
Children.forEach( children, ( child ) => {
|
|
271
|
+
if ( ! isValidElement( child ) ) {
|
|
272
|
+
rest.push( child );
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const key = child.key?.toString();
|
|
276
|
+
if ( ! key ) {
|
|
277
|
+
rest.push( child );
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// Strip `actionableArea` so it does not leak to the DOM;
|
|
282
|
+
// the grid lifts it to a slot separately.
|
|
283
|
+
const { actionableArea } = child.props as {
|
|
284
|
+
actionableArea?: React.ReactNode;
|
|
285
|
+
};
|
|
286
|
+
const stripped =
|
|
287
|
+
actionableArea !== undefined
|
|
288
|
+
? cloneElement(
|
|
289
|
+
child as React.ReactElement< {
|
|
290
|
+
actionableArea?: React.ReactNode;
|
|
291
|
+
} >,
|
|
292
|
+
{ actionableArea: undefined }
|
|
293
|
+
)
|
|
294
|
+
: ( child as React.ReactElement );
|
|
295
|
+
|
|
296
|
+
byKey.set( key, stripped );
|
|
297
|
+
|
|
298
|
+
if ( layoutKeys.has( key ) ) {
|
|
299
|
+
if ( actionableArea !== undefined ) {
|
|
300
|
+
actionableMap.set( key, actionableArea );
|
|
301
|
+
}
|
|
302
|
+
childMap.set( key, stripped );
|
|
303
|
+
} else {
|
|
304
|
+
rest.push( child );
|
|
305
|
+
}
|
|
306
|
+
} );
|
|
307
|
+
|
|
308
|
+
return [ childMap, actionableMap, rest, byKey ];
|
|
309
|
+
}, [ children, layoutKeys ] );
|
|
310
|
+
|
|
311
|
+
// Persist the latest rendered children so a removed tile's content
|
|
312
|
+
// is still available for its exit overlay. Filled from an effect so a
|
|
313
|
+
// discarded render never writes to the cache.
|
|
314
|
+
useLayoutEffect( () => {
|
|
315
|
+
for ( const [ key, child ] of renderedByKey ) {
|
|
316
|
+
childrenCacheRef.current.set( key, child );
|
|
317
|
+
}
|
|
318
|
+
}, [ renderedByKey ] );
|
|
319
|
+
|
|
320
|
+
const sensors = useSensors(
|
|
321
|
+
useSensor( PointerSensor ),
|
|
322
|
+
useSensor( KeyboardSensor, {
|
|
323
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
324
|
+
} )
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
const handleDragStart = useEvent( ( event: DragStartEvent ) => {
|
|
328
|
+
setActiveId( String( event.active.id ) );
|
|
329
|
+
lastReorderCursorRef.current = null;
|
|
330
|
+
} );
|
|
331
|
+
|
|
332
|
+
const handleDragCancel = useEvent( () => {
|
|
333
|
+
setActiveId( null );
|
|
334
|
+
latestLayoutRef.current = undefined;
|
|
335
|
+
lastReorderCursorRef.current = null;
|
|
336
|
+
resizeBaselineRef.current = null;
|
|
337
|
+
setIsResizing( false );
|
|
338
|
+
setResizeSnapPreview( null );
|
|
339
|
+
setTemporaryLayout( undefined );
|
|
340
|
+
} );
|
|
341
|
+
|
|
342
|
+
const handleDragMove = useEvent( ( event: DragMoveEvent ) => {
|
|
343
|
+
const { active, over } = event;
|
|
344
|
+
if ( ! over || active.id === over.id ) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
const activeRect = active.rect.current.translated;
|
|
348
|
+
if ( ! activeRect ) {
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
const activeCenterX = activeRect.left + activeRect.width / 2;
|
|
352
|
+
const activeCenterY = activeRect.top + activeRect.height / 2;
|
|
353
|
+
|
|
354
|
+
const lastCursor = lastReorderCursorRef.current;
|
|
355
|
+
if ( lastCursor ) {
|
|
356
|
+
const dx = activeCenterX - lastCursor.x;
|
|
357
|
+
const dy = activeCenterY - lastCursor.y;
|
|
358
|
+
if ( dx * dx + dy * dy < 100 ) {
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const overCenterX = over.rect.left + over.rect.width / 2;
|
|
364
|
+
const insertAfter = activeCenterX > overCenterX;
|
|
365
|
+
|
|
366
|
+
const currentIndex = items.indexOf( String( active.id ) );
|
|
367
|
+
const overIndex = items.indexOf( String( over.id ) );
|
|
368
|
+
let newIndex: number;
|
|
369
|
+
if ( insertAfter ) {
|
|
370
|
+
newIndex = currentIndex > overIndex ? overIndex + 1 : overIndex;
|
|
371
|
+
} else {
|
|
372
|
+
newIndex = currentIndex > overIndex ? overIndex : overIndex - 1;
|
|
373
|
+
}
|
|
374
|
+
newIndex = Math.max( 0, Math.min( newIndex, items.length - 1 ) );
|
|
375
|
+
|
|
376
|
+
if ( newIndex === currentIndex ) {
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const updatedItems = arrayMove( items, currentIndex, newIndex );
|
|
381
|
+
// Build a key→index lookup so the .map below is O(n)
|
|
382
|
+
// instead of O(n²) from per-item `indexOf` calls.
|
|
383
|
+
const orderByKey = new Map< string, number >();
|
|
384
|
+
updatedItems.forEach( ( key, index ) => {
|
|
385
|
+
orderByKey.set( key, index );
|
|
386
|
+
} );
|
|
387
|
+
const updatedLayout = activeLayout.map( ( item ) => ( {
|
|
388
|
+
...item,
|
|
389
|
+
order: orderByKey.get( item.key ) ?? 0,
|
|
390
|
+
} ) );
|
|
391
|
+
|
|
392
|
+
lastReorderCursorRef.current = {
|
|
393
|
+
x: activeCenterX,
|
|
394
|
+
y: activeCenterY,
|
|
395
|
+
};
|
|
396
|
+
latestLayoutRef.current = updatedLayout;
|
|
397
|
+
captureLayoutSnapshotRef.current();
|
|
398
|
+
setTemporaryLayout( updatedLayout );
|
|
399
|
+
onPreviewLayout?.( updatedLayout );
|
|
400
|
+
} );
|
|
401
|
+
|
|
402
|
+
const persistTemporaryLayout = useEvent( () => {
|
|
403
|
+
const latest = latestLayoutRef.current;
|
|
404
|
+
latestLayoutRef.current = undefined;
|
|
405
|
+
resizeBaselineRef.current = null;
|
|
406
|
+
setIsResizing( false );
|
|
407
|
+
setResizeSnapPreview( null );
|
|
408
|
+
if ( ! onChangeLayout || ! latest ) {
|
|
409
|
+
setTemporaryLayout( undefined );
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
onChangeLayout( latest );
|
|
414
|
+
setTemporaryLayout( undefined );
|
|
415
|
+
} );
|
|
416
|
+
|
|
417
|
+
const handleResize = useEvent( ( id: string, delta: ResizeDelta ) => {
|
|
418
|
+
if ( ! editMode ) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if ( ! isResizing ) {
|
|
422
|
+
setIsResizing( true );
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const relativeDelta = Math.round(
|
|
426
|
+
delta.width / ( columnWidth + gapPx )
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
if ( resizeBaselineRef.current === null ) {
|
|
430
|
+
const baseItem = layoutMap.get( id );
|
|
431
|
+
const baseWidth =
|
|
432
|
+
typeof baseItem?.width === 'number' ? baseItem.width : 1;
|
|
433
|
+
resizeBaselineRef.current = baseWidth;
|
|
434
|
+
}
|
|
435
|
+
const baseline = resizeBaselineRef.current;
|
|
436
|
+
const newWidth = Math.max(
|
|
437
|
+
1,
|
|
438
|
+
Math.min( baseline + relativeDelta, effectiveColumns )
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
setResizeSnapPreview( {
|
|
442
|
+
id,
|
|
443
|
+
snap: gridSpanToPixelSize(
|
|
444
|
+
newWidth,
|
|
445
|
+
1,
|
|
446
|
+
columnWidth,
|
|
447
|
+
gapPx,
|
|
448
|
+
null
|
|
449
|
+
),
|
|
450
|
+
} );
|
|
451
|
+
|
|
452
|
+
const pendingItem = latestLayoutRef.current?.find(
|
|
453
|
+
( item ) => item.key === id
|
|
454
|
+
);
|
|
455
|
+
const currentItem = pendingItem ?? layoutMap.get( id );
|
|
456
|
+
if ( currentItem && currentItem.width === newWidth ) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const updatedLayout = activeLayout.map( ( item ) =>
|
|
461
|
+
item.key === id ? { ...item, width: newWidth } : item
|
|
462
|
+
);
|
|
463
|
+
|
|
464
|
+
latestLayoutRef.current = updatedLayout;
|
|
465
|
+
captureLayoutSnapshotRef.current();
|
|
466
|
+
setTemporaryLayout( updatedLayout );
|
|
467
|
+
onPreviewLayout?.( updatedLayout );
|
|
468
|
+
} );
|
|
469
|
+
|
|
470
|
+
const interacting = activeId !== null || isResizing;
|
|
471
|
+
|
|
472
|
+
// Drag-overlay clone composition: the surface always wraps with a
|
|
473
|
+
// thin functional frame (lift, cursor, pointer pass-through). When
|
|
474
|
+
// `renderDragPreview` is supplied, the consumer's wrapper sits
|
|
475
|
+
// inside the frame around the cloned children; otherwise the
|
|
476
|
+
// cloned children render directly so any persistent chrome on
|
|
477
|
+
// them carries through unchanged.
|
|
478
|
+
const activeClone = activeId ? childrenMap.get( activeId ) : null;
|
|
479
|
+
const DragPreview = renderDragPreview;
|
|
480
|
+
const dragOverlayContent =
|
|
481
|
+
activeId && activeClone ? (
|
|
482
|
+
<div className={ styles[ 'drag-preview-frame' ] }>
|
|
483
|
+
<div className={ styles[ 'drag-preview-frame__lift' ] }>
|
|
484
|
+
{ DragPreview ? (
|
|
485
|
+
<DragPreview itemId={ activeId }>
|
|
486
|
+
{ activeClone }
|
|
487
|
+
</DragPreview>
|
|
488
|
+
) : (
|
|
489
|
+
activeClone
|
|
490
|
+
) }
|
|
491
|
+
</div>
|
|
492
|
+
</div>
|
|
493
|
+
) : null;
|
|
494
|
+
|
|
495
|
+
// Edit-mode background. Lanes are content-driven vertically, so
|
|
496
|
+
// the overlay mirrors columns only. Rendered unconditionally so
|
|
497
|
+
// it can cross-fade on edit-mode toggles (`isActive` drives the
|
|
498
|
+
// transition); memoized so drag/resize re-renders skip it.
|
|
499
|
+
const Overlay = renderGridOverlay ?? GridOverlay;
|
|
500
|
+
const gridOverlay = useMemo(
|
|
501
|
+
() => (
|
|
502
|
+
<Overlay columns={ effectiveColumns } isActive={ editMode } />
|
|
503
|
+
),
|
|
504
|
+
[ Overlay, editMode, effectiveColumns ]
|
|
505
|
+
);
|
|
506
|
+
|
|
507
|
+
const layoutFingerprint = useMemo( () => {
|
|
508
|
+
const layoutSig = getLayoutFingerprint( activeLayout );
|
|
509
|
+
const placementSig = getPlacementFingerprint( itemStyles );
|
|
510
|
+
return `${ layoutSig }\0${ placementSig }`;
|
|
511
|
+
}, [ activeLayout, itemStyles ] );
|
|
512
|
+
const excludeLayoutAnimationKey =
|
|
513
|
+
activeId ?? ( isResizing ? resizeSnapPreview?.id : null );
|
|
514
|
+
const { captureLayoutSnapshot, getPositionsBeforeLastChange } =
|
|
515
|
+
useLayoutShiftAnimation( {
|
|
516
|
+
container,
|
|
517
|
+
enabled: editMode,
|
|
518
|
+
layoutFingerprint,
|
|
519
|
+
excludeItemKey: excludeLayoutAnimationKey,
|
|
520
|
+
} );
|
|
521
|
+
const { exitingItems, clearExitingItem } = useItemExitAnimation( {
|
|
522
|
+
container,
|
|
523
|
+
enabled: editMode,
|
|
524
|
+
layoutKeys,
|
|
525
|
+
getPositionsBeforeLastChange,
|
|
526
|
+
childrenCacheRef,
|
|
527
|
+
} );
|
|
528
|
+
const layoutAnimating = editMode;
|
|
529
|
+
useLayoutEffect( () => {
|
|
530
|
+
captureLayoutSnapshotRef.current = captureLayoutSnapshot;
|
|
531
|
+
}, [ captureLayoutSnapshot ] );
|
|
532
|
+
|
|
533
|
+
return (
|
|
534
|
+
<DndContext
|
|
535
|
+
sensors={ sensors }
|
|
536
|
+
onDragStart={ handleDragStart }
|
|
537
|
+
onDragCancel={ handleDragCancel }
|
|
538
|
+
onDragMove={ handleDragMove }
|
|
539
|
+
onDragEnd={ () => {
|
|
540
|
+
persistTemporaryLayout();
|
|
541
|
+
lastReorderCursorRef.current = null;
|
|
542
|
+
setActiveId( null );
|
|
543
|
+
} }
|
|
544
|
+
>
|
|
545
|
+
<SortableContext items={ items } strategy={ NO_SORT_STRATEGY }>
|
|
546
|
+
<div
|
|
547
|
+
{ ...divProps }
|
|
548
|
+
ref={ mergedRootRef }
|
|
549
|
+
className={ clsx(
|
|
550
|
+
styles.lanes,
|
|
551
|
+
layoutAnimating &&
|
|
552
|
+
layoutAnimationStyles[ 'layout-animating' ],
|
|
553
|
+
className
|
|
554
|
+
) }
|
|
555
|
+
data-wp-grid-dragging={ activeId || undefined }
|
|
556
|
+
data-wp-grid-resizing={ isResizing || undefined }
|
|
557
|
+
style={
|
|
558
|
+
{
|
|
559
|
+
...style,
|
|
560
|
+
gridTemplateColumns: `repeat(${ effectiveColumns }, minmax(0, 1fr))`,
|
|
561
|
+
// `column-gap` and `row-gap` are set in
|
|
562
|
+
// `lanes.module.css` from the
|
|
563
|
+
// design-system gap token, with an
|
|
564
|
+
// `@supports` block that zeroes `row-gap`
|
|
565
|
+
// in polyfill mode (the skyline already
|
|
566
|
+
// encodes vertical spacing in each tile's
|
|
567
|
+
// `top`). Driving the toggle from CSS
|
|
568
|
+
// keeps SSR and client output identical
|
|
569
|
+
// regardless of native support.
|
|
570
|
+
'--wp-grid-lane-row-unit': `${ Math.max(
|
|
571
|
+
1,
|
|
572
|
+
rowUnit
|
|
573
|
+
) }px`,
|
|
574
|
+
} as React.CSSProperties
|
|
575
|
+
}
|
|
576
|
+
>
|
|
577
|
+
{ gridOverlay }
|
|
578
|
+
{ items.map( ( id ) => {
|
|
579
|
+
const child = childrenMap.get( id );
|
|
580
|
+
if ( ! child ) {
|
|
581
|
+
return null;
|
|
582
|
+
}
|
|
583
|
+
return (
|
|
584
|
+
<LanesItem
|
|
585
|
+
key={ id }
|
|
586
|
+
itemKey={ id }
|
|
587
|
+
placementStyle={
|
|
588
|
+
itemStyles.get( id ) ?? {}
|
|
589
|
+
}
|
|
590
|
+
disabled={ ! editMode }
|
|
591
|
+
interacting={ interacting }
|
|
592
|
+
dragging={ activeId !== null }
|
|
593
|
+
onResize={ handleResize }
|
|
594
|
+
onResizeEnd={ persistTemporaryLayout }
|
|
595
|
+
resizeSnapPreview={
|
|
596
|
+
resizeSnapPreview?.id === id
|
|
597
|
+
? resizeSnapPreview.snap
|
|
598
|
+
: null
|
|
599
|
+
}
|
|
600
|
+
minResizeWidthPx={ minResizeWidthPx }
|
|
601
|
+
actionableArea={ actionableAreaMap.get(
|
|
602
|
+
id
|
|
603
|
+
) }
|
|
604
|
+
renderResizeHandle={ renderResizeHandle }
|
|
605
|
+
>
|
|
606
|
+
{ child }
|
|
607
|
+
</LanesItem>
|
|
608
|
+
);
|
|
609
|
+
} ) }
|
|
610
|
+
{ remaining }
|
|
611
|
+
{ exitingItems.map( ( { key, rect, child } ) => (
|
|
612
|
+
<ItemExitOverlay
|
|
613
|
+
key={ `exiting-${ key }` }
|
|
614
|
+
itemKey={ key }
|
|
615
|
+
rect={ rect }
|
|
616
|
+
onAnimationEnd={ () => clearExitingItem( key ) }
|
|
617
|
+
>
|
|
618
|
+
{ child }
|
|
619
|
+
</ItemExitOverlay>
|
|
620
|
+
) ) }
|
|
621
|
+
</div>
|
|
622
|
+
</SortableContext>
|
|
623
|
+
<DragOverlay dropAnimation={ dashboardDragDropAnimation }>
|
|
624
|
+
{ dragOverlayContent }
|
|
625
|
+
</DragOverlay>
|
|
626
|
+
</DndContext>
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
);
|