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
2 changes: 2 additions & 0 deletions packages/block-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Breaking Changes

- Date format and preset selectors now use `@wordpress/ui`. They no longer render `.components-custom-select-control` or its item and hint classes, the `has-hint` modifier, the legacy `.components-input-base`, `.components-input-control__container`, `.components-input-control__backdrop`, `.components-input-control__suffix`, and `.components-input-control-suffix-wrapper` classes, `.components-base-control__label`, or `.block-editor-date-format-picker__custom-format-select-control__custom-option`. Control-specific root classes are retained. Update overrides that target the removed classes to use control-specific hooks or Design System theme tokens. Popup items are now portaled outside the control root, so root-descendant selectors no longer reach them ([#84584](https://github.com/WordPress/gutenberg/pull/84584)).
- `ImageSizeControl`: The event in the `onChangeImage` callback's `{ event }` argument now comes from popup selection rather than a native select change. Read the selected value from the `slug` argument rather than `event.target.value` ([#84546](https://github.com/WordPress/gutenberg/pull/84546)).

- Raise the minimum required Node.js version to `>=22.13.0`, for stable `require(esm)` support ([#84313](https://github.com/WordPress/gutenberg/pull/84313)).
Expand All @@ -15,6 +16,7 @@

### Enhancements

- Migrate date format and preset input custom selectors to `@wordpress/ui`, preserving saved values, option previews, forwarded trigger props, and control-specific root CSS hooks. Also migrate the date format switch, custom input, link, and layout, and simplify the aspect-ratio control's option rendering ([#84584](https://github.com/WordPress/gutenberg/pull/84584)).
- Migrate `FontAppearanceControl` to `@wordpress/ui`. Internal markup and CSS classes have changed; custom CSS overrides may need updates. See the pull request for details ([#84716](https://github.com/WordPress/gutenberg/pull/84716)).
- Migrate HTML element, image size, resolution, aspect ratio, and text-shadow preset controls to `@wordpress/ui` `SelectControl` ([#84546](https://github.com/WordPress/gutenberg/pull/84546)).
- `BlockBreadcrumb`: Use the `Breadcrumb` component from `@wordpress/ui` to collapse overflowing ancestors into a menu while preserving block selection and document-root activation, including multi-block selections. Use block titles when renamed labels are blank after stripping HTML. Accept a `className` for consumer-specific layout ([#84295](https://github.com/WordPress/gutenberg/pull/84295)).
Expand Down
108 changes: 63 additions & 45 deletions packages/block-editor/src/components/date-format-picker/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import { _x, __ } from '@wordpress/i18n';
import { dateI18n, humanTimeDiff } from '@wordpress/date';
import { useState, createInterpolateElement } from '@wordpress/element';
import {
TextControl,
ExternalLink,
ToggleControl,
__experimentalVStack as VStack,
CustomSelectControl,
} from '@wordpress/components';
import { VisuallyHidden } from '@wordpress/ui';
Fieldset,
InputControl,
Link,
SelectControl,
Stack,
SwitchControl,
} from '@wordpress/ui';

// So that we illustrate the different formats in the dropdown properly, show a date that is
// somewhat recent, has a day greater than 12, and a month with more than three letters.
Expand All @@ -26,40 +26,41 @@ if ( exampleDate.getMonth() === 4 ) {
*
* @see https://github.com/WordPress/gutenberg/blob/HEAD/packages/block-editor/src/components/date-format-picker/README.md
*
* @param {Object} props
* @param {string|null} props.format The selected date format. If `null`, _Default_ is selected.
* @param {string} props.defaultFormat The date format that will be used if the user selects 'Default'.
* @param {Function} props.onChange Called when a selection is made. If `null`, _Default_ is selected.
* @param {Object} props
* @param {string|null} props.format The selected date format. If `null`, _Default_ is selected.
* @param {string} props.defaultFormat The date format that will be used if the user selects 'Default'.
* @param {(format: string|null) => void} props.onChange Called when a selection is made. If `null`, _Default_ is selected.
*/
export default function DateFormatPicker( {
format,
defaultFormat,
onChange,
} ) {
return (
<VStack
as="fieldset"
spacing={ 4 }
<Stack
render={ <Fieldset.Root /> }
direction="column"
gap="lg"
className="block-editor-date-format-picker"
>
<VisuallyHidden render={ <legend /> }>
<Fieldset.Legend hideFromVision>
{ __( 'Date format' ) }
</VisuallyHidden>
<ToggleControl
</Fieldset.Legend>
<SwitchControl
label={ __( 'Default format' ) }
help={ `${ __( 'Example:' ) } ${ dateI18n(
description={ `${ __( 'Example:' ) } ${ dateI18n(
defaultFormat,
exampleDate
) }` }
checked={ ! format }
onChange={ ( checked ) =>
onCheckedChange={ ( checked ) =>
onChange( checked ? null : defaultFormat )
}
/>
{ format && (
<NonDefaultControls format={ format } onChange={ onChange } />
) }
</VStack>
</Stack>
);
}

Expand Down Expand Up @@ -91,64 +92,81 @@ function NonDefaultControls( { format, onChange } ) {
];

const suggestedOptions = [
...suggestedFormats.map( ( suggestedFormat, index ) => ( {
key: `suggested-${ index }`,
name: dateI18n( suggestedFormat, exampleDate ),
format: suggestedFormat,
...suggestedFormats.map( ( suggestedFormat ) => ( {
value: suggestedFormat,
label: dateI18n( suggestedFormat, exampleDate ),
} ) ),
{
key: 'human-diff',
name: humanTimeDiff( exampleDate ),
format: 'human-diff',
value: 'human-diff',
label: humanTimeDiff( exampleDate ),
},
];

const customOption = {
key: 'custom',
name: __( 'Custom' ),
className:
'block-editor-date-format-picker__custom-format-select-control__custom-option',
hint: __( 'Enter your own date format' ),
value: 'custom',
label: __( 'Custom' ),
description: __( 'Enter your own date format' ),
};

const [ isCustom, setIsCustom ] = useState(
() =>
!! format &&
! suggestedOptions.some( ( option ) => option.format === format )
! suggestedOptions.some( ( option ) => option.value === format )
);

return (
<VStack>
<CustomSelectControl
<Stack direction="column" gap="sm">
<SelectControl
label={ __( 'Choose a format' ) }
options={ [ ...suggestedOptions, customOption ] }
items={ [ ...suggestedOptions, customOption ] }
value={
isCustom
? customOption
: ( suggestedOptions.find(
( option ) => option.format === format
( option ) => option.value === format
) ?? customOption )
}
onChange={ ( { selectedItem } ) => {
onValueChange={ ( selectedItem ) => {
if ( ! selectedItem ) {
return;
}
if ( selectedItem === customOption ) {
setIsCustom( true );
} else {
setIsCustom( false );
onChange( selectedItem.format );
onChange( selectedItem.value );
}
} }
/>
>
{ suggestedOptions.map( ( option ) => (
<SelectControl.Item key={ option.value } value={ option }>
<SelectControl.ItemLabel>
{ option.label }
</SelectControl.ItemLabel>
</SelectControl.Item>
) ) }
<SelectControl.Separator />
<SelectControl.Item value={ customOption }>
<SelectControl.ItemLabel>
{ customOption.label }
</SelectControl.ItemLabel>
<SelectControl.ItemDescription>
{ customOption.description }
</SelectControl.ItemDescription>
</SelectControl.Item>
</SelectControl>
{ isCustom && (
<TextControl
<InputControl
label={ __( 'Custom format' ) }
hideLabelFromVision
help={ createInterpolateElement(
details={ createInterpolateElement(
__(
'Enter a date or time <Link>format string</Link>.'
),
{
Link: (
<ExternalLink
<Link
openInNewTab
href={ __(
'https://wordpress.org/documentation/article/customize-date-and-time-format/'
) }
Expand All @@ -157,9 +175,9 @@ function NonDefaultControls( { format, onChange } ) {
}
) }
value={ format }
onChange={ ( value ) => onChange( value ) }
onValueChange={ ( value ) => onChange( value ) }
/>
) }
</VStack>
</Stack>
);
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
@use "@wordpress/base-styles/variables" as *;
@use "@wordpress/base-styles/colors" as *;

.block-editor-date-format-picker {
margin: 0 0 $grid-unit-20;
padding: 0;
border: none;
}

.block-editor-date-format-picker__custom-format-select-control__custom-option {
border-top: 1px solid $gray-300;
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,24 +109,9 @@ export default function AspectRatioTool( {
resolvedOptions.find( ( item ) => ! item.disabled ) ??
null
}
items={ resolvedOptions }
items={ resolvedOptions.filter( ( item ) => ! item.hidden ) }
onValueChange={ ( item ) => onChange( item?.value ?? '' ) }
>
{ resolvedOptions
.filter( ( item ) => ! item.hidden )
.map( ( item ) => (
<SelectControl.Item
key={ item.value }
value={ item }
label={ item.label }
disabled={ item.disabled }
>
<SelectControl.ItemLabel>
{ item.label }
</SelectControl.ItemLabel>
</SelectControl.Item>
) ) }
</SelectControl>
/>
</InheritanceToolsPanelItem>
);
}
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {
Button,
CustomSelectControl,
Icon as WCIcon,
RangeControl,
__experimentalHStack as HStack,
__experimentalParseQuantityAndUnitFromRawValue as parseQuantityAndUnitFromRawValue,
} from '@wordpress/components';
import { Select } from '@wordpress/ui';
import { usePrevious } from '@wordpress/compose';
import { __, sprintf } from '@wordpress/i18n';
import { settings } from '@wordpress/icons';
Expand Down Expand Up @@ -143,8 +143,8 @@ export default function PresetInputControl( {
}

const options = selectListOptions.map( ( size, index ) => ( {
key: index,
name: size.name,
value: index,
label: size.name,
} ) );

const resolvedPresetValue = isValuePreset( value, presetType )
Expand Down Expand Up @@ -266,38 +266,50 @@ export default function PresetInputControl( {
/>
) }
{ hasPresets && ! showRangeControl && ! showCustomValueControl && (
<CustomSelectControl
className="preset-input-control__custom-select-control"
hideLabelFromVision
label={ ariaLabel }
onBlur={ onMouseOut }
onChange={ ( selection ) => {
<Select.Root
items={ options }
onValueChange={ ( selection ) => {
if ( ! selection ) {
return;
}
if (
showCustomValueInSelectList &&
selection.selectedItem.key === options.length - 1
selection.value === options.length - 1
) {
setShowCustomValueControl( true );
} else {
onChange(
getNewPresetValue(
selection.selectedItem.key,
selection.value,
'selectList'
)
);
}
} }
onFocus={ onMouseOver }
onMouseOut={ onMouseOut }
onMouseOver={ onMouseOver }
options={ options }
value={
// passing empty string as a fallback to continue using the
// component in controlled mode
options.find(
( option ) => option.key === currentValue
) || ''
( option ) => option.value === currentValue
) ?? options[ 0 ]
}
/>
>
<Select.Trigger
className="preset-input-control__custom-select-control"
aria-label={ ariaLabel }
onBlur={ onMouseOut }
onFocus={ onMouseOver }
onMouseOut={ onMouseOut }
onMouseOver={ onMouseOver }
/>
<Select.Popup>
{ options.map( ( option ) => (
<Select.Item key={ option.value } value={ option }>
<Select.ItemLabel>
{ option.label }
</Select.ItemLabel>
</Select.Item>
) ) }
</Select.Popup>
</Select.Root>
) }
{ hasPresets && ! disableCustomValues && (
<Button
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ describe( 'PresetInputControl browser interactions', () => {

it( 'can interact with select dropdown options', async () => {
const user = userEvent.setup();
const onMouseOver = vi.fn();
const onMouseOut = vi.fn();
const manyPresets = Array.from( { length: 12 }, ( _, index ) => ( {
name: `Preset ${ index + 1 }`,
slug: `preset-${ index + 1 }`,
Expand All @@ -51,13 +53,27 @@ describe( 'PresetInputControl browser interactions', () => {
createElement( PresetInputControl, {
...defaultProps,
presets: manyPresets,
onMouseOver,
onMouseOut,
} )
);

await user.click( screen.getByRole( 'combobox' ) );
await user.tab();
expect( onMouseOver ).toHaveBeenCalled();

const trigger = screen.getByRole( 'combobox' );
await user.click( trigger );

await expect
.element( page.getByRole( 'combobox', { expanded: true } ) )
.toBeVisible();

await user.click( screen.getByRole( 'option', { name: 'Preset 2' } ) );
expect( defaultProps.onChange ).toHaveBeenLastCalledWith(
'var:preset|spacing|preset-2'
);
await expect.element( trigger ).toHaveFocus();
await user.tab();
expect( onMouseOut ).toHaveBeenCalled();
} );
} );
3 changes: 0 additions & 3 deletions tools/eslint/suppressions.json
Original file line number Diff line number Diff line change
Expand Up @@ -929,9 +929,6 @@
}
},
"packages/block-editor/src/components/date-format-picker/index.jsx": {
"@wordpress/use-recommended-components": {
"count": 4
},
"react/jsx-filename-extension": {
"count": 1
}
Expand Down
Loading