Skip to content
Merged
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
3 changes: 3 additions & 0 deletions packages/grid/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Enhancements

- Clamp tile resize so width cannot shrink below a single column
track (and height below a single row on `DashboardGrid` when
vertical resize is enabled).
- Rework the default edit-mode `GridOverlay` to paint per-row marker
tiles (with `border-radius` md) inside each column instead of
column backgrounds, outlines, and repeating row dividers. Theme via
Expand Down
22 changes: 16 additions & 6 deletions packages/grid/src/dashboard-grid/grid-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { useMergeRefs } from '@wordpress/compose';
*/
import { GRID_ITEM_DATA_KEY } from '../shared/grid-item-key';
import ResizeHandle from '../shared/resize-handle';
import type { ResizeSnapSize } from '../shared/resize-snap';
import { clampResizeDelta, type ResizeSnapSize } from '../shared/resize-snap';
import type { ResizeDelta } from '../shared/types';
import type { GridItemProps } from './types';
import styles from './grid-item.module.css';
Expand Down Expand Up @@ -46,6 +46,8 @@ export function GridItem( {
onResize,
onResizeEnd,
resizeSnapPreview = null,
minResizeWidthPx,
minResizeHeightPx,
renderResizeHandle,
}: GridItemProps ) {
const [ resizeDelta, setResizeDelta ] = useState< ResizeDelta | null >(
Expand Down Expand Up @@ -95,14 +97,22 @@ export function GridItem( {
);

const handleResize = ( delta: ResizeDelta ) => {
const clamped = {
const contentNode = contentRef.current;
let baselineSize = initialContentSize;
if ( contentNode && ! baselineSize ) {
const { width, height } = contentNode.getBoundingClientRect();
baselineSize = { width, height };
setInitialContentSize( baselineSize );
}
let clamped: ResizeDelta = {
width: delta.width,
height: verticalResizable ? delta.height : 0,
};
const contentNode = contentRef.current;
if ( contentNode && ! initialContentSize ) {
const { width, height } = contentNode.getBoundingClientRect();
setInitialContentSize( { width, height } );
if ( baselineSize ) {
clamped = clampResizeDelta( clamped, baselineSize, {
width: minResizeWidthPx,
height: verticalResizable ? minResizeHeightPx : undefined,
} );
}
setResizeDelta( clamped );
onResize( item.key, clamped );
Expand Down
17 changes: 15 additions & 2 deletions packages/grid/src/dashboard-grid/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,19 @@ export const DashboardGrid = forwardRef< HTMLDivElement, DashboardGridProps >(
const columnWidth =
( containerWidth - ( effectiveColumns - 1 ) * gapPx ) /
effectiveColumns;
const minResizeWidthPx = gridSpanToPixelSize(
1,
1,
columnWidth,
gapPx,
null
).widthPx;
const rowHeightPx = typeof rowHeight === 'number' ? rowHeight : null;
const minResizeHeightPx =
rowHeightPx === null
? undefined
: gridSpanToPixelSize( 1, 1, columnWidth, gapPx, rowHeightPx )
.heightPx ?? undefined;

const layoutMap = useMemo( () => {
const map = new Map< string, DashboardGridLayoutItem >();
Expand Down Expand Up @@ -461,8 +474,6 @@ export const DashboardGrid = forwardRef< HTMLDivElement, DashboardGridProps >(
1,
baseline.height + relativeDelta.height
);
const rowHeightPx =
typeof rowHeight === 'number' ? rowHeight : null;

setResizeSnapPreview( {
id,
Expand Down Expand Up @@ -636,6 +647,8 @@ export const DashboardGrid = forwardRef< HTMLDivElement, DashboardGridProps >(
? resizeSnapPreview.snap
: null
}
minResizeWidthPx={ minResizeWidthPx }
minResizeHeightPx={ minResizeHeightPx }
actionableArea={ actionableAreaMap.get( id ) }
renderResizeHandle={ renderResizeHandle }
>
Expand Down
11 changes: 11 additions & 0 deletions packages/grid/src/dashboard-grid/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,17 @@ export type GridItemProps = {
*/
resizeSnapPreview?: ResizeSnapSize | null;

/**
* Minimum tile width while resizing, in pixels (one column track).
*/
minResizeWidthPx: number;

/**
* Minimum tile height while resizing, in pixels (one row track).
* Omitted when vertical resize is disabled.
*/
minResizeHeightPx?: number;

/**
* Callback fired when the resize gesture ends.
*/
Expand Down
8 changes: 8 additions & 0 deletions packages/grid/src/dashboard-lanes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,13 @@ export const DashboardLanes = forwardRef< HTMLDivElement, DashboardLanesProps >(
const columnWidth =
( containerWidth - ( effectiveColumns - 1 ) * gapPx ) /
effectiveColumns;
const minResizeWidthPx = gridSpanToPixelSize(
1,
1,
columnWidth,
gapPx,
null
).widthPx;

const layoutMap = useMemo( () => {
const map = new Map< string, DashboardLanesLayoutItem >();
Expand Down Expand Up @@ -551,6 +558,7 @@ export const DashboardLanes = forwardRef< HTMLDivElement, DashboardLanesProps >(
? resizeSnapPreview.snap
: null
}
minResizeWidthPx={ minResizeWidthPx }
actionableArea={ actionableAreaMap.get(
id
) }
Expand Down
21 changes: 17 additions & 4 deletions packages/grid/src/dashboard-lanes/lanes-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import { useMergeRefs } from '@wordpress/compose';
* Internal dependencies
*/
import ResizeHandle from '../shared/resize-handle';
import { clampResizeDelta, type ResizeSnapSize } from '../shared/resize-snap';
import { GRID_ITEM_DATA_KEY } from '../shared/grid-item-key';
import type { ResizeSnapSize } from '../shared/resize-snap';
import type { ResizeDelta, ResizeHandleRenderProps } from '../shared/types';
import styles from './lanes-item.module.css';

Expand Down Expand Up @@ -73,6 +73,11 @@ export type LanesItemProps = {
*/
resizeSnapPreview?: ResizeSnapSize | null;

/**
* Minimum tile width while resizing, in pixels (one column track).
*/
minResizeWidthPx: number;

onResizeEnd: () => void;

renderResizeHandle?: React.ComponentType< ResizeHandleRenderProps >;
Expand All @@ -88,6 +93,7 @@ export function LanesItem( {
onResize,
onResizeEnd,
resizeSnapPreview = null,
minResizeWidthPx,
renderResizeHandle,
}: LanesItemProps ) {
const [ resizeDelta, setResizeDelta ] = useState< ResizeDelta | null >(
Expand Down Expand Up @@ -126,11 +132,18 @@ export function LanesItem( {
);

const handleResize = ( delta: ResizeDelta ) => {
const clamped = { width: delta.width, height: 0 };
const contentNode = contentRef.current;
if ( contentNode && ! initialContentSize ) {
let baselineSize = initialContentSize;
if ( contentNode && ! baselineSize ) {
const { width, height } = contentNode.getBoundingClientRect();
setInitialContentSize( { width, height } );
baselineSize = { width, height };
setInitialContentSize( baselineSize );
}
let clamped: ResizeDelta = { width: delta.width, height: 0 };
if ( baselineSize ) {
clamped = clampResizeDelta( clamped, baselineSize, {
width: minResizeWidthPx,
} );
}
setResizeDelta( clamped );
onResize( itemKey, clamped );
Expand Down
29 changes: 29 additions & 0 deletions packages/grid/src/shared/resize-snap.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { ResizeDelta } from './types';

/**
* Pixel dimensions for the snapped resize preview outline.
*/
Expand All @@ -7,6 +9,33 @@ export type ResizeSnapSize = {
heightPx: number | null;
};

/**
* Clamps a resize delta so the tile cannot shrink below the given
* minimum width (and height when provided).
*
* @param delta Cursor offset from the gesture start in pixels.
* @param initialSize Size captured at gesture start.
* @param initialSize.width Initial width in pixels.
* @param initialSize.height Initial height in pixels.
* @param minSize Minimum tile size in pixels.
* @param minSize.width Minimum width in pixels.
* @param minSize.height Minimum height in pixels, when vertical resize applies.
*/
export function clampResizeDelta(
delta: ResizeDelta,
initialSize: { width: number; height: number },
minSize: { width: number; height?: number }
): ResizeDelta {
const maxShrinkWidth = initialSize.width - minSize.width;
const width = Math.max( delta.width, -maxShrinkWidth );
if ( minSize.height === undefined ) {
return { ...delta, width };
}
const maxShrinkHeight = initialSize.height - minSize.height;
const height = Math.max( delta.height, -maxShrinkHeight );
return { width, height };
}

/**
* Converts grid spans to pixel width/height for the resize-preview
* outline, using the same track math the surface uses for placement.
Expand Down
35 changes: 35 additions & 0 deletions packages/grid/src/shared/test/resize-snap.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { clampResizeDelta, gridSpanToPixelSize } from '../resize-snap';

describe( 'gridSpanToPixelSize', () => {
it( 'returns one column track width for a single-column span', () => {
expect( gridSpanToPixelSize( 1, 1, 100, 16, null ).widthPx ).toBe(
100
);
} );
} );

describe( 'clampResizeDelta', () => {
it( 'does not shrink width below one column', () => {
const initial = { width: 216, height: 120 };
const min = { width: 100, height: 48 };
expect(
clampResizeDelta( { width: -200, height: 0 }, initial, min )
).toEqual( { width: -116, height: 0 } );
} );

it( 'does not shrink height below one row when a minimum height is set', () => {
const initial = { width: 200, height: 144 };
const min = { width: 100, height: 48 };
expect(
clampResizeDelta( { width: 0, height: -120 }, initial, min )
).toEqual( { width: 0, height: -96 } );
} );

it( 'leaves growth deltas unchanged', () => {
const initial = { width: 100, height: 48 };
const min = { width: 100, height: 48 };
expect(
clampResizeDelta( { width: 80, height: 40 }, initial, min )
).toEqual( { width: 80, height: 40 } );
} );
} );
Loading