Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions packages/grid/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ interface DashboardGridLayoutItem {
| `editMode` | `boolean` | `false` | Enables drag-to-reorder and resize handles. |
| `onChangeLayout` | `( layout ) => void` | — | Fired when the user commits a drag or resize. |
| `onPreviewLayout` | `( layout ) => void` | — | Fired continuously during a drag or resize with the in-progress layout. Use for live feedback; `onChangeLayout` still emits the committed result. |
| `renderResizeHandle` | `( props ) => ReactNode` | — | Override the default corner-triangle resize handle with a custom element. Receives gesture wiring (`ref`, `listeners`, `attributes`) plus `disabled`, `verticalResizable`, and `itemId`. The grid keeps ownership of the `<DndContext>` and the throttled delta loop. |
| `className` | `string` | — | Extra class on the grid root. |

`DashboardGrid` forwards refs to its root `<div>`, and standard `<div>`
Expand Down Expand Up @@ -206,6 +207,62 @@ keyboard sensor:

Resize handles are currently pointer-only.

## Custom resize handle

The default handle is a small corner triangle on the bottom-right of
each tile. To swap it for a custom element (icon, button, branded
shape…), pass `renderResizeHandle`. The grid still owns the gesture
— `<DndContext>`, throttled delta loop, step-to-grid logic — and
passes the wiring to your render prop. Spread `listeners` and
`attributes` and assign `ref` on the element that should receive
pointer events.

```jsx
import { Icon } from '@wordpress/ui';
import { resizeCornerNE } from '@wordpress/icons';

<DashboardGrid
layout={ layout }
editMode
renderResizeHandle={ ( {
ref,
listeners,
attributes,
verticalResizable,
} ) => (
<div
ref={ ref }
{ ...listeners }
{ ...attributes }
style={ {
position: 'absolute',
bottom: 4,
insetInlineEnd: 4,
cursor: verticalResizable ? 'nwse-resize' : 'ew-resize',
} }
>
<Icon icon={ resizeCornerNE } size={ 16 } />
</div>
) }
>
{ tiles }
</DashboardGrid>;
```

The render prop receives:

| Field | Type | Description |
|-------|------|-------------|
| `ref` | `( node ) => void` | dnd-kit ref; assign on the gesture-bearing element. |
| `listeners` | `SyntheticListenerMap \| undefined` | Pointer/keyboard listeners; spread on the same element. |
| `attributes` | `DraggableAttributes` | Accessibility/dnd-kit attributes; spread alongside `listeners`. |
| `verticalResizable` | `boolean` | False when `rowHeight: 'auto'` — useful for adapting cursor or visual cue. |
| `isResizing` | `boolean` | True while the user is actively dragging this handle. Use it to swap colors, icons, or transforms during the gesture. |
| `itemId` | `string` | Owning tile's `key`. |

The handle is only mounted while the grid is in edit mode (`editMode={ true }`),
so the consumer's render prop never has to short-circuit on a disabled state.

## Contributing to this package

This is an individual package that's part of the Gutenberg project.
Expand Down
96 changes: 16 additions & 80 deletions packages/grid/src/grid-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,72 +14,9 @@ import { useMergeRefs } from '@wordpress/compose';
* Internal dependencies
*/
import ResizeHandle from './resize-handle';
import type { DashboardGridLayoutItem } from './types';
import type { GridItemProps, ResizeDelta } from './types';
import styles from './grid-item.module.css';

type GridItemProps = {
/**
* The layout item containing grid positioning information.
*/
item: DashboardGridLayoutItem;

/**
* The maximum number of columns in the grid.
*/
maxColumns: number;

/**
* Whether drag and resize interactions are disabled.
*
* @default false
*/
disabled?: boolean;

/**
* Whether the item can be resized vertically. Disabled when the
* grid uses `rowHeight: 'auto'`, where row height is driven by
* content rather than by the user.
*
* @default true
*/
verticalResizable?: boolean;

/**
* Whether any tile in the grid is currently being dragged or
* resized. When true, the item mutes its `actionableArea` with
* `inert` so pointer hovers over buttons in other tiles do not
* steal the in-progress gesture.
*
* @default false
*/
interacting?: boolean;

/**
* The content to be displayed within the grid item.
*/
children: React.ReactNode;

/**
* Content rendered above the draggable area that stays interactive
* in edit mode — typically action buttons, menus, or links. While
* any tile in the grid is being dragged or resized, this content
* is set `inert` so hovers on other tiles can't steal the gesture.
*/
actionableArea?: React.ReactNode;

/**
* Callback fired while the item is being resized. Receives the
* item's `key` plus the cursor offset from the gesture start in
* pixels; the grid converts the offset to column/row spans.
*/
onResize: ( id: string, delta: { width: number; height: number } ) => void;

/**
* Callback fired when the resize gesture ends.
*/
onResizeEnd: () => void;
};

export function GridItem( {
item,
maxColumns,
Expand All @@ -90,11 +27,11 @@ export function GridItem( {
actionableArea = null,
onResize,
onResizeEnd,
renderResizeHandle,
}: GridItemProps ) {
const [ previewDelta, setPreviewDelta ] = useState< {
width: number;
height: number;
} | null >( null );
const [ previewDelta, setPreviewDelta ] = useState< ResizeDelta | null >(
null
);
const itemRef = useRef< HTMLDivElement >( null );
// Tile bounding rect at the first resize frame. The cursor `delta`
// from the handle is anchored to the gesture start, but the
Expand All @@ -107,10 +44,7 @@ export function GridItem( {
// *after* React commits a width step but before paint, so the
// frame that follows a column step never renders the overlay
// at the pre-step offset.
const lastResizeDeltaRef = useRef< {
width: number;
height: number;
} | null >( null );
const lastResizeDeltaRef = useRef< ResizeDelta | null >( null );
const { attributes, listeners, setNodeRef, isDragging } = useSortable( {
id: item.key,
disabled,
Expand Down Expand Up @@ -140,7 +74,7 @@ export function GridItem( {
isDragging && styles[ 'is-dragging' ]
);

const handleResize = ( delta: { width: number; height: number } ) => {
const handleResize = ( delta: ResizeDelta ) => {
const clamped = {
width: delta.width,
height: verticalResizable ? delta.height : 0,
Expand Down Expand Up @@ -228,13 +162,15 @@ export function GridItem( {
<div { ...listeners } style={ { height: '100%' } }>
<div className={ styles[ 'item-content' ] }>
{ children }
<ResizeHandle
disabled={ disabled }
itemId={ item.key }
verticalResizable={ verticalResizable }
onResize={ handleResize }
onResizeEnd={ handleResizeEnd }
/>
{ ! disabled && (
<ResizeHandle
itemId={ item.key }
verticalResizable={ verticalResizable }
onResize={ handleResize }
onResizeEnd={ handleResizeEnd }
renderResizeHandle={ renderResizeHandle }
/>
) }
</div>
{ previewOverlay }
</div>
Expand Down
Loading
Loading