Skip to content
2 changes: 2 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@

- `Text`: Make single-line truncation and `isBlock` overrides consistent across the main document and iframes ([#82573](https://github.com/WordPress/gutenberg/pull/82573)).
- `CardDivider`: Apply the Card's width, display, and border color consistently inside iframes ([#82577](https://github.com/WordPress/gutenberg/pull/82577)).
- `BorderControl`, `BorderBoxControl`: Stop offering `%` as a width unit, since `border-width` does not accept percentages. A width already set in `%` keeps showing its unit ([#83492](https://github.com/WordPress/gutenberg/pull/83492)).
- `UnitControl`: After clearing a value whose unit is not in `units`, give the next typed value the unit shown in the select instead of the cleared one ([#83492](https://github.com/WordPress/gutenberg/pull/83492)).

### Internal

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { __ } from '@wordpress/i18n';
import BorderControlDropdown from '../border-control-dropdown';
import UnitControl from '../../unit-control';
import { CSS_UNITS } from '../../unit-control/utils';
import RangeControl from '../../range-control';
import { HStack } from '../../h-stack';
import { StyledLabel } from '../../base-control/styles/base-control-styles';
Expand All @@ -12,6 +13,9 @@ import { useBorderControl } from './hook';
import type { BorderControlProps, LabelProps } from '../types';
import { Spacer } from '../../spacer';

// Percentages are not valid `border-width` values.
const BORDER_WIDTH_UNITS = CSS_UNITS.filter( ( unit ) => unit.value !== '%' );

const BorderLabel = ( props: LabelProps ) => {
const { label, hideLabelFromVision } = props;

Expand Down Expand Up @@ -93,6 +97,7 @@ const UnconnectedBorderControl = (
onChange={ onWidthChange }
value={ border?.width || '' }
placeholder={ placeholder }
units={ BORDER_WIDTH_UNITS }
disableUnits={ disableUnits }
__unstableInputWidth={ inputWidth }
/>
Expand All @@ -105,7 +110,7 @@ const UnconnectedBorderControl = (
max={ 100 }
min={ 0 }
onChange={ onSliderChange }
step={ [ 'px', '%' ].includes( widthUnit ) ? 1 : 0.1 }
step={ widthUnit === 'px' ? 1 : 0.1 }
value={ widthValue || undefined }
withInputField={ false }
/>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,44 @@ describe( 'BorderControl', () => {
expect( widthInput ).toHaveAttribute( 'placeholder', 'Mixed' );
} );

it( 'should not offer a percentage unit for the width', () => {
const props = createProps();
render( <TestBorderControl { ...props } /> );

const unitSelect = screen.getByRole( 'combobox', {
name: 'Select unit',
} );
expect(
within( unitSelect ).queryByRole( 'option', { name: '%' } )
).not.toBeInTheDocument();
} );

it( 'should keep the unit of a percentage width until it is changed', async () => {
const user = userEvent.setup();
const props = createProps( {
value: { ...defaultBorder, width: '10%' },
} );
const { rerender } = render( <TestBorderControl { ...props } /> );

const unitSelect = screen.getByRole( 'combobox', {
name: 'Select unit',
} );
expect( unitSelect ).toHaveValue( '%' );

await user.selectOptions( unitSelect, 'px' );

expect( props.onChange ).toHaveBeenNthCalledWith( 1, {
...defaultBorder,
width: '10px',
} );

rerender( <TestBorderControl { ...props } /> );

expect(
within( unitSelect ).queryByRole( 'option', { name: '%' } )
).not.toBeInTheDocument();
} );

describe( 'color indicator inline styles', () => {
const getIndicatorWrapper = ( border: Border ) => {
render(
Expand Down
8 changes: 7 additions & 1 deletion packages/components/src/unit-control/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,12 @@ function UnforwardedUnitControl(
return;
}

// After clearing a value whose unit isn't offered, the select shows the
// first offered unit, so only fall back to the current unit if offered.
const fallbackUnit = units.some( ( option ) => option.value === unit )
? unit
: undefined;

/*
* Customizing the onChange callback.
* This allows as to broadcast a combined value+unit to onChange.
Expand All @@ -127,7 +133,7 @@ function UnforwardedUnitControl(
nextQuantityValue,
units,
parsedQuantity,
unit
fallbackUnit
).join( '' );

onChangeProp?.( onChangeValue, changeProps );
Expand Down
37 changes: 37 additions & 0 deletions packages/components/src/unit-control/test/index.jsdom.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,43 @@ describe( 'UnitControl', () => {
expect( select ).toHaveValue( 'vmax' );
} );

it( 'should use the unit shown in the select after clearing a value whose unit is not offered', async () => {
const user = userEvent.setup();
const onChangeSpy = vi.fn();

const ControlledUnitControl = () => {
const [ value, setValue ] = useState< string | undefined >(
'10%'
);
return (
<UnitControl
value={ value }
units={ [
{ value: 'px', label: 'px' },
{ value: 'em', label: 'em' },
] }
onChange={ ( nextValue, extra ) => {
setValue( nextValue );
onChangeSpy( nextValue, extra );
} }
/>
);
};
render( <ControlledUnitControl /> );

const input = getInput();
await user.clear( input );

expect( getSelect() ).toHaveValue( 'px' );

await user.type( input, '5' );

expect( onChangeSpy ).toHaveBeenLastCalledWith(
'5px',
expect.anything()
);
} );

it( 'should run onBlur callback when the unit select is blurred', async () => {
const user = userEvent.setup();

Expand Down
Loading