@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,16 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Sibling tile reflow during drag/resize/remove in edit mode. Transition
|
|
3
|
+
* lives on tiles so the browser can interpolate `transform` reliably
|
|
4
|
+
* (inline `transition` + FLIP in the same frame is skipped).
|
|
5
|
+
*/
|
|
6
|
+
.layout-animating :global([data-wp-grid-item-key]) {
|
|
7
|
+
transition:
|
|
8
|
+
transform var(--wpds-motion-duration-md)
|
|
9
|
+
var(--wpds-motion-easing-balanced);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
@media (prefers-reduced-motion: reduce) {
|
|
13
|
+
.layout-animating :global([data-wp-grid-item-key]) {
|
|
14
|
+
transition: none;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Fade with opacity only: `visibility` is not interpolatable and pairing
|
|
3
|
+
* it with delayed transitions breaks fade-in.
|
|
4
|
+
*/
|
|
5
|
+
.resize-handle-slot {
|
|
6
|
+
opacity: 1;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
10
|
+
.resize-handle-slot {
|
|
11
|
+
transition:
|
|
12
|
+
opacity var(--wpds-motion-duration-md)
|
|
13
|
+
var(--wpds-motion-easing-subtle);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Grid-level interaction hooks (`data-wp-grid-dragging`,
|
|
19
|
+
* `data-wp-grid-resizing`, `data-wp-grid-item-resizing`) drive
|
|
20
|
+
* handle visibility.
|
|
21
|
+
*/
|
|
22
|
+
:global([data-wp-grid-dragging]) .resize-handle-slot,
|
|
23
|
+
:global([data-wp-grid-resizing]) .resize-handle-slot {
|
|
24
|
+
opacity: 0;
|
|
25
|
+
pointer-events: none;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
:global([data-wp-grid-resizing])
|
|
29
|
+
:global([data-wp-grid-item-resizing])
|
|
30
|
+
.resize-handle-slot {
|
|
31
|
+
opacity: 1;
|
|
32
|
+
pointer-events: auto;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.resize-handle {
|
|
36
|
+
position: absolute;
|
|
37
|
+
bottom: 0;
|
|
38
|
+
inset-inline-end: 0;
|
|
39
|
+
width: 0;
|
|
40
|
+
height: 0;
|
|
41
|
+
z-index: 1;
|
|
42
|
+
cursor: nwse-resize;
|
|
43
|
+
/*
|
|
44
|
+
* Triangle drawn via borders. Use per-side logical shorthands
|
|
45
|
+
* instead of `border-width: 0` + `border-block-end-width: 12px`:
|
|
46
|
+
* the build's CSS optimizer merges the global shorthands into a
|
|
47
|
+
* single `border: 0 solid transparent` and emits it AFTER the
|
|
48
|
+
* logical longhands, which then loses the cascade and zeroes
|
|
49
|
+
* the triangle widths. The unused sides default to
|
|
50
|
+
* `border-style: none` and render as 0 regardless of width.
|
|
51
|
+
*/
|
|
52
|
+
border-block-end: 12px solid var(--wpds-color-foreground-interactive-brand);
|
|
53
|
+
border-inline-start: 12px solid transparent;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/*
|
|
57
|
+
* When the host surface only resizes horizontally (e.g. lanes,
|
|
58
|
+
* where height is content-driven), the corner triangle advertises a
|
|
59
|
+
* vertical drag the user cannot perform. Override with a vertical
|
|
60
|
+
* grip centered on the trailing edge.
|
|
61
|
+
*/
|
|
62
|
+
.is-horizontal-only {
|
|
63
|
+
cursor: ew-resize;
|
|
64
|
+
border-style: none;
|
|
65
|
+
bottom: auto;
|
|
66
|
+
top: 50%;
|
|
67
|
+
transform: translateY(-50%);
|
|
68
|
+
width: 16px;
|
|
69
|
+
height: 40px;
|
|
70
|
+
background: transparent;
|
|
71
|
+
display: flex;
|
|
72
|
+
align-items: center;
|
|
73
|
+
justify-content: center;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.is-horizontal-only::before {
|
|
77
|
+
content: "";
|
|
78
|
+
width: 4px;
|
|
79
|
+
height: 24px;
|
|
80
|
+
border-radius: 2px;
|
|
81
|
+
background-color: var(--wpds-color-foreground-interactive-brand);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@media (forced-colors: active) {
|
|
85
|
+
.is-horizontal-only::before {
|
|
86
|
+
background-color: Highlight;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import { DndContext, useDraggable } from '@dnd-kit/core';
|
|
5
|
+
import type { DragMoveEvent } from '@dnd-kit/core';
|
|
6
|
+
import clsx from 'clsx';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* WordPress dependencies
|
|
10
|
+
*/
|
|
11
|
+
import { useCallback, useEffect, useRef } from '@wordpress/element';
|
|
12
|
+
import { useMergeRefs, useThrottle } from '@wordpress/compose';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Internal dependencies
|
|
16
|
+
*/
|
|
17
|
+
import type { ResizeDelta, ResizeHandleProps } from './types';
|
|
18
|
+
import styles from './resize-handle.module.css';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Sets `document.documentElement.style.cursor` for the duration of a drag
|
|
22
|
+
* and restores it on cleanup. Lives outside the component so cursor writes
|
|
23
|
+
* are not analyzed as mutating values derived from refs in the component
|
|
24
|
+
* body (react-hooks/immutability).
|
|
25
|
+
*
|
|
26
|
+
* @param getDocument Returns the document whose root element should receive
|
|
27
|
+
* the cursor (handle owner, or global `document`).
|
|
28
|
+
* @param cursor CSS cursor value while active.
|
|
29
|
+
* @return Cleanup that restores the previous cursor.
|
|
30
|
+
*/
|
|
31
|
+
function lockDocumentCursorWhileActive(
|
|
32
|
+
getDocument: () => Document,
|
|
33
|
+
cursor: string
|
|
34
|
+
): () => void {
|
|
35
|
+
const root = getDocument().documentElement;
|
|
36
|
+
const previous = root.style.cursor;
|
|
37
|
+
root.style.cursor = cursor;
|
|
38
|
+
return () => {
|
|
39
|
+
root.style.cursor = previous;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function ResizeHandle( {
|
|
44
|
+
itemId,
|
|
45
|
+
verticalResizable = true,
|
|
46
|
+
renderResizeHandle,
|
|
47
|
+
}: ResizeHandleProps ) {
|
|
48
|
+
const { attributes, listeners, setNodeRef, isDragging } = useDraggable( {
|
|
49
|
+
id: 'draggable',
|
|
50
|
+
data: { itemId },
|
|
51
|
+
} );
|
|
52
|
+
|
|
53
|
+
// Snapshot owner document on mount/update via ref callback so the
|
|
54
|
+
// cursor-lock effect can resolve the correct document in an iframe.
|
|
55
|
+
const ownerDocumentRef = useRef< Document | null >( null );
|
|
56
|
+
const setOwnerDocumentRef = useCallback( ( node: HTMLElement | null ) => {
|
|
57
|
+
ownerDocumentRef.current = node?.ownerDocument ?? null;
|
|
58
|
+
}, [] );
|
|
59
|
+
const mergedRef = useMergeRefs( [ setOwnerDocumentRef, setNodeRef ] );
|
|
60
|
+
|
|
61
|
+
// Lock the document cursor while the gesture is active. Without
|
|
62
|
+
// this, the OS pointer reverts to the default arrow as soon as it
|
|
63
|
+
// leaves the handle's hit area, even though the resize is still
|
|
64
|
+
// in progress.
|
|
65
|
+
useEffect( () => {
|
|
66
|
+
if ( ! isDragging ) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const cursor = verticalResizable ? 'nwse-resize' : 'ew-resize';
|
|
70
|
+
return lockDocumentCursorWhileActive(
|
|
71
|
+
() => ownerDocumentRef.current ?? document,
|
|
72
|
+
cursor
|
|
73
|
+
);
|
|
74
|
+
}, [ isDragging, verticalResizable ] );
|
|
75
|
+
|
|
76
|
+
if ( renderResizeHandle ) {
|
|
77
|
+
const RenderResizeHandle = renderResizeHandle;
|
|
78
|
+
return (
|
|
79
|
+
<RenderResizeHandle
|
|
80
|
+
ref={ mergedRef }
|
|
81
|
+
listeners={ listeners }
|
|
82
|
+
attributes={ attributes }
|
|
83
|
+
verticalResizable={ verticalResizable }
|
|
84
|
+
isResizing={ isDragging }
|
|
85
|
+
itemId={ itemId }
|
|
86
|
+
/>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<div
|
|
92
|
+
ref={ mergedRef }
|
|
93
|
+
className={ clsx(
|
|
94
|
+
styles[ 'resize-handle' ],
|
|
95
|
+
! verticalResizable && styles[ 'is-horizontal-only' ]
|
|
96
|
+
) }
|
|
97
|
+
{ ...listeners }
|
|
98
|
+
{ ...attributes }
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Renders a corner resize handle inside an isolated `<DndContext>`.
|
|
105
|
+
* Reports the cursor offset since the gesture started (in pixels)
|
|
106
|
+
* via `onResize`, throttled to one animation frame so the grid
|
|
107
|
+
* commit loop runs at most once per paint.
|
|
108
|
+
*
|
|
109
|
+
* Auto-scroll is enabled with a tight trigger zone and a low
|
|
110
|
+
* acceleration so a resize gesture near the viewport edge scrolls
|
|
111
|
+
* the page only when the user deliberately pushes against the very
|
|
112
|
+
* edge, and even then at a pace the user can interrupt by releasing.
|
|
113
|
+
* Default tuning would otherwise produce a runaway loop where the
|
|
114
|
+
* page scrolls fast, dnd-kit's document-coordinate `delta` inflates
|
|
115
|
+
* with the scroll, and the tile keeps growing without further user
|
|
116
|
+
* input.
|
|
117
|
+
*
|
|
118
|
+
* @param props Component props.
|
|
119
|
+
*/
|
|
120
|
+
export default function ResizeHandleWrapper( props: ResizeHandleProps ) {
|
|
121
|
+
const throttleDelay = 16;
|
|
122
|
+
const throttledResize = useThrottle( ( delta: ResizeDelta ) => {
|
|
123
|
+
if ( props.onResize ) {
|
|
124
|
+
props.onResize( delta );
|
|
125
|
+
}
|
|
126
|
+
}, throttleDelay );
|
|
127
|
+
|
|
128
|
+
// `event.delta` is the cursor offset from the gesture start —
|
|
129
|
+
// not from the handle's current position — so it stays stable
|
|
130
|
+
// even when the tile (and therefore the handle) jumps a column.
|
|
131
|
+
// The grid's resize logic snapshots the start width and adds
|
|
132
|
+
// `delta`, so the two must share the same frame of reference.
|
|
133
|
+
const handleDragMove = ( event: DragMoveEvent ) => {
|
|
134
|
+
if ( event.active.id !== 'draggable' ) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
throttledResize( {
|
|
138
|
+
width: event.delta.x,
|
|
139
|
+
height: event.delta.y,
|
|
140
|
+
} );
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const handleDragEnd = () => {
|
|
144
|
+
if ( props.onResizeEnd ) {
|
|
145
|
+
props.onResizeEnd();
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
return (
|
|
150
|
+
<DndContext
|
|
151
|
+
autoScroll={ {
|
|
152
|
+
threshold: { x: 0.005, y: 0.005 },
|
|
153
|
+
acceleration: 1,
|
|
154
|
+
} }
|
|
155
|
+
onDragMove={ handleDragMove }
|
|
156
|
+
onDragEnd={ handleDragEnd }
|
|
157
|
+
>
|
|
158
|
+
<div className={ styles[ 'resize-handle-slot' ] }>
|
|
159
|
+
<ResizeHandle { ...props } />
|
|
160
|
+
</div>
|
|
161
|
+
</DndContext>
|
|
162
|
+
);
|
|
163
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ResizeDelta } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Pixel dimensions for the snapped resize preview outline.
|
|
5
|
+
*/
|
|
6
|
+
export type ResizeSnapSize = {
|
|
7
|
+
widthPx: number;
|
|
8
|
+
/** When `null`, the preview spans the item's content height (lanes). */
|
|
9
|
+
heightPx: number | null;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Clamps a resize delta so the tile cannot shrink below the given
|
|
14
|
+
* minimum width (and height when provided).
|
|
15
|
+
*
|
|
16
|
+
* @param delta Cursor offset from the gesture start in pixels.
|
|
17
|
+
* @param initialSize Size captured at gesture start.
|
|
18
|
+
* @param initialSize.width Initial width in pixels.
|
|
19
|
+
* @param initialSize.height Initial height in pixels.
|
|
20
|
+
* @param minSize Minimum tile size in pixels.
|
|
21
|
+
* @param minSize.width Minimum width in pixels.
|
|
22
|
+
* @param minSize.height Minimum height in pixels, when vertical resize applies.
|
|
23
|
+
*/
|
|
24
|
+
export function clampResizeDelta(
|
|
25
|
+
delta: ResizeDelta,
|
|
26
|
+
initialSize: { width: number; height: number },
|
|
27
|
+
minSize: { width: number; height?: number }
|
|
28
|
+
): ResizeDelta {
|
|
29
|
+
const maxShrinkWidth = initialSize.width - minSize.width;
|
|
30
|
+
const width = Math.max( delta.width, -maxShrinkWidth );
|
|
31
|
+
if ( minSize.height === undefined ) {
|
|
32
|
+
return { ...delta, width };
|
|
33
|
+
}
|
|
34
|
+
const maxShrinkHeight = initialSize.height - minSize.height;
|
|
35
|
+
const height = Math.max( delta.height, -maxShrinkHeight );
|
|
36
|
+
return { width, height };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Converts grid spans to pixel width/height for the resize-preview
|
|
41
|
+
* outline, using the same track math the surface uses for placement.
|
|
42
|
+
*
|
|
43
|
+
* @param columnSpan Number of columns the snap target spans.
|
|
44
|
+
* @param rowSpan Number of rows the snap target spans.
|
|
45
|
+
* @param columnWidth Width of one column track in pixels.
|
|
46
|
+
* @param gapPx Gap between tracks in pixels.
|
|
47
|
+
* @param rowHeightPx Row track height in pixels, or `null` when rows
|
|
48
|
+
* are content-sized.
|
|
49
|
+
*/
|
|
50
|
+
export function gridSpanToPixelSize(
|
|
51
|
+
columnSpan: number,
|
|
52
|
+
rowSpan: number,
|
|
53
|
+
columnWidth: number,
|
|
54
|
+
gapPx: number,
|
|
55
|
+
rowHeightPx: number | null
|
|
56
|
+
): ResizeSnapSize {
|
|
57
|
+
const widthPx = columnSpan * columnWidth + ( columnSpan - 1 ) * gapPx;
|
|
58
|
+
const heightPx =
|
|
59
|
+
rowHeightPx === null
|
|
60
|
+
? null
|
|
61
|
+
: rowSpan * rowHeightPx + ( rowSpan - 1 ) * gapPx;
|
|
62
|
+
return { widthPx, heightPx };
|
|
63
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { clampResizeDelta, gridSpanToPixelSize } from '../resize-snap';
|
|
2
|
+
|
|
3
|
+
describe( 'gridSpanToPixelSize', () => {
|
|
4
|
+
it( 'returns one column track width for a single-column span', () => {
|
|
5
|
+
expect( gridSpanToPixelSize( 1, 1, 100, 16, null ).widthPx ).toBe(
|
|
6
|
+
100
|
|
7
|
+
);
|
|
8
|
+
} );
|
|
9
|
+
} );
|
|
10
|
+
|
|
11
|
+
describe( 'clampResizeDelta', () => {
|
|
12
|
+
it( 'does not shrink width below one column', () => {
|
|
13
|
+
const initial = { width: 216, height: 120 };
|
|
14
|
+
const min = { width: 100, height: 48 };
|
|
15
|
+
expect(
|
|
16
|
+
clampResizeDelta( { width: -200, height: 0 }, initial, min )
|
|
17
|
+
).toEqual( { width: -116, height: 0 } );
|
|
18
|
+
} );
|
|
19
|
+
|
|
20
|
+
it( 'does not shrink height below one row when a minimum height is set', () => {
|
|
21
|
+
const initial = { width: 200, height: 144 };
|
|
22
|
+
const min = { width: 100, height: 48 };
|
|
23
|
+
expect(
|
|
24
|
+
clampResizeDelta( { width: 0, height: -120 }, initial, min )
|
|
25
|
+
).toEqual( { width: 0, height: -96 } );
|
|
26
|
+
} );
|
|
27
|
+
|
|
28
|
+
it( 'leaves growth deltas unchanged', () => {
|
|
29
|
+
const initial = { width: 100, height: 48 };
|
|
30
|
+
const min = { width: 100, height: 48 };
|
|
31
|
+
expect(
|
|
32
|
+
clampResizeDelta( { width: 80, height: 40 }, initial, min )
|
|
33
|
+
).toEqual( { width: 80, height: 40 } );
|
|
34
|
+
} );
|
|
35
|
+
} );
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* External dependencies
|
|
3
|
+
*/
|
|
4
|
+
import type { useDraggable } from '@dnd-kit/core';
|
|
5
|
+
|
|
6
|
+
// `useDraggable`'s `listeners` and `attributes` types are not exported
|
|
7
|
+
// from `@dnd-kit/core`'s public surface, so derive them from the hook
|
|
8
|
+
// itself rather than via a deep import.
|
|
9
|
+
type DraggableBindings = ReturnType< typeof useDraggable >;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Cursor offset reported by the resize handle, in pixels relative to
|
|
13
|
+
* the gesture start. Width and height are independent so the surface
|
|
14
|
+
* can step columns and rows separately.
|
|
15
|
+
*/
|
|
16
|
+
export type ResizeDelta = {
|
|
17
|
+
width: number;
|
|
18
|
+
height: number;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Props received by a custom resize handle component. Spread `listeners`
|
|
23
|
+
* and `attributes` onto the element that should respond to the gesture,
|
|
24
|
+
* and assign `ref` to the same element so dnd-kit can track it.
|
|
25
|
+
*/
|
|
26
|
+
export interface ResizeHandleRenderProps {
|
|
27
|
+
/**
|
|
28
|
+
* Ref callback to attach to the gesture-bearing element.
|
|
29
|
+
*/
|
|
30
|
+
ref: DraggableBindings[ 'setNodeRef' ];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Pointer/keyboard event listeners that initiate the drag.
|
|
34
|
+
*/
|
|
35
|
+
listeners: DraggableBindings[ 'listeners' ];
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Accessibility and dnd-kit attributes (role, aria-*, tabIndex…).
|
|
39
|
+
*/
|
|
40
|
+
attributes: DraggableBindings[ 'attributes' ];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Whether vertical resizing is allowed for this tile. Useful for
|
|
44
|
+
* adapting the cursor or visual cue.
|
|
45
|
+
*/
|
|
46
|
+
verticalResizable: boolean;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* True while the user is actively dragging this handle. Use it to
|
|
50
|
+
* swap colors, icons, or transforms during the gesture.
|
|
51
|
+
*/
|
|
52
|
+
isResizing: boolean;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Owning item's `key`. Available so consumers can render per-tile
|
|
56
|
+
* content if needed.
|
|
57
|
+
*/
|
|
58
|
+
itemId?: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Props received by a custom drag-preview component. The surface mounts
|
|
63
|
+
* the component inside `<DragOverlay>` and supplies the active tile's
|
|
64
|
+
* cloned children plus its `key`. The component is responsible for the
|
|
65
|
+
* visual chrome of the dragged clone (shadow, radius, padding); the
|
|
66
|
+
* surface keeps a thin functional wrapper around it that owns the lift
|
|
67
|
+
* cue, the cursor, and pointer pass-through during the gesture.
|
|
68
|
+
*/
|
|
69
|
+
export interface DragPreviewRenderProps {
|
|
70
|
+
/**
|
|
71
|
+
* The cloned tile content the surface mounts inside the
|
|
72
|
+
* `<DragOverlay>` portal. Render it where the visual wrapper
|
|
73
|
+
* expects the tile body.
|
|
74
|
+
*/
|
|
75
|
+
children: React.ReactNode;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Owning tile's `key`. Useful when the visual chrome needs to
|
|
79
|
+
* vary by which tile is being dragged.
|
|
80
|
+
*/
|
|
81
|
+
itemId: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Props received by a custom grid overlay component. The overlay
|
|
86
|
+
* paints behind the tiles in edit mode to visualize the column tracks
|
|
87
|
+
* and (when `rowHeight` is defined) the row tracks. Receives a
|
|
88
|
+
* snapshot of the surface's resolved layout parameters so the visual
|
|
89
|
+
* can reproduce the tracks pixel-accurately without re-deriving them.
|
|
90
|
+
*
|
|
91
|
+
* Reused by both `DashboardGrid` and `DashboardLanes`: lanes pass no
|
|
92
|
+
* `rowHeight` because heights are content-driven.
|
|
93
|
+
*/
|
|
94
|
+
export interface GridOverlayRenderProps {
|
|
95
|
+
/**
|
|
96
|
+
* Number of column tracks in the active surface. In responsive
|
|
97
|
+
* mode (`minColumnWidth`), this is the count derived from the
|
|
98
|
+
* container width, not the prop value.
|
|
99
|
+
*/
|
|
100
|
+
columns: number;
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Row height in pixels for surfaces with uniform rows. Omitted on
|
|
104
|
+
* surfaces with content-driven heights (lanes) or when row height
|
|
105
|
+
* is `'auto'`; in those cases row markers are omitted.
|
|
106
|
+
*/
|
|
107
|
+
rowHeight?: number;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Number of row tracks to mirror in each column. Derived from the
|
|
111
|
+
* grid container height when `rowHeight` is numeric; omitted when
|
|
112
|
+
* row height is unknown.
|
|
113
|
+
*/
|
|
114
|
+
rows?: number;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether the overlay should be visible. Surfaces render the
|
|
118
|
+
* overlay even when `false` so the implementation can transition
|
|
119
|
+
* opacity in and out; while `false`, the overlay must hide itself
|
|
120
|
+
* (and ideally release paint cost via `visibility: hidden` or an
|
|
121
|
+
* equivalent).
|
|
122
|
+
*/
|
|
123
|
+
isActive: boolean;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Props for the internal `<ResizeHandle />` wrapper.
|
|
128
|
+
*/
|
|
129
|
+
export interface ResizeHandleProps {
|
|
130
|
+
/**
|
|
131
|
+
* Owning item's `key`. Forwarded as `data.itemId` on the draggable
|
|
132
|
+
* so the parent can correlate the gesture with a tile if needed.
|
|
133
|
+
*/
|
|
134
|
+
itemId?: string;
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Whether the handle should track vertical movement. When false,
|
|
138
|
+
* the handle still appears but only emits horizontal deltas, and
|
|
139
|
+
* the cursor is constrained to the column resize axis.
|
|
140
|
+
*
|
|
141
|
+
* @default true
|
|
142
|
+
*/
|
|
143
|
+
verticalResizable?: boolean;
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Callback fired while the handle is being dragged. Receives the
|
|
147
|
+
* cursor offset from the gesture start in pixels.
|
|
148
|
+
*/
|
|
149
|
+
onResize?: ( delta: ResizeDelta ) => void;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Callback fired when the gesture ends.
|
|
153
|
+
*/
|
|
154
|
+
onResizeEnd?: () => void;
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Component that overrides the default corner triangle with a
|
|
158
|
+
* custom element. Receives gesture wiring (`ref`, `listeners`,
|
|
159
|
+
* `attributes`) plus context. The surface keeps ownership of the
|
|
160
|
+
* `<DndContext>` and the throttled delta loop; consumers are only
|
|
161
|
+
* responsible for the visual.
|
|
162
|
+
*/
|
|
163
|
+
renderResizeHandle?: React.ComponentType< ResizeHandleRenderProps >;
|
|
164
|
+
}
|