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
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,7 @@ Settings related to spacing.
| units | List of units the user can use for spacing values. | `[ string ]` | `["px","em","rem","vh","vw","%"]` |
| customSpacingSize | Allow users to set custom space sizes. | `boolean` | `true` |
| defaultSpacingSizes | Allow users to choose space sizes from the default space size presets. | `boolean` | `true` |
| linkedByDefault | Whether margin and padding controls have their sides linked by default. | `boolean`, `{ margin, padding }` | `true` |
| spacingSizes | Space size presets for the space size selector. | `[ { name, slug, size } ]` | |
| spacingScale | Settings to auto-generate space size presets for the space size selector. | `{ operator, increment, steps, mediumStep, unit }` | |

Expand Down
1 change: 1 addition & 0 deletions lib/class-wp-theme-json-gutenberg.php
Original file line number Diff line number Diff line change
Expand Up @@ -472,6 +472,7 @@ class WP_Theme_JSON_Gutenberg {
'spacing' => array(
'customSpacingSize' => null,
'defaultSpacingSizes' => null,
'linkedByDefault' => null,
'spacingSizes' => null,
'spacingScale' => null,
'blockGap' => null,
Expand Down
1 change: 1 addition & 0 deletions lib/theme.json
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@
"padding": false,
"customSpacingSize": true,
"defaultSpacingSizes": true,
"linkedByDefault": true,
"units": [ "px", "em", "rem", "vh", "vw", "%" ],
"spacingScale": {
"operator": "*",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import SpacingSizesControl from '../spacing-sizes-control';
import DimensionControl from '../dimension-control';
import ChildLayoutControl from '../child-layout-control';
import AspectRatioTool from '../dimensions-tool/aspect-ratio-tool';
import { useSettings } from '../use-settings';
import { cleanEmptyObject } from '../../hooks/utils';
import { setImmutably } from '../../utils/object';
import {
Expand Down Expand Up @@ -380,6 +381,16 @@ export default function DimensionsPanel( {
);

const showSpacingPresetsControl = hasSpacingPresets( settings );
const [ linkedByDefaultSetting ] = useSettings( 'spacing.linkedByDefault' );
const linkedByDefault =
settings?.spacing?.linkedByDefault ?? linkedByDefaultSetting ?? true;
const isGlobalBoolean = typeof linkedByDefault === 'boolean';
const marginLinked = isGlobalBoolean
? linkedByDefault
: ( linkedByDefault?.margin ?? true );
const paddingLinked = isGlobalBoolean
? linkedByDefault
: ( linkedByDefault?.padding ?? true );
const units = useCustomUnits( {
availableUnits: settings?.spacing?.units || [
'%',
Expand Down Expand Up @@ -878,6 +889,7 @@ export default function DimensionsPanel( {
units={ units }
allowReset={ false }
splitOnAxis={ isAxialPadding }
linkedByDefault={ paddingLinked }
inputProps={ {
onMouseOver: onMouseOverPadding,
onMouseOut: onMouseLeaveControls,
Expand All @@ -895,6 +907,7 @@ export default function DimensionsPanel( {
sides={ paddingSides }
units={ units }
allowReset={ false }
linkedByDefault={ paddingLinked }
onMouseOver={ onMouseOverPadding }
onMouseOut={ onMouseLeaveControls }
/>
Expand Down Expand Up @@ -947,6 +960,7 @@ export default function DimensionsPanel( {
units={ units }
allowReset={ false }
splitOnAxis={ isAxialMargin }
linkedByDefault={ marginLinked }
/>
) }
{ showSpacingPresetsControl && (
Expand All @@ -958,6 +972,7 @@ export default function DimensionsPanel( {
sides={ marginSides }
units={ units }
allowReset={ false }
linkedByDefault={ marginLinked }
onMouseOver={ onMouseOverMargin }
onMouseOut={ onMouseLeaveControls }
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,48 @@ describe( 'DimensionsPanel — per-control placeholder pattern', () => {
expect( paddingAllSides ).toHaveValue( '24' );
expect( paddingAllSides ).not.toHaveAttribute( 'placeholder' );
} );

it( 'renders unlinked inputs in BoxControl when linkedByDefault is false', () => {
renderPanel( {
value: {},
settings: {
...baseSettings,
spacing: {
...baseSettings.spacing,
linkedByDefault: false,
},
},
} );

const paddingGroup = screen.getByRole( 'group', {
name: /Padding/,
} );
expect(
within( paddingGroup ).getByRole( 'textbox', {
name: 'Top side',
} )
).toBeVisible();
expect(
within( paddingGroup ).getByRole( 'textbox', {
name: 'Right side',
} )
).toBeVisible();
expect(
within( paddingGroup ).getByRole( 'textbox', {
name: 'Bottom side',
} )
).toBeVisible();
expect(
within( paddingGroup ).getByRole( 'textbox', {
name: 'Left side',
} )
).toBeVisible();
expect(
within( paddingGroup ).queryByRole( 'textbox', {
name: 'All sides',
} )
).not.toBeInTheDocument();
} );
} );

// The min-dimension DimensionControls (minHeight, minWidth) use the
Expand Down Expand Up @@ -803,6 +845,53 @@ describe( 'DimensionsPanel — per-control placeholder pattern', () => {
true
);
} );

it( '(padding) renders unlinked sides when linkedByDefault is false', () => {
renderPanel( {
value: {},
settings: {
...settingsWithSpacingPresets,
spacing: {
...settingsWithSpacingPresets.spacing,
linkedByDefault: false,
},
},
} );

const paddingSliders = getPaddingSliders();
// Separated view renders 4 sliders: Top, Right, Bottom, Left.
expect( paddingSliders ).toHaveLength( 4 );
expect(
screen.getAllByRole( 'button', { name: 'Link sides' } ).length
).toBeGreaterThanOrEqual( 1 );
} );

it( 'supports object overrides for linkedByDefault (e.g. padding unlinked, margin linked)', () => {
renderPanel( {
value: {},
settings: {
...settingsWithSpacingPresets,
spacing: {
...settingsWithSpacingPresets.spacing,
linkedByDefault: {
padding: false,
margin: true,
},
},
},
} );

const paddingSliders = getPaddingSliders();
expect( paddingSliders ).toHaveLength( 4 );

const marginSliders = screen
.getAllByRole( 'slider' )
.filter( ( s ) =>
/margin/i.test( s.getAttribute( 'aria-label' ) || '' )
);
// Margin linked by default renders 2 axial sliders (horizontal & vertical).
expect( marginSliders ).toHaveLength( 2 );
} );
} );

describe( 'local zero values (override, not inherited placeholder)', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from '@wordpress/components';
import { useState } from '@wordpress/element';
import { _x, sprintf } from '@wordpress/i18n';
import { useSettings } from '../use-settings';
import useSpacingSizes from './hooks/use-spacing-sizes';
import AxialInputControls from './input-controls/axial';
import SeparatedInputControls from './input-controls/separated';
Expand Down Expand Up @@ -48,22 +49,24 @@ import {
* }
* ```
*
* @param {Object} props Component props.
* @param {Object} props.inputProps Additional props for input controls.
* @param {string} props.label Label for the control.
* @param {number} props.minimumCustomValue Minimum value for custom input.
* @param {Function} props.onChange Called when spacing values change.
* @param {Function} props.onMouseOut Called when mouse leaves the control.
* @param {Function} props.onMouseOver Called when mouse enters the control.
* @param {boolean} props.showSideInLabel Show side in control label.
* @param {Array} props.sides Available sides for control.
* @param {boolean} props.useSelect Use select control for predefined values.
* @param {Object} props.values Current spacing values.
* @param {Object} props Component props.
* @param {Object} props.inputProps Additional props for input controls.
* @param {string} props.label Label for the control.
* @param {boolean} props.linkedByDefault Whether sides are linked by default when no values are defined.
* @param {number} props.minimumCustomValue Minimum value for custom input.
* @param {( values: Object ) => void} props.onChange Called when spacing values change.
* @param {( event: React.SyntheticEvent ) => void} props.onMouseOut Called when mouse leaves the control.
* @param {( event: React.SyntheticEvent ) => void} props.onMouseOver Called when mouse enters the control.
* @param {boolean} props.showSideInLabel Show side in control label.
* @param {Array} props.sides Available sides for control.
* @param {boolean} props.useSelect Use select control for predefined values.
* @param {Object} props.values Current spacing values.
* @return {Element} Spacing sizes control component.
*/
export default function SpacingSizesControl( {
inputProps,
label: labelProp,
linkedByDefault: linkedByDefaultProp,
minimumCustomValue = 0,
onChange,
onMouseOut,
Expand All @@ -74,14 +77,28 @@ export default function SpacingSizesControl( {
values,
} ) {
const spacingSizes = useSpacingSizes();
const [ linkedByDefaultSetting = true ] = useSettings(
'spacing.linkedByDefault'
);
// If linkedByDefault is an object, callers (like DimensionsPanel) resolve
// the explicit margin/padding boolean prop. For standalone use where the prop
// is omitted, fall back to boolean settings or default to true.
const resolvedSetting =
typeof linkedByDefaultSetting === 'boolean'
? linkedByDefaultSetting
: true;
const linkedByDefault = linkedByDefaultProp ?? resolvedSetting;
const inputValues = values || DEFAULT_VALUES;

const hasOneSide = sides?.length === 1;
const hasOnlyAxialSides =
sides?.includes( 'horizontal' ) &&
sides?.includes( 'vertical' ) &&
sides?.length === 2;

const [ view, setView ] = useState( getInitialView( inputValues, sides ) );
const [ view, setView ] = useState( () =>
getInitialView( inputValues, sides, linkedByDefault )
);

const toggleLinked = () => {
setView( view === VIEWS.axial ? VIEWS.custom : VIEWS.axial );
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ vi.mock( import( '../../use-settings' ), () => ( {
'spacing.spacingSizes.theme': [],
'spacing.spacingSizes.default': [],
'spacing.defaultSpacingSizes': true,
'spacing.linkedByDefault': true,
};
return keys.map( ( key ) => defaults[ key ] );
} ),
Expand Down Expand Up @@ -1065,6 +1066,22 @@ describe( 'SpacingSizesControl', () => {
screen.getAllByLabelText( 'Set custom value' )[ 0 ]
).toBeInTheDocument();
} );

it( 'respects linkedByDefault prop when false', () => {
render(
<SpacingSizesControl
{ ...defaultProps }
values={ undefined }
linkedByDefault={ false }
/>
);

// In custom view, all 4 separated side sliders should be rendered.
expect( screen.getAllByRole( 'slider' ) ).toHaveLength( 4 );
expect(
screen.getByRole( 'button', { name: 'Link sides' } )
).toBeInTheDocument();
} );
} );

describe( 'Label Customization', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,18 @@ describe( 'getInitialView', () => {
)
).not.toBe( VIEWS.axial );
} );
it( 'should return axial view when only axial sides are supported with matching values, even if linkedByDefault is false', () => {
expect(
getInitialView(
{ top: '1em', bottom: '1em', left: '2em', right: '2em' },
[ 'horizontal', 'vertical' ],
false
)
).toBe( VIEWS.axial );
expect(
getInitialView( {}, [ 'horizontal', 'vertical' ], false )
).toBe( VIEWS.axial );
} );
} );

describe( 'Custom sides view', () => {
Expand All @@ -209,6 +221,28 @@ describe( 'getInitialView', () => {
getInitialView( undefined, [ 'right', 'bottom', 'left' ] )
).toBe( VIEWS.custom );
} );
it( 'should return custom view when linkedByDefault is false, even if values match or are undefined', () => {
expect( getInitialView( {}, ALL_SIDES, false ) ).toBe(
VIEWS.custom
);
expect( getInitialView( undefined, ALL_SIDES, false ) ).toBe(
VIEWS.custom
);
expect(
getInitialView(
{ top: '0px', right: '0px', bottom: '0px', left: '0px' },
ALL_SIDES,
false
)
).toBe( VIEWS.custom );
expect(
getInitialView(
{ top: '1em', right: '1em', bottom: '1em', left: '1em' },
ALL_SIDES,
false
)
).toBe( VIEWS.custom );
} );
it( 'should return custom sides view when all values match and unbalanced sides config', () => {
expect(
getInitialView( { top: '1em', right: '1em', bottom: '1em' }, [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,20 +235,24 @@ export function hasBalancedSidesSupport( sides: string[] = [] ) {
/**
* Determines which view the SpacingSizesControl should default to on its
* first render; Axial, Custom, or Single side.
*
* @param values Current side values.
* @param sides Supported sides.
* @param values Current side values.
* @param sides Supported sides.
* @param linkedByDefault Whether sides are linked by default when no values are defined.
*
* @return View to display.
*/
export function getInitialView( values: SideValues = {}, sides?: string[] ) {
export function getInitialView(
values: SideValues = {},
sides?: string[],
linkedByDefault: boolean = true
) {
const { top, right, bottom, left } = values;
const sideValues = [ top, right, bottom, left ].filter( Boolean );

// Axial ( Horizontal & vertical ).
// - Has axial side support
// - Has axial side values which match
// - Has no values and the supported sides are balanced
// - Has axial side values which match and sides are linked by default, or only axial sides supported
// - Has no values, the supported sides are balanced, and sides are linked by default
const hasMatchingAxialValues =
top === bottom && left === right && ( !! top || !! left );
const hasNoValuesAndBalancedSides =
Expand All @@ -260,7 +264,10 @@ export function getInitialView( values: SideValues = {}, sides?: string[] ) {

if (
hasAxisSupport( sides ) &&
( hasMatchingAxialValues || hasNoValuesAndBalancedSides )
( ( ( hasMatchingAxialValues || hasNoValuesAndBalancedSides ) &&
linkedByDefault ) ||
( hasOnlyAxialSides &&
( ! sideValues.length || hasMatchingAxialValues ) ) )
) {
return VIEWS.axial;
}
Expand Down
Loading
Loading