From 180c5a3d39a44532872c0efe5379c244b186ee7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 15 May 2026 10:52:53 +0100 Subject: [PATCH 1/4] migrate layout settings drawer to DataForm declarative field schema with synthetic toggles for fixed columns and auto row height; spacing now matches the dashboard's form surfaces --- .../layout-settings/layout-settings.tsx | 331 +++++++----------- 1 file changed, 134 insertions(+), 197 deletions(-) diff --git a/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx b/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx index 7162919fe47a23..44fdd42c09ef08 100644 --- a/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx +++ b/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx @@ -1,17 +1,12 @@ /** * WordPress dependencies */ -import { ToggleControl } from '@wordpress/components'; +import { DataForm } from '@wordpress/dataviews'; +import type { Field, Form } from '@wordpress/dataviews'; import { useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /* eslint-disable @wordpress/use-recommended-components */ -import { - Button, - Drawer, - InputControl, - SelectControl, - Stack, -} from '@wordpress/ui'; +import { Button, Drawer } from '@wordpress/ui'; /* eslint-enable @wordpress/use-recommended-components */ /** @@ -20,33 +15,129 @@ import { import { useDashboardInternalContext } from '../../context/dashboard-context'; import { migrateLayout } from '../../utils/migrate-layout'; import type { + WidgetGridLayoutSettings, WidgetGridModel, WidgetGridSettings, - WidgetMasonryLayoutSettings, } from '../../types'; -const MODEL_ITEMS = [ - { value: 'grid', label: __( 'Standard grid' ) }, - { value: 'masonry', label: __( 'Masonry' ) }, -] as const satisfies ReadonlyArray< { value: WidgetGridModel; label: string } >; - +const DEFAULT_FIXED_COLUMNS = 6; +const DEFAULT_MIN_COLUMN_WIDTH = 350; +const DEFAULT_ROW_HEIGHT = 200; const ROW_HEIGHT_AUTO = 'auto' as const; -function getModelValue( settings: WidgetGridSettings ): WidgetGridModel { - return settings.model ?? 'grid'; +function getModel( item: WidgetGridSettings ): WidgetGridModel { + return item.model ?? 'grid'; } -function parsePositiveInt( raw: unknown ): number | undefined { - if ( typeof raw !== 'string' && typeof raw !== 'number' ) { - return undefined; - } - const parsed = typeof raw === 'number' ? raw : Number.parseInt( raw, 10 ); - if ( ! Number.isFinite( parsed ) || parsed <= 0 ) { +function isMasonry( item: WidgetGridSettings ): boolean { + return getModel( item ) === 'masonry'; +} + +function isFixedColumns( item: WidgetGridSettings ): boolean { + return item.columns !== undefined; +} + +function getRowHeight( + item: WidgetGridSettings +): WidgetGridLayoutSettings[ 'rowHeight' ] { + if ( isMasonry( item ) ) { return undefined; } - return parsed; + return ( item as WidgetGridLayoutSettings ).rowHeight; +} + +function isAutoRowHeight( item: WidgetGridSettings ): boolean { + return getRowHeight( item ) === ROW_HEIGHT_AUTO; } +const fields: Field< WidgetGridSettings >[] = [ + { + id: 'model', + type: 'text', + Edit: 'select', + label: __( 'Layout model' ), + description: __( + 'Standard grid uses explicit widths and heights. Masonry packs items by content height.' + ), + elements: [ + { value: 'grid', label: __( 'Standard grid' ) }, + { value: 'masonry', label: __( 'Masonry' ) }, + ], + getValue: ( { item } ) => getModel( item ), + }, + { + id: 'fixedColumns', + type: 'boolean', + Edit: 'toggle', + label: __( 'Fixed column count' ), + getValue: ( { item } ) => isFixedColumns( item ), + setValue: ( { item, value } ) => + value + ? { + columns: item.columns ?? DEFAULT_FIXED_COLUMNS, + minColumnWidth: undefined, + } + : { + columns: undefined, + minColumnWidth: + item.minColumnWidth ?? DEFAULT_MIN_COLUMN_WIDTH, + }, + }, + { + id: 'columns', + type: 'integer', + label: __( 'Columns' ), + isValid: { min: 1, max: 12 }, + isVisible: ( item ) => isFixedColumns( item ), + }, + { + id: 'minColumnWidth', + type: 'integer', + label: __( 'Min column width (px)' ), + description: __( + 'Minimum width of each column. The number of columns adapts to the container width.' + ), + isValid: { min: 200 }, + isVisible: ( item ) => ! isFixedColumns( item ), + }, + { + id: 'autoRowHeight', + type: 'boolean', + Edit: 'toggle', + label: __( 'Auto-fit row height to content' ), + getValue: ( { item } ) => isAutoRowHeight( item ), + setValue: ( { value } ) => ( { + rowHeight: value ? ROW_HEIGHT_AUTO : DEFAULT_ROW_HEIGHT, + } ), + isDisabled: ( { item } ) => isMasonry( item ), + }, + { + id: 'rowHeight', + type: 'integer', + label: __( 'Row height (px)' ), + description: __( 'Height of each row in the standard grid.' ), + isValid: { min: 100 }, + getValue: ( { item } ) => { + const rh = getRowHeight( item ); + return typeof rh === 'number' ? rh : undefined; + }, + isVisible: ( item ) => ! isMasonry( item ), + isDisabled: ( { item } ) => isAutoRowHeight( item ), + }, +]; + +const form: Form = { + layout: { type: 'regular', labelPosition: 'top' }, + fields: [ + 'model', + 'fixedColumns', + 'columns', + 'minColumnWidth', + 'autoRowHeight', + 'rowHeight', + ], +}; + interface LayoutSettingsProps { open: boolean; onOpenChange: ( open: boolean ) => void; @@ -94,109 +185,27 @@ export function LayoutSettings( { hasUncommittedChanges, } = useDashboardInternalContext(); - const currentModel = getModelValue( gridSettings ); - const isMasonry = currentModel === 'masonry'; - - const handleModelChange = useCallback( - ( nextModel: WidgetGridModel ) => { - if ( nextModel === currentModel ) { - return; - } - - const migrated = migrateLayout( layout, currentModel, nextModel, { - columns: gridSettings.columns ?? 6, - } ); - - onLayoutChange( migrated ); - onGridSettingsChange( { - ...gridSettings, - model: nextModel, - } as WidgetGridSettings ); - }, - [ - currentModel, - gridSettings, - layout, - onGridSettingsChange, - onLayoutChange, - ] - ); - - const handleMinColumnWidthChange = useCallback( - ( raw: unknown ) => { - const next = parsePositiveInt( raw ); - onGridSettingsChange( { - ...gridSettings, - minColumnWidth: next, - } as WidgetGridSettings ); - }, - [ gridSettings, onGridSettingsChange ] - ); - - const handleColumnsChange = useCallback( - ( raw: unknown ) => { - const next = parsePositiveInt( raw ); - onGridSettingsChange( { - ...gridSettings, - columns: next, - } as WidgetGridSettings ); - }, - [ gridSettings, onGridSettingsChange ] - ); + const handleChange = useCallback( + ( edits: Record< string, unknown > ) => { + const nextModel = edits.model as WidgetGridModel | undefined; + const currentModel = getModel( gridSettings ); - const handleColumnsModeChange = useCallback( - ( useFixedColumns: boolean ) => { - if ( useFixedColumns ) { - onGridSettingsChange( { - ...gridSettings, - columns: gridSettings.columns ?? 6, - minColumnWidth: undefined, - } as WidgetGridSettings ); - } else { - onGridSettingsChange( { - ...gridSettings, - columns: undefined, - minColumnWidth: gridSettings.minColumnWidth ?? 350, - } as WidgetGridSettings ); + if ( nextModel && nextModel !== currentModel ) { + const migrated = migrateLayout( + layout, + currentModel, + nextModel, + { columns: gridSettings.columns ?? DEFAULT_FIXED_COLUMNS } + ); + onLayoutChange( migrated ); } - }, - [ gridSettings, onGridSettingsChange ] - ); - const setRowHeight = useCallback( - ( raw: unknown ) => { - const next = parsePositiveInt( raw ); onGridSettingsChange( { ...gridSettings, - rowHeight: next, + ...edits, } as WidgetGridSettings ); }, - [ gridSettings, onGridSettingsChange ] - ); - - const setRowHeightAuto = useCallback( - ( checked: boolean ) => { - onGridSettingsChange( { - ...gridSettings, - rowHeight: checked ? ROW_HEIGHT_AUTO : 200, - } as WidgetGridSettings ); - }, - [ gridSettings, onGridSettingsChange ] - ); - - const rowHeight = isMasonry - ? undefined - : ( - gridSettings as Exclude< - WidgetGridSettings, - WidgetMasonryLayoutSettings - > - ).rowHeight; - const isRowHeightAuto = rowHeight === ROW_HEIGHT_AUTO; - const isFixedColumns = gridSettings.columns !== undefined; - - const modelItem = MODEL_ITEMS.find( - ( item ) => item.value === currentModel + [ gridSettings, layout, onGridSettingsChange, onLayoutChange ] ); const handleCancel = useCallback( () => { @@ -234,84 +243,12 @@ export function LayoutSettings( { - - { - if ( item ) { - handleModelChange( - item.value as WidgetGridModel - ); - } - } } - /> - - - - { isFixedColumns ? ( - - ) : ( - - ) } - - - - - - - + From fa4c955435d5e63561a52fdb6c48e7bdcf8e9850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 15 May 2026 11:02:09 +0100 Subject: [PATCH 2/4] minor jsdoc improvement --- .../components/layout-settings/layout-settings.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx b/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx index 44fdd42c09ef08..b6ead18747551b 100644 --- a/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx +++ b/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx @@ -139,7 +139,14 @@ const form: Form = { }; interface LayoutSettingsProps { + /** + * Whether the drawer is visible. + */ open: boolean; + + /** + * Callback to toggle the drawer. + */ onOpenChange: ( open: boolean ) => void; } @@ -166,9 +173,8 @@ interface LayoutSettingsProps { * commit never publishes layout edits that the user is in the * middle of staging through the toolbar. * - * @param props - * @param props.open Whether the drawer is visible. - * @param props.onOpenChange Toggle controller from the trigger. + * @param {LayoutSettingsProps} props Layout settings props. + * @return {React.ReactNode} The layout settings component. */ export function LayoutSettings( { open, From c19d4d2e1b10e7cea77a7fa01cac4683bde64dd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 15 May 2026 11:18:03 +0100 Subject: [PATCH 3/4] add +/- steppers to layout settings integers per simison's review on PR #78202: NumberControl with spinControls="custom" for Columns, Min column width, and Row height --- .../layout-settings/layout-settings.tsx | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx b/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx index b6ead18747551b..5ef34d1d5a06fe 100644 --- a/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx +++ b/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx @@ -1,8 +1,9 @@ /** * WordPress dependencies */ +import { __experimentalNumberControl as NumberControl } from '@wordpress/components'; import { DataForm } from '@wordpress/dataviews'; -import type { Field, Form } from '@wordpress/dataviews'; +import type { DataFormControlProps, Field, Form } from '@wordpress/dataviews'; import { useCallback } from '@wordpress/element'; import { __ } from '@wordpress/i18n'; /* eslint-disable @wordpress/use-recommended-components */ @@ -50,6 +51,45 @@ function isAutoRowHeight( item: WidgetGridSettings ): boolean { return getRowHeight( item ) === ROW_HEIGHT_AUTO; } +function StepperIntegerEdit( { + data, + field, + onChange, +}: DataFormControlProps< WidgetGridSettings > ) { + const { label, description, getValue, setValue, isValid } = field; + const value = getValue( { item: data } ); + const disabled = field.isDisabled( { item: data, field } ); + const min = + typeof isValid.min?.constraint === 'number' + ? isValid.min.constraint + : undefined; + const max = + typeof isValid.max?.constraint === 'number' + ? isValid.max.constraint + : undefined; + + return ( + { + const parsed = + next === '' || next === undefined + ? undefined + : Number( next ); + onChange( setValue( { item: data, value: parsed } ) ); + } } + /> + ); +} + const fields: Field< WidgetGridSettings >[] = [ { id: 'model', @@ -86,6 +126,7 @@ const fields: Field< WidgetGridSettings >[] = [ { id: 'columns', type: 'integer', + Edit: StepperIntegerEdit, label: __( 'Columns' ), isValid: { min: 1, max: 12 }, isVisible: ( item ) => isFixedColumns( item ), @@ -93,6 +134,7 @@ const fields: Field< WidgetGridSettings >[] = [ { id: 'minColumnWidth', type: 'integer', + Edit: StepperIntegerEdit, label: __( 'Min column width (px)' ), description: __( 'Minimum width of each column. The number of columns adapts to the container width.' @@ -114,6 +156,7 @@ const fields: Field< WidgetGridSettings >[] = [ { id: 'rowHeight', type: 'integer', + Edit: StepperIntegerEdit, label: __( 'Row height (px)' ), description: __( 'Height of each row in the standard grid.' ), isValid: { min: 100 }, From b9da43c8cebd4edffa8836c0a8c57ec90d139665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dami=C3=A1n=20Su=C3=A1rez?= Date: Fri, 15 May 2026 12:02:39 +0100 Subject: [PATCH 4/4] set min and max for tile height --- .../components/layout-settings/layout-settings.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx b/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx index 5ef34d1d5a06fe..e794b8f708df73 100644 --- a/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx +++ b/routes/dashboard/widget-dashboard/components/layout-settings/layout-settings.tsx @@ -139,7 +139,7 @@ const fields: Field< WidgetGridSettings >[] = [ description: __( 'Minimum width of each column. The number of columns adapts to the container width.' ), - isValid: { min: 200 }, + isValid: { min: 48, max: 1024 }, isVisible: ( item ) => ! isFixedColumns( item ), }, {