@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,358 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* External dependencies
|
|
7
|
+
*/
|
|
8
|
+
import { render, act, screen } from '@testing-library/react';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* WordPress dependencies
|
|
12
|
+
*/
|
|
13
|
+
import { useState } from '@wordpress/element';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Internal dependencies
|
|
17
|
+
*/
|
|
18
|
+
import { GRID_ITEM_DATA_KEY } from '../../shared/grid-item-key';
|
|
19
|
+
import { useLanePlacement } from '../use-lane-placement';
|
|
20
|
+
import type {
|
|
21
|
+
UseLanePlacementInput,
|
|
22
|
+
UseLanePlacementResult,
|
|
23
|
+
} from '../use-lane-placement';
|
|
24
|
+
|
|
25
|
+
type Entry = {
|
|
26
|
+
target: Element;
|
|
27
|
+
contentRect: { height: number; width: number };
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
class MockResizeObserver {
|
|
31
|
+
static instances: MockResizeObserver[] = [];
|
|
32
|
+
static lastInstance(): MockResizeObserver | undefined {
|
|
33
|
+
return MockResizeObserver.instances[
|
|
34
|
+
MockResizeObserver.instances.length - 1
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
static reset() {
|
|
38
|
+
MockResizeObserver.instances = [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
callback: ResizeObserverCallback;
|
|
42
|
+
observed: Set< Element > = new Set();
|
|
43
|
+
disconnected = false;
|
|
44
|
+
|
|
45
|
+
constructor( callback: ResizeObserverCallback ) {
|
|
46
|
+
this.callback = callback;
|
|
47
|
+
MockResizeObserver.instances.push( this );
|
|
48
|
+
}
|
|
49
|
+
observe( element: Element ) {
|
|
50
|
+
this.observed.add( element );
|
|
51
|
+
}
|
|
52
|
+
unobserve( element: Element ) {
|
|
53
|
+
this.observed.delete( element );
|
|
54
|
+
}
|
|
55
|
+
disconnect() {
|
|
56
|
+
this.observed.clear();
|
|
57
|
+
this.disconnected = true;
|
|
58
|
+
}
|
|
59
|
+
fire( entries: Entry[] ) {
|
|
60
|
+
this.callback(
|
|
61
|
+
entries as unknown as ResizeObserverEntry[],
|
|
62
|
+
this as unknown as ResizeObserver
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let originalResizeObserver: typeof ResizeObserver;
|
|
68
|
+
let originalSupports: typeof CSS.supports | undefined;
|
|
69
|
+
let originalRaf: typeof requestAnimationFrame;
|
|
70
|
+
|
|
71
|
+
function installMockObserver() {
|
|
72
|
+
originalResizeObserver = global.ResizeObserver;
|
|
73
|
+
MockResizeObserver.reset();
|
|
74
|
+
( global as unknown as { ResizeObserver: unknown } ).ResizeObserver =
|
|
75
|
+
MockResizeObserver;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function restoreObserver() {
|
|
79
|
+
( global as unknown as { ResizeObserver: unknown } ).ResizeObserver =
|
|
80
|
+
originalResizeObserver;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function setNativeSupport( supported: boolean ) {
|
|
84
|
+
if ( typeof CSS === 'undefined' ) {
|
|
85
|
+
( global as unknown as { CSS: unknown } ).CSS = {
|
|
86
|
+
supports: () => supported,
|
|
87
|
+
};
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
originalSupports = CSS.supports;
|
|
91
|
+
CSS.supports = ( property: string, value?: string ) => {
|
|
92
|
+
if ( property === 'display' && value === 'grid-lanes' ) {
|
|
93
|
+
return supported;
|
|
94
|
+
}
|
|
95
|
+
return originalSupports
|
|
96
|
+
? originalSupports.call( CSS, property, value as string )
|
|
97
|
+
: false;
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function restoreSupport() {
|
|
102
|
+
if ( originalSupports ) {
|
|
103
|
+
CSS.supports = originalSupports;
|
|
104
|
+
originalSupports = undefined;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function flushRaf() {
|
|
109
|
+
// jsdom polyfills `requestAnimationFrame` via `setTimeout`; an
|
|
110
|
+
// `act` boundary lets React commit any state set inside the rAF.
|
|
111
|
+
jest.runAllTimers();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
beforeEach( () => {
|
|
115
|
+
installMockObserver();
|
|
116
|
+
originalRaf = global.requestAnimationFrame;
|
|
117
|
+
global.requestAnimationFrame = ( cb ) =>
|
|
118
|
+
setTimeout( () => cb( performance.now() ), 0 ) as unknown as number;
|
|
119
|
+
jest.useFakeTimers();
|
|
120
|
+
} );
|
|
121
|
+
|
|
122
|
+
afterEach( () => {
|
|
123
|
+
jest.useRealTimers();
|
|
124
|
+
global.requestAnimationFrame = originalRaf;
|
|
125
|
+
restoreObserver();
|
|
126
|
+
restoreSupport();
|
|
127
|
+
} );
|
|
128
|
+
|
|
129
|
+
type HarnessProps = {
|
|
130
|
+
input: UseLanePlacementInput;
|
|
131
|
+
measuredHeights?: Record< string, number >;
|
|
132
|
+
onResult?: ( result: UseLanePlacementResult ) => void;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
function Harness( { input, measuredHeights, onResult }: HarnessProps ) {
|
|
136
|
+
const [ container, setContainer ] = useState< HTMLDivElement | null >(
|
|
137
|
+
null
|
|
138
|
+
);
|
|
139
|
+
const result = useLanePlacement( container, input );
|
|
140
|
+
if ( onResult ) {
|
|
141
|
+
onResult( result );
|
|
142
|
+
}
|
|
143
|
+
return (
|
|
144
|
+
<div ref={ setContainer } data-testid="container">
|
|
145
|
+
{ input.items.map( ( item ) => (
|
|
146
|
+
<div
|
|
147
|
+
key={ item.key }
|
|
148
|
+
{ ...{ [ GRID_ITEM_DATA_KEY ]: item.key } }
|
|
149
|
+
data-testid={ `item-${ item.key }` }
|
|
150
|
+
style={ {
|
|
151
|
+
...result.itemStyles.get( item.key ),
|
|
152
|
+
height: measuredHeights?.[ item.key ] ?? 0,
|
|
153
|
+
} }
|
|
154
|
+
/>
|
|
155
|
+
) ) }
|
|
156
|
+
</div>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function captureLatestResult() {
|
|
161
|
+
let latest: UseLanePlacementResult | null = null;
|
|
162
|
+
const onResult = ( result: UseLanePlacementResult ) => {
|
|
163
|
+
latest = result;
|
|
164
|
+
};
|
|
165
|
+
const get = (): UseLanePlacementResult => {
|
|
166
|
+
if ( ! latest ) {
|
|
167
|
+
throw new Error( 'Hook never produced a result' );
|
|
168
|
+
}
|
|
169
|
+
return latest;
|
|
170
|
+
};
|
|
171
|
+
return { onResult, get };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function fireMeasurements( heights: Record< string, number > ) {
|
|
175
|
+
const observer = MockResizeObserver.lastInstance();
|
|
176
|
+
if ( ! observer ) {
|
|
177
|
+
throw new Error( 'No ResizeObserver instance registered' );
|
|
178
|
+
}
|
|
179
|
+
const entries: Entry[] = [];
|
|
180
|
+
for ( const [ key, height ] of Object.entries( heights ) ) {
|
|
181
|
+
const element = screen.getByTestId( `item-${ key }` );
|
|
182
|
+
entries.push( {
|
|
183
|
+
target: element,
|
|
184
|
+
contentRect: { height, width: 0 },
|
|
185
|
+
} );
|
|
186
|
+
}
|
|
187
|
+
act( () => {
|
|
188
|
+
observer.fire( entries );
|
|
189
|
+
flushRaf();
|
|
190
|
+
} );
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
describe( 'useLanePlacement', () => {
|
|
194
|
+
describe( 'native support', () => {
|
|
195
|
+
it( 'returns isPolyfilled=false and span-only styles when supported', () => {
|
|
196
|
+
setNativeSupport( true );
|
|
197
|
+
const { onResult, get } = captureLatestResult();
|
|
198
|
+
|
|
199
|
+
render(
|
|
200
|
+
<Harness
|
|
201
|
+
input={ {
|
|
202
|
+
items: [
|
|
203
|
+
{ key: 'a', span: 1 },
|
|
204
|
+
{ key: 'b', span: 2 },
|
|
205
|
+
],
|
|
206
|
+
lanes: 3,
|
|
207
|
+
gap: 16,
|
|
208
|
+
flowTolerance: 0,
|
|
209
|
+
} }
|
|
210
|
+
onResult={ onResult }
|
|
211
|
+
/>
|
|
212
|
+
);
|
|
213
|
+
act( () => {
|
|
214
|
+
flushRaf();
|
|
215
|
+
} );
|
|
216
|
+
|
|
217
|
+
const result = get();
|
|
218
|
+
expect( result.isPolyfilled ).toBe( false );
|
|
219
|
+
expect( result.itemStyles.get( 'a' ) ).toEqual( {
|
|
220
|
+
gridColumn: 'span 1',
|
|
221
|
+
} );
|
|
222
|
+
expect( result.itemStyles.get( 'b' ) ).toEqual( {
|
|
223
|
+
gridColumn: 'span 2',
|
|
224
|
+
} );
|
|
225
|
+
// No observers should have been instantiated.
|
|
226
|
+
expect( MockResizeObserver.instances.length ).toBe( 0 );
|
|
227
|
+
} );
|
|
228
|
+
} );
|
|
229
|
+
|
|
230
|
+
describe( 'polyfill path', () => {
|
|
231
|
+
it( 'returns native-shape styles before measurement completes', () => {
|
|
232
|
+
setNativeSupport( false );
|
|
233
|
+
const { onResult, get } = captureLatestResult();
|
|
234
|
+
|
|
235
|
+
render(
|
|
236
|
+
<Harness
|
|
237
|
+
input={ {
|
|
238
|
+
items: [
|
|
239
|
+
{ key: 'a', span: 1 },
|
|
240
|
+
{ key: 'b', span: 2 },
|
|
241
|
+
],
|
|
242
|
+
lanes: 3,
|
|
243
|
+
gap: 16,
|
|
244
|
+
flowTolerance: 0,
|
|
245
|
+
} }
|
|
246
|
+
onResult={ onResult }
|
|
247
|
+
/>
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
const result = get();
|
|
251
|
+
expect( result.isPolyfilled ).toBe( true );
|
|
252
|
+
expect( result.itemStyles.get( 'a' ) ).toEqual( {
|
|
253
|
+
gridColumn: 'span 1',
|
|
254
|
+
} );
|
|
255
|
+
} );
|
|
256
|
+
|
|
257
|
+
it( 'observes children and emits placement styles after measurement', () => {
|
|
258
|
+
setNativeSupport( false );
|
|
259
|
+
const { onResult, get } = captureLatestResult();
|
|
260
|
+
|
|
261
|
+
render(
|
|
262
|
+
<Harness
|
|
263
|
+
input={ {
|
|
264
|
+
items: [
|
|
265
|
+
{ key: 'a', span: 1 },
|
|
266
|
+
{ key: 'b', span: 1 },
|
|
267
|
+
{ key: 'c', span: 1 },
|
|
268
|
+
],
|
|
269
|
+
lanes: 3,
|
|
270
|
+
gap: 0,
|
|
271
|
+
flowTolerance: 0,
|
|
272
|
+
rowUnit: 4,
|
|
273
|
+
} }
|
|
274
|
+
onResult={ onResult }
|
|
275
|
+
/>
|
|
276
|
+
);
|
|
277
|
+
|
|
278
|
+
// One ResizeObserver should have been constructed for the
|
|
279
|
+
// container's children.
|
|
280
|
+
expect( MockResizeObserver.instances.length ).toBe( 1 );
|
|
281
|
+
const observer = MockResizeObserver.lastInstance();
|
|
282
|
+
expect( observer?.observed.size ).toBe( 3 );
|
|
283
|
+
|
|
284
|
+
fireMeasurements( {
|
|
285
|
+
a: 100,
|
|
286
|
+
b: 80,
|
|
287
|
+
c: 60,
|
|
288
|
+
} );
|
|
289
|
+
|
|
290
|
+
const result = get();
|
|
291
|
+
expect( result.isPolyfilled ).toBe( true );
|
|
292
|
+
|
|
293
|
+
// All three items should land in lane 0/1/2 respectively
|
|
294
|
+
// (zero baselines), at row 1.
|
|
295
|
+
const a = result.itemStyles.get( 'a' )!;
|
|
296
|
+
const b = result.itemStyles.get( 'b' )!;
|
|
297
|
+
const c = result.itemStyles.get( 'c' )!;
|
|
298
|
+
expect( a.gridColumnStart ).toBe( 1 );
|
|
299
|
+
expect( a.gridRowStart ).toBe( 1 );
|
|
300
|
+
expect( a.gridRowEnd ).toBe( 'span 25' ); // 100 / 4
|
|
301
|
+
expect( b.gridColumnStart ).toBe( 2 );
|
|
302
|
+
expect( b.gridRowEnd ).toBe( 'span 20' ); // 80 / 4
|
|
303
|
+
expect( c.gridColumnStart ).toBe( 3 );
|
|
304
|
+
expect( c.gridRowEnd ).toBe( 'span 15' ); // 60 / 4
|
|
305
|
+
} );
|
|
306
|
+
|
|
307
|
+
it( 'recomputes when an item resizes', () => {
|
|
308
|
+
setNativeSupport( false );
|
|
309
|
+
const { onResult, get } = captureLatestResult();
|
|
310
|
+
|
|
311
|
+
render(
|
|
312
|
+
<Harness
|
|
313
|
+
input={ {
|
|
314
|
+
items: [
|
|
315
|
+
{ key: 'a', span: 1 },
|
|
316
|
+
{ key: 'b', span: 1 },
|
|
317
|
+
],
|
|
318
|
+
lanes: 1,
|
|
319
|
+
gap: 0,
|
|
320
|
+
flowTolerance: 0,
|
|
321
|
+
rowUnit: 4,
|
|
322
|
+
} }
|
|
323
|
+
onResult={ onResult }
|
|
324
|
+
/>
|
|
325
|
+
);
|
|
326
|
+
|
|
327
|
+
fireMeasurements( { a: 100, b: 50 } );
|
|
328
|
+
expect( get().itemStyles.get( 'b' )?.gridRowStart ).toBe( 26 ); // 100/4 + 1
|
|
329
|
+
|
|
330
|
+
fireMeasurements( { a: 200 } );
|
|
331
|
+
expect( get().itemStyles.get( 'b' )?.gridRowStart ).toBe( 51 ); // 200/4 + 1
|
|
332
|
+
} );
|
|
333
|
+
} );
|
|
334
|
+
|
|
335
|
+
describe( 'cleanup', () => {
|
|
336
|
+
it( 'disconnects observers on unmount', () => {
|
|
337
|
+
setNativeSupport( false );
|
|
338
|
+
|
|
339
|
+
const { unmount } = render(
|
|
340
|
+
<Harness
|
|
341
|
+
input={ {
|
|
342
|
+
items: [ { key: 'a', span: 1 } ],
|
|
343
|
+
lanes: 3,
|
|
344
|
+
gap: 16,
|
|
345
|
+
flowTolerance: 0,
|
|
346
|
+
} }
|
|
347
|
+
/>
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
const observer = MockResizeObserver.lastInstance();
|
|
351
|
+
expect( observer ).toBeDefined();
|
|
352
|
+
expect( observer!.disconnected ).toBe( false );
|
|
353
|
+
|
|
354
|
+
unmount();
|
|
355
|
+
expect( observer!.disconnected ).toBe( true );
|
|
356
|
+
} );
|
|
357
|
+
} );
|
|
358
|
+
} );
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dependencies
|
|
3
|
+
*/
|
|
4
|
+
import type {
|
|
5
|
+
DragPreviewRenderProps,
|
|
6
|
+
GridOverlayRenderProps,
|
|
7
|
+
ResizeHandleRenderProps,
|
|
8
|
+
} from '../shared/types';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Lanes layout item definition.
|
|
12
|
+
*
|
|
13
|
+
* Mirrors the public surface of `display: grid-lanes`: column span,
|
|
14
|
+
* an optional pinned lane, and an optional source order. Heights are
|
|
15
|
+
* content-driven; there is no `height` field. There is no `'fill'`
|
|
16
|
+
* (lanes pack their items by skyline; nothing is "left over").
|
|
17
|
+
* `'full'` is expressed by setting `width` to the lane count.
|
|
18
|
+
*/
|
|
19
|
+
export type DashboardLanesLayoutItem = {
|
|
20
|
+
/**
|
|
21
|
+
* Unique key that matches a child component key.
|
|
22
|
+
*/
|
|
23
|
+
key: string;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Number of lanes this item spans (`grid-column: span N`). Clamped
|
|
27
|
+
* to the surface's lane count.
|
|
28
|
+
*
|
|
29
|
+
* @default 1
|
|
30
|
+
*/
|
|
31
|
+
width?: number;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Pin the item to a specific 0-indexed lane. Pinned items are
|
|
35
|
+
* placed before auto items, so the auto flow runs around them.
|
|
36
|
+
* Out-of-range values (negative, or beyond `columns - width`) are
|
|
37
|
+
* clamped to the available range.
|
|
38
|
+
*/
|
|
39
|
+
lane?: number;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Display order. Lower values render first. When omitted, the
|
|
43
|
+
* item falls back to its index in the `layout` array.
|
|
44
|
+
*/
|
|
45
|
+
order?: number;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Props for `DashboardLanes`.
|
|
50
|
+
*
|
|
51
|
+
* `columns` and `minColumnWidth` compose as a layered model:
|
|
52
|
+
* - `columns` alone: fixed N lanes; tiles scale with the container.
|
|
53
|
+
* - `minColumnWidth` alone: lane count derives from container width,
|
|
54
|
+
* floored by the per-tile minimum, down to 1.
|
|
55
|
+
* - Both together: `columns` caps the count, `minColumnWidth` enforces
|
|
56
|
+
* a per-tile width floor that can reduce the count below the cap.
|
|
57
|
+
*/
|
|
58
|
+
export interface DashboardLanesProps
|
|
59
|
+
extends Omit<
|
|
60
|
+
React.ComponentPropsWithoutRef< 'div' >,
|
|
61
|
+
'children' | 'className' | 'style'
|
|
62
|
+
> {
|
|
63
|
+
/**
|
|
64
|
+
* Array of layout items.
|
|
65
|
+
*/
|
|
66
|
+
layout: DashboardLanesLayoutItem[];
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Surface children. Each child must carry a `key` matching an
|
|
70
|
+
* entry in `layout`; children without a match render at the end
|
|
71
|
+
* of the surface without explicit placement and fall through the
|
|
72
|
+
* lanes auto-flow.
|
|
73
|
+
*/
|
|
74
|
+
children: React.ReactNode;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Additional CSS class on the surface root.
|
|
78
|
+
*/
|
|
79
|
+
className?: string;
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Inline styles on the surface root. Merged underneath the
|
|
83
|
+
* surface's own layout styles, so `display` and
|
|
84
|
+
* `gridTemplateColumns` always win. The gap between tiles is
|
|
85
|
+
* owned by the design-system gap token and is not configurable
|
|
86
|
+
* per instance; override it via a theme or density change.
|
|
87
|
+
*/
|
|
88
|
+
style?: React.CSSProperties;
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* `flow-tolerance` value in pixels. When two candidate lanes
|
|
92
|
+
* differ in baseline by no more than this, the earlier lane wins
|
|
93
|
+
* to preserve source order. Larger values keep tiles closer to
|
|
94
|
+
* reading order at the cost of bigger empty regions.
|
|
95
|
+
*
|
|
96
|
+
* @default 16
|
|
97
|
+
*/
|
|
98
|
+
flowTolerance?: number;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Snap unit for the polyfill's `grid-row-start` / `grid-row-end:
|
|
102
|
+
* span N` math. Smaller values produce sharper placement at the
|
|
103
|
+
* cost of a larger implicit row count. Ignored on browsers with
|
|
104
|
+
* native `display: grid-lanes` support.
|
|
105
|
+
*
|
|
106
|
+
* @default 4
|
|
107
|
+
*/
|
|
108
|
+
rowUnit?: number;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Whether the surface is in edit mode (drag-to-reorder, resize).
|
|
112
|
+
*
|
|
113
|
+
* @default false
|
|
114
|
+
*/
|
|
115
|
+
editMode?: boolean;
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Fired when the user commits a drag or resize.
|
|
119
|
+
*/
|
|
120
|
+
onChangeLayout?: ( newLayout: DashboardLanesLayoutItem[] ) => void;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Fired continuously during a gesture with the in-progress
|
|
124
|
+
* layout. The committed result still emits via `onChangeLayout`.
|
|
125
|
+
*/
|
|
126
|
+
onPreviewLayout?: ( previewLayout: DashboardLanesLayoutItem[] ) => void;
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Override the default corner resize handle. See `DashboardGrid`
|
|
130
|
+
* for the full contract; on lanes the handle is horizontal-only
|
|
131
|
+
* because heights are content-driven.
|
|
132
|
+
*/
|
|
133
|
+
renderResizeHandle?: React.ComponentType< ResizeHandleRenderProps >;
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Custom wrapper for the dragged-clone visual mounted inside
|
|
137
|
+
* `<DragOverlay>`. The surface always wraps the clone with a thin
|
|
138
|
+
* functional frame (lift scale, grabbing cursor, pointer pass-
|
|
139
|
+
* through) and mounts this component inside it; the consumer
|
|
140
|
+
* owns the visual chrome (shadow, radius, padding).
|
|
141
|
+
*
|
|
142
|
+
* When omitted, the cloned children render directly inside the
|
|
143
|
+
* functional frame so any chrome the consumer applied to the
|
|
144
|
+
* persistent tile carries through unchanged.
|
|
145
|
+
*
|
|
146
|
+
* Token-only adjustments (lift scale, placeholder opacity,
|
|
147
|
+
* outline color, placeholder radius) flow through CSS custom
|
|
148
|
+
* properties documented in the README.
|
|
149
|
+
*/
|
|
150
|
+
renderDragPreview?: React.ComponentType< DragPreviewRenderProps >;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Override the default edit-mode overlay (empty column tracks) with
|
|
154
|
+
* a custom component. Lanes are content-driven vertically, so no
|
|
155
|
+
* `rowHeight` or `rows` is supplied and the default visual paints
|
|
156
|
+
* columns only.
|
|
157
|
+
*
|
|
158
|
+
* The overlay only renders when `editMode` is true. When omitted,
|
|
159
|
+
* the package's default visual is used.
|
|
160
|
+
*/
|
|
161
|
+
renderGridOverlay?: React.ComponentType< GridOverlayRenderProps >;
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Target lane count, used as a cap. Defaults to six when neither
|
|
165
|
+
* `columns` nor `minColumnWidth` is set; with `minColumnWidth` set
|
|
166
|
+
* it can resolve lower on narrow containers.
|
|
167
|
+
*/
|
|
168
|
+
columns?: number;
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Per-tile minimum width in pixels. Enables responsive mode: the
|
|
172
|
+
* lane count derives from container width, floored by this value,
|
|
173
|
+
* down to 1.
|
|
174
|
+
*/
|
|
175
|
+
minColumnWidth?: number;
|
|
176
|
+
}
|