@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,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @jest-environment jsdom
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* External dependencies
|
|
7
|
+
*/
|
|
8
|
+
import { render } from '@testing-library/react';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Internal dependencies
|
|
12
|
+
*/
|
|
13
|
+
import { DashboardGrid } from '..';
|
|
14
|
+
|
|
15
|
+
class MockResizeObserver {
|
|
16
|
+
observed: Set< Element > = new Set();
|
|
17
|
+
observe( element: Element ) {
|
|
18
|
+
this.observed.add( element );
|
|
19
|
+
}
|
|
20
|
+
unobserve( element: Element ) {
|
|
21
|
+
this.observed.delete( element );
|
|
22
|
+
}
|
|
23
|
+
disconnect() {
|
|
24
|
+
this.observed.clear();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let originalResizeObserver: typeof ResizeObserver;
|
|
29
|
+
|
|
30
|
+
beforeEach( () => {
|
|
31
|
+
originalResizeObserver = global.ResizeObserver;
|
|
32
|
+
( global as unknown as { ResizeObserver: unknown } ).ResizeObserver =
|
|
33
|
+
MockResizeObserver;
|
|
34
|
+
} );
|
|
35
|
+
|
|
36
|
+
afterEach( () => {
|
|
37
|
+
( global as unknown as { ResizeObserver: unknown } ).ResizeObserver =
|
|
38
|
+
originalResizeObserver;
|
|
39
|
+
} );
|
|
40
|
+
|
|
41
|
+
describe( 'DashboardGrid keyboard activation', () => {
|
|
42
|
+
it( 'places the dnd-kit keyboard activator on the inner wrapper, not the outer item', () => {
|
|
43
|
+
// Verifies the DOM hierarchy: keyboard activation needs the
|
|
44
|
+
// focused node and the keydown listener to share a node, so
|
|
45
|
+
// the activator must live nested inside the outer item.
|
|
46
|
+
/* eslint-disable testing-library/no-container, testing-library/no-node-access */
|
|
47
|
+
const { container } = render(
|
|
48
|
+
<DashboardGrid
|
|
49
|
+
layout={ [ { key: 'a', width: 1 } ] }
|
|
50
|
+
columns={ 2 }
|
|
51
|
+
editMode
|
|
52
|
+
>
|
|
53
|
+
<div key="a">A</div>
|
|
54
|
+
</DashboardGrid>
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
// Edit mode also renders a resize handle with `role="button"`;
|
|
58
|
+
// `aria-roledescription="sortable"` isolates the activator.
|
|
59
|
+
const activator = container.querySelector(
|
|
60
|
+
'[role="button"][aria-roledescription="sortable"]'
|
|
61
|
+
);
|
|
62
|
+
expect( activator ).not.toBeNull();
|
|
63
|
+
expect( activator ).toHaveAttribute( 'tabindex', '0' );
|
|
64
|
+
|
|
65
|
+
// Outer item is identified by its inline `grid-column-end`
|
|
66
|
+
// placement style; the activator must be its descendant.
|
|
67
|
+
const items = container.querySelectorAll(
|
|
68
|
+
'[style*="grid-column-end"]'
|
|
69
|
+
);
|
|
70
|
+
expect( items ).toHaveLength( 1 );
|
|
71
|
+
const item = items[ 0 ];
|
|
72
|
+
expect( activator ).not.toBe( item );
|
|
73
|
+
expect( item.contains( activator! ) ).toBe( true );
|
|
74
|
+
/* eslint-enable testing-library/no-container, testing-library/no-node-access */
|
|
75
|
+
} );
|
|
76
|
+
} );
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { resolveFillWidths } from '../resolve-fill-widths';
|
|
5
|
+
import type { DashboardGridLayoutItem } from '../types';
|
|
6
|
+
|
|
7
|
+
function makeMap(
|
|
8
|
+
items: DashboardGridLayoutItem[]
|
|
9
|
+
): Map< string, DashboardGridLayoutItem > {
|
|
10
|
+
const map = new Map< string, DashboardGridLayoutItem >();
|
|
11
|
+
items.forEach( ( item ) => map.set( item.key, item ) );
|
|
12
|
+
return map;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function keys( items: DashboardGridLayoutItem[] ): string[] {
|
|
16
|
+
return items.map( ( item ) => item.key );
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe( 'resolveFillWidths', () => {
|
|
20
|
+
it( 'returns empty map when no items use width: "fill"', () => {
|
|
21
|
+
const items: DashboardGridLayoutItem[] = [
|
|
22
|
+
{ key: 'a', width: 2 },
|
|
23
|
+
{ key: 'b', width: 4 },
|
|
24
|
+
];
|
|
25
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
|
|
26
|
+
expect( result.size ).toBe( 0 );
|
|
27
|
+
} );
|
|
28
|
+
|
|
29
|
+
it( 'fill item takes all columns when alone', () => {
|
|
30
|
+
const items: DashboardGridLayoutItem[] = [
|
|
31
|
+
{ key: 'fill', width: 'fill' },
|
|
32
|
+
];
|
|
33
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
|
|
34
|
+
expect( result.get( 'fill' ) ).toBe( 6 );
|
|
35
|
+
} );
|
|
36
|
+
|
|
37
|
+
it( 'fill item takes remaining columns after fixed items', () => {
|
|
38
|
+
const items: DashboardGridLayoutItem[] = [
|
|
39
|
+
{ key: 'sidebar', width: 1 },
|
|
40
|
+
{ key: 'fill', width: 'fill' },
|
|
41
|
+
];
|
|
42
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
|
|
43
|
+
expect( result.get( 'fill' ) ).toBe( 5 );
|
|
44
|
+
} );
|
|
45
|
+
|
|
46
|
+
it( 'fill item reserves space for subsequent fixed items', () => {
|
|
47
|
+
const items: DashboardGridLayoutItem[] = [
|
|
48
|
+
{ key: 'left', width: 1 },
|
|
49
|
+
{ key: 'fill', width: 'fill' },
|
|
50
|
+
{ key: 'right', width: 2 },
|
|
51
|
+
];
|
|
52
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
|
|
53
|
+
expect( result.get( 'fill' ) ).toBe( 3 );
|
|
54
|
+
} );
|
|
55
|
+
|
|
56
|
+
it( 'fill after a full-width item starts a new row', () => {
|
|
57
|
+
const items: DashboardGridLayoutItem[] = [
|
|
58
|
+
{ key: 'full', width: 'full' },
|
|
59
|
+
{ key: 'fill', width: 'fill' },
|
|
60
|
+
{ key: 'sidebar', width: 1 },
|
|
61
|
+
];
|
|
62
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
|
|
63
|
+
expect( result.get( 'fill' ) ).toBe( 5 );
|
|
64
|
+
} );
|
|
65
|
+
|
|
66
|
+
it( 'consecutive fills each take a full row', () => {
|
|
67
|
+
const items: DashboardGridLayoutItem[] = [
|
|
68
|
+
{ key: 'fill-1', width: 'fill' },
|
|
69
|
+
{ key: 'fill-2', width: 'fill' },
|
|
70
|
+
];
|
|
71
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
|
|
72
|
+
expect( result.get( 'fill-1' ) ).toBe( 6 );
|
|
73
|
+
expect( result.get( 'fill-2' ) ).toBe( 6 );
|
|
74
|
+
} );
|
|
75
|
+
|
|
76
|
+
it( 'does not reserve items that overflow the row', () => {
|
|
77
|
+
const items: DashboardGridLayoutItem[] = [
|
|
78
|
+
{ key: 'fill', width: 'fill' },
|
|
79
|
+
{ key: 'a', width: 3 },
|
|
80
|
+
{ key: 'b', width: 4 },
|
|
81
|
+
];
|
|
82
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
|
|
83
|
+
expect( result.get( 'fill' ) ).toBe( 3 );
|
|
84
|
+
} );
|
|
85
|
+
|
|
86
|
+
it( 'clamps item widths to maxColumns', () => {
|
|
87
|
+
const items: DashboardGridLayoutItem[] = [
|
|
88
|
+
{ key: 'fill', width: 'fill' },
|
|
89
|
+
{ key: 'wide', width: 10 },
|
|
90
|
+
];
|
|
91
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 4 );
|
|
92
|
+
expect( result.get( 'fill' ) ).toBe( 4 );
|
|
93
|
+
} );
|
|
94
|
+
|
|
95
|
+
it( 'fill in the middle of a row', () => {
|
|
96
|
+
const items: DashboardGridLayoutItem[] = [
|
|
97
|
+
{ key: 'a', width: 1 },
|
|
98
|
+
{ key: 'b', width: 1 },
|
|
99
|
+
{ key: 'fill', width: 'fill' },
|
|
100
|
+
{ key: 'c', width: 1 },
|
|
101
|
+
];
|
|
102
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
|
|
103
|
+
expect( result.get( 'fill' ) ).toBe( 3 );
|
|
104
|
+
} );
|
|
105
|
+
|
|
106
|
+
it( 'multiple fills in different rows', () => {
|
|
107
|
+
const items: DashboardGridLayoutItem[] = [
|
|
108
|
+
{ key: 'fill-1', width: 'fill' },
|
|
109
|
+
{ key: 'sidebar-1', width: 1 },
|
|
110
|
+
{ key: 'full', width: 'full' },
|
|
111
|
+
{ key: 'fill-2', width: 'fill' },
|
|
112
|
+
{ key: 'sidebar-2', width: 2 },
|
|
113
|
+
];
|
|
114
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
|
|
115
|
+
expect( result.get( 'fill-1' ) ).toBe( 5 );
|
|
116
|
+
expect( result.get( 'fill-2' ) ).toBe( 4 );
|
|
117
|
+
} );
|
|
118
|
+
|
|
119
|
+
it( 'fill gets minimum of 1 column when row is almost full', () => {
|
|
120
|
+
const items: DashboardGridLayoutItem[] = [
|
|
121
|
+
{ key: 'a', width: 3 },
|
|
122
|
+
{ key: 'b', width: 2 },
|
|
123
|
+
{ key: 'fill', width: 'fill' },
|
|
124
|
+
];
|
|
125
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
|
|
126
|
+
expect( result.get( 'fill' ) ).toBe( 1 );
|
|
127
|
+
} );
|
|
128
|
+
|
|
129
|
+
it( 'adapts to different column counts (responsive)', () => {
|
|
130
|
+
const items: DashboardGridLayoutItem[] = [
|
|
131
|
+
{ key: 'fill', width: 'fill' },
|
|
132
|
+
{ key: 'sidebar', width: 1 },
|
|
133
|
+
];
|
|
134
|
+
expect(
|
|
135
|
+
resolveFillWidths( keys( items ), makeMap( items ), 6 ).get(
|
|
136
|
+
'fill'
|
|
137
|
+
)
|
|
138
|
+
).toBe( 5 );
|
|
139
|
+
expect(
|
|
140
|
+
resolveFillWidths( keys( items ), makeMap( items ), 4 ).get(
|
|
141
|
+
'fill'
|
|
142
|
+
)
|
|
143
|
+
).toBe( 3 );
|
|
144
|
+
expect(
|
|
145
|
+
resolveFillWidths( keys( items ), makeMap( items ), 2 ).get(
|
|
146
|
+
'fill'
|
|
147
|
+
)
|
|
148
|
+
).toBe( 1 );
|
|
149
|
+
} );
|
|
150
|
+
|
|
151
|
+
it( 'look-ahead stops at the next fill boundary', () => {
|
|
152
|
+
const items: DashboardGridLayoutItem[] = [
|
|
153
|
+
{ key: 'fill-1', width: 'fill' },
|
|
154
|
+
{ key: 'fill-2', width: 'fill' },
|
|
155
|
+
{ key: 'sidebar', width: 1 },
|
|
156
|
+
];
|
|
157
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
|
|
158
|
+
expect( result.get( 'fill-1' ) ).toBe( 6 );
|
|
159
|
+
expect( result.get( 'fill-2' ) ).toBe( 5 );
|
|
160
|
+
} );
|
|
161
|
+
|
|
162
|
+
it( 'look-ahead stops at the next full-width boundary', () => {
|
|
163
|
+
const items: DashboardGridLayoutItem[] = [
|
|
164
|
+
{ key: 'fill', width: 'fill' },
|
|
165
|
+
{ key: 'full', width: 'full' },
|
|
166
|
+
{ key: 'sidebar', width: 1 },
|
|
167
|
+
];
|
|
168
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 6 );
|
|
169
|
+
expect( result.get( 'fill' ) ).toBe( 6 );
|
|
170
|
+
} );
|
|
171
|
+
|
|
172
|
+
it( 'every item gets 1 column when maxColumns is 1', () => {
|
|
173
|
+
const items: DashboardGridLayoutItem[] = [
|
|
174
|
+
{ key: 'a', width: 3 },
|
|
175
|
+
{ key: 'fill', width: 'fill' },
|
|
176
|
+
{ key: 'b', width: 2 },
|
|
177
|
+
];
|
|
178
|
+
const result = resolveFillWidths( keys( items ), makeMap( items ), 1 );
|
|
179
|
+
expect( result.get( 'fill' ) ).toBe( 1 );
|
|
180
|
+
} );
|
|
181
|
+
|
|
182
|
+
it( 'returns empty map for an empty layout', () => {
|
|
183
|
+
const result = resolveFillWidths( [], new Map(), 6 );
|
|
184
|
+
expect( result.size ).toBe( 0 );
|
|
185
|
+
} );
|
|
186
|
+
|
|
187
|
+
describe( 'with multi-row items (height > 1)', () => {
|
|
188
|
+
it( 'accounts for the shadow of a tall tile on the left', () => {
|
|
189
|
+
const items: DashboardGridLayoutItem[] = [
|
|
190
|
+
{ key: 'tall', width: 3, height: 2 },
|
|
191
|
+
{ key: 'header', width: 9, height: 1 },
|
|
192
|
+
{ key: 'sub', width: 3, height: 1 },
|
|
193
|
+
{ key: 'fill', width: 'fill', height: 1 },
|
|
194
|
+
];
|
|
195
|
+
const result = resolveFillWidths(
|
|
196
|
+
keys( items ),
|
|
197
|
+
makeMap( items ),
|
|
198
|
+
12
|
|
199
|
+
);
|
|
200
|
+
expect( result.get( 'fill' ) ).toBe( 6 );
|
|
201
|
+
} );
|
|
202
|
+
|
|
203
|
+
it( 'accounts for the shadow of a tall tile in the middle', () => {
|
|
204
|
+
const items: DashboardGridLayoutItem[] = [
|
|
205
|
+
{ key: 'a', width: 3, height: 1 },
|
|
206
|
+
{ key: 'b', width: 3, height: 2 },
|
|
207
|
+
{ key: 'c', width: 6, height: 1 },
|
|
208
|
+
{ key: 'd', width: 3, height: 1 },
|
|
209
|
+
{ key: 'fill', width: 'fill', height: 1 },
|
|
210
|
+
];
|
|
211
|
+
const result = resolveFillWidths(
|
|
212
|
+
keys( items ),
|
|
213
|
+
makeMap( items ),
|
|
214
|
+
12
|
|
215
|
+
);
|
|
216
|
+
expect( result.get( 'fill' ) ).toBe( 6 );
|
|
217
|
+
} );
|
|
218
|
+
|
|
219
|
+
it( 'accounts for the shadow of a tall tile on the right', () => {
|
|
220
|
+
const items: DashboardGridLayoutItem[] = [
|
|
221
|
+
{ key: 'a', width: 3, height: 1 },
|
|
222
|
+
{ key: 'b', width: 6, height: 1 },
|
|
223
|
+
{ key: 'c', width: 3, height: 2 },
|
|
224
|
+
{ key: 'd', width: 6, height: 1 },
|
|
225
|
+
{ key: 'fill', width: 'fill', height: 1 },
|
|
226
|
+
];
|
|
227
|
+
const result = resolveFillWidths(
|
|
228
|
+
keys( items ),
|
|
229
|
+
makeMap( items ),
|
|
230
|
+
12
|
|
231
|
+
);
|
|
232
|
+
expect( result.get( 'fill' ) ).toBe( 3 );
|
|
233
|
+
} );
|
|
234
|
+
|
|
235
|
+
it( 'tracks shadow across multiple rows for height > 2', () => {
|
|
236
|
+
const items: DashboardGridLayoutItem[] = [
|
|
237
|
+
{ key: 'tall', width: 3, height: 3 },
|
|
238
|
+
{ key: 'a', width: 9, height: 1 },
|
|
239
|
+
{ key: 'b', width: 9, height: 1 },
|
|
240
|
+
{ key: 'fill', width: 'fill', height: 1 },
|
|
241
|
+
];
|
|
242
|
+
const result = resolveFillWidths(
|
|
243
|
+
keys( items ),
|
|
244
|
+
makeMap( items ),
|
|
245
|
+
12
|
|
246
|
+
);
|
|
247
|
+
expect( result.get( 'fill' ) ).toBe( 9 );
|
|
248
|
+
} );
|
|
249
|
+
} );
|
|
250
|
+
} );
|
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal dependencies
|
|
3
|
+
*/
|
|
4
|
+
import type {
|
|
5
|
+
DragPreviewRenderProps,
|
|
6
|
+
GridOverlayRenderProps,
|
|
7
|
+
ResizeDelta,
|
|
8
|
+
ResizeHandleRenderProps,
|
|
9
|
+
} from '../shared/types';
|
|
10
|
+
import type { ResizeSnapSize } from '../shared/resize-snap';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Dashboard grid layout item definition.
|
|
14
|
+
*
|
|
15
|
+
* `width` accepts either a numeric column span or a discriminated string:
|
|
16
|
+
* - `number` spans that many columns (clamped to the grid's column count).
|
|
17
|
+
* - `'fill'` spans the remaining columns in the current row.
|
|
18
|
+
* - `'full'` spans all columns (`grid-column: 1 / -1`).
|
|
19
|
+
*/
|
|
20
|
+
export type DashboardGridLayoutItem = {
|
|
21
|
+
/**
|
|
22
|
+
* Unique key that matches a child component key.
|
|
23
|
+
*/
|
|
24
|
+
key: string;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Number of columns this item spans, or a string discriminator
|
|
28
|
+
* (`'fill'` or `'full'`).
|
|
29
|
+
*/
|
|
30
|
+
width?: number | 'fill' | 'full';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Number of rows this item spans.
|
|
34
|
+
*
|
|
35
|
+
* @default 1
|
|
36
|
+
*/
|
|
37
|
+
height?: number;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Display order for the item. Lower values render first. When
|
|
41
|
+
* omitted, the item falls back to its index in the `layout` array.
|
|
42
|
+
*/
|
|
43
|
+
order?: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Props for the internal `<GridItem />` wrapper.
|
|
48
|
+
*/
|
|
49
|
+
export type GridItemProps = {
|
|
50
|
+
/**
|
|
51
|
+
* The layout item containing grid positioning information.
|
|
52
|
+
*/
|
|
53
|
+
item: DashboardGridLayoutItem;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The maximum number of columns in the grid.
|
|
57
|
+
*/
|
|
58
|
+
maxColumns: number;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Whether drag and resize interactions are disabled.
|
|
62
|
+
*
|
|
63
|
+
* @default false
|
|
64
|
+
*/
|
|
65
|
+
disabled?: boolean;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Whether the item can be resized vertically. Disabled when the
|
|
69
|
+
* grid uses `rowHeight: 'auto'`, where row height is driven by
|
|
70
|
+
* content rather than by the user.
|
|
71
|
+
*
|
|
72
|
+
* @default true
|
|
73
|
+
*/
|
|
74
|
+
verticalResizable?: boolean;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Whether any tile in the grid is currently being dragged or
|
|
78
|
+
* resized. Drives the drag activator cursor.
|
|
79
|
+
*
|
|
80
|
+
* @default false
|
|
81
|
+
*/
|
|
82
|
+
interacting?: boolean;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Whether a tile drag is in progress. Mutes each tile's
|
|
86
|
+
* `actionableArea` with `inert` so hovers on other tiles' controls
|
|
87
|
+
* do not steal the gesture.
|
|
88
|
+
*
|
|
89
|
+
* @default false
|
|
90
|
+
*/
|
|
91
|
+
dragging?: boolean;
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The content to be displayed within the grid item.
|
|
95
|
+
*/
|
|
96
|
+
children: React.ReactNode;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Content rendered above the draggable area that stays interactive
|
|
100
|
+
* in edit mode, typically action buttons, menus, or links. While
|
|
101
|
+
* a tile drag is in progress, this content is set `inert` so hovers
|
|
102
|
+
* on other tiles can't steal the gesture. During resize, visibility
|
|
103
|
+
* is controlled by grid-level CSS hooks.
|
|
104
|
+
*/
|
|
105
|
+
actionableArea?: React.ReactNode;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Callback fired while the item is being resized. Receives the
|
|
109
|
+
* item's `key` plus the cursor offset from the gesture start in
|
|
110
|
+
* pixels. The grid derives snapped spans from the delta and passes
|
|
111
|
+
* them back through `resizeSnapPreview`.
|
|
112
|
+
*/
|
|
113
|
+
onResize: ( id: string, delta: ResizeDelta ) => void;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Snapped grid size in pixels for the resize-preview outline. The
|
|
117
|
+
* tile content resizes continuously with the cursor; this outline
|
|
118
|
+
* shows the span the layout will commit to on release.
|
|
119
|
+
*/
|
|
120
|
+
resizeSnapPreview?: ResizeSnapSize | null;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Minimum tile width while resizing, in pixels (one column track).
|
|
124
|
+
*/
|
|
125
|
+
minResizeWidthPx: number;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Minimum tile height while resizing, in pixels (one row track).
|
|
129
|
+
* Omitted when vertical resize is disabled.
|
|
130
|
+
*/
|
|
131
|
+
minResizeHeightPx?: number;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Callback fired when the resize gesture ends.
|
|
135
|
+
*/
|
|
136
|
+
onResizeEnd: () => void;
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Component forwarded to `<ResizeHandle />` to override the default
|
|
140
|
+
* corner triangle. See `DashboardGridProps.renderResizeHandle`.
|
|
141
|
+
*/
|
|
142
|
+
renderResizeHandle?: React.ComponentType< ResizeHandleRenderProps >;
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Props for `DashboardGrid`. Extends the standard div props so consumers
|
|
147
|
+
* can pass `id`, `aria-*`, `data-*`, event handlers, etc., directly on
|
|
148
|
+
* the grid root.
|
|
149
|
+
*
|
|
150
|
+
* `columns` and `minColumnWidth` compose as a layered model:
|
|
151
|
+
* - `columns` alone: fixed N columns; each tile scales with the container.
|
|
152
|
+
* - `minColumnWidth` alone: column count derives from container width,
|
|
153
|
+
* floored by the per-tile minimum, down to 1 column.
|
|
154
|
+
* - Both together: `columns` caps the count, `minColumnWidth` enforces a
|
|
155
|
+
* per-tile width floor that can reduce the count below the cap on
|
|
156
|
+
* narrow containers ("up to N columns, but never narrower than W px").
|
|
157
|
+
*/
|
|
158
|
+
export interface DashboardGridProps
|
|
159
|
+
extends Omit<
|
|
160
|
+
React.ComponentPropsWithoutRef< 'div' >,
|
|
161
|
+
'children' | 'className' | 'style'
|
|
162
|
+
> {
|
|
163
|
+
/**
|
|
164
|
+
* Array of layout items.
|
|
165
|
+
*/
|
|
166
|
+
layout: DashboardGridLayoutItem[];
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Grid children. Each child must carry a `key` that matches an
|
|
170
|
+
* entry in `layout`; children without a match render at the end
|
|
171
|
+
* of the grid without explicit placement and fall through CSS
|
|
172
|
+
* Grid's auto-flow.
|
|
173
|
+
*/
|
|
174
|
+
children: React.ReactNode;
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Additional CSS class on the grid root.
|
|
178
|
+
*/
|
|
179
|
+
className?: string;
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Inline styles applied to the grid root. Merged underneath the
|
|
183
|
+
* grid's own layout styles, so the layout (`gridTemplateColumns`,
|
|
184
|
+
* `gridAutoRows`) always wins. The gap between tiles is owned by
|
|
185
|
+
* the design-system gap token and is not configurable per
|
|
186
|
+
* instance; override it via a theme or density change.
|
|
187
|
+
*/
|
|
188
|
+
style?: React.CSSProperties;
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Height of each row in pixels, or `'auto'` to let the tallest
|
|
192
|
+
* tile in the row size it.
|
|
193
|
+
*
|
|
194
|
+
* @default 'auto'
|
|
195
|
+
*/
|
|
196
|
+
rowHeight?: number | 'auto';
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Whether the grid is in edit mode (allows dragging and
|
|
200
|
+
* repositioning items).
|
|
201
|
+
*
|
|
202
|
+
* @default false
|
|
203
|
+
*/
|
|
204
|
+
editMode?: boolean;
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Callback fired when the user commits a drag or resize. Receives
|
|
208
|
+
* the resulting layout.
|
|
209
|
+
*/
|
|
210
|
+
onChangeLayout?: ( newLayout: DashboardGridLayoutItem[] ) => void;
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Callback fired continuously during a drag or resize interaction
|
|
214
|
+
* with the in-progress layout. Useful for live feedback in the
|
|
215
|
+
* surface (e.g., displaying the current width/position). The final
|
|
216
|
+
* committed layout is still emitted via `onChangeLayout`.
|
|
217
|
+
*/
|
|
218
|
+
onPreviewLayout?: ( previewLayout: DashboardGridLayoutItem[] ) => void;
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Override the default corner-triangle resize handle with a custom
|
|
222
|
+
* component. The grid still owns the gesture (dnd-kit `<DndContext>`,
|
|
223
|
+
* throttled delta loop) and passes the wiring to the consumer:
|
|
224
|
+
* spread `listeners` and `attributes` and assign `ref` on the
|
|
225
|
+
* element that should receive the gesture. Use `disabled` and
|
|
226
|
+
* `verticalResizable` to adapt the visual to context.
|
|
227
|
+
*/
|
|
228
|
+
renderResizeHandle?: React.ComponentType< ResizeHandleRenderProps >;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Custom wrapper for the dragged-clone visual mounted inside
|
|
232
|
+
* `<DragOverlay>`. The surface always wraps the clone with a thin
|
|
233
|
+
* functional frame (lift scale, grabbing cursor, pointer pass-
|
|
234
|
+
* through) and mounts this component inside it; the consumer
|
|
235
|
+
* owns the visual chrome (shadow, radius, padding).
|
|
236
|
+
*
|
|
237
|
+
* When omitted, the cloned children render directly inside the
|
|
238
|
+
* functional frame so any chrome the consumer applied to the
|
|
239
|
+
* persistent tile carries through unchanged.
|
|
240
|
+
*
|
|
241
|
+
* Token-only adjustments (lift scale, placeholder opacity,
|
|
242
|
+
* outline color, placeholder radius) flow through CSS custom
|
|
243
|
+
* properties documented in the README.
|
|
244
|
+
*/
|
|
245
|
+
renderDragPreview?: React.ComponentType< DragPreviewRenderProps >;
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Override the default edit-mode overlay (row-marker tiles per
|
|
249
|
+
* column) with a custom component. The grid supplies the resolved
|
|
250
|
+
* column count, row height, and row count; the consumer is
|
|
251
|
+
* responsible for the visual.
|
|
252
|
+
*
|
|
253
|
+
* The overlay only renders when `editMode` is true. When omitted,
|
|
254
|
+
* the package's default visual is used.
|
|
255
|
+
*/
|
|
256
|
+
renderGridOverlay?: React.ComponentType< GridOverlayRenderProps >;
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Target column count, used as a cap. Defaults to six when neither
|
|
260
|
+
* `columns` nor `minColumnWidth` is set; with `minColumnWidth` set
|
|
261
|
+
* it can resolve lower on narrow containers.
|
|
262
|
+
*/
|
|
263
|
+
columns?: number;
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Per-tile minimum width in pixels. Enables responsive mode: the
|
|
267
|
+
* column count derives from container width, floored by this value,
|
|
268
|
+
* down to 1.
|
|
269
|
+
*/
|
|
270
|
+
minColumnWidth?: number;
|
|
271
|
+
}
|