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
1 change: 1 addition & 0 deletions packages/ui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

### New Features

- `Autocomplete`, `Combobox`, `Select`, `SearchableSelect`, `SearchableChipSelect`, and the select controls: Add `Separator` subcomponents for dividing popup items and groups ([#83776](https://github.com/WordPress/gutenberg/pull/83776)).
- Add `SearchableSelectControl` component ([#80979](https://github.com/WordPress/gutenberg/pull/80979)).

### Enhancements
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/form/primitives/autocomplete/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { Portal } from './portal';
export { Positioner } from './positioner';
export { Root } from './root';
export { Row } from './row';
export { Separator } from './separator';
export { Status } from './status';
export { useFilteredItems } from './use-filtered-items';
export { Value } from './value';
28 changes: 28 additions & 0 deletions packages/ui/src/form/primitives/autocomplete/separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { Autocomplete as _Autocomplete } from '@base-ui/react/autocomplete';
import clsx from 'clsx';
import { forwardRef } from '@wordpress/element';
import itemPopupStyles from '../../../utils/css/item-popup.module.css';
import type { AutocompleteSeparatorProps } from './types';

/**
* Renders a visual separator between autocomplete items or groups.
*/
export const Separator = forwardRef<
HTMLDivElement,
AutocompleteSeparatorProps
>( function AutocompleteSeparator(
{
className,
orientation: _orientation,
...props
}: _Autocomplete.Separator.Props,
ref
) {
return (
<_Autocomplete.Separator
ref={ ref }
className={ clsx( itemPopupStyles.separator, className ) }
{ ...props }
/>
);
} );
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import type { CSSProperties } from 'react';
import { useRef, useState } from '@wordpress/element';
import { Fragment, useRef, useState } from '@wordpress/element';
import { search } from '@wordpress/icons';
import * as Autocomplete from '../index';
import { Icon } from '../../../../icon';
Expand Down Expand Up @@ -39,6 +39,7 @@ const meta: Meta< typeof Autocomplete.Root > = {
'Autocomplete.ItemLabel': Autocomplete.ItemLabel,
'Autocomplete.ItemDescription': Autocomplete.ItemDescription,
'Autocomplete.Row': Autocomplete.Row,
'Autocomplete.Separator': Autocomplete.Separator,
'Autocomplete.Value': Autocomplete.Value,
'Autocomplete.Empty': Autocomplete.Empty,
'Autocomplete.Status': Autocomplete.Status,
Expand Down Expand Up @@ -618,6 +619,43 @@ export const Grouped: Story = {
},
};

/**
* Use `Autocomplete.Separator` to set choices apart from each other.
*/
export const WithSeparator: Story = {
args: {
items: [ ...COMMANDS.slice( 0, 3 ), { id: 'help', value: 'Get help' } ],
openOnInputClick: true,
children: [
<Autocomplete.Input
aria-label="Command"
placeholder="Type a command"
key="input"
/>,
<Autocomplete.Popup key="popup">
<Autocomplete.List>
<Autocomplete.ListBody>
<Autocomplete.Collection>
{ ( item: FixtureItem, index: number ) => (
<Fragment key={ item.id }>
{ item.id === 'help' && index > 0 && (
<Autocomplete.Separator />
) }
<Autocomplete.Item value={ item }>
<Autocomplete.ItemLabel>
{ item.value }
</Autocomplete.ItemLabel>
</Autocomplete.Item>
</Fragment>
) }
</Autocomplete.Collection>
</Autocomplete.ListBody>
</Autocomplete.List>
</Autocomplete.Popup>,
],
},
};

const EMOJI_COLUMNS = 8;
const EMOJI_TILE_SIZE = 'var(--wpds-dimension-size-lg)';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ describe( 'Autocomplete', () => {
const popupRef = createRef< HTMLDivElement >();
const listRef = createRef< HTMLDivElement >();
const listBodyRef = createRef< HTMLDivElement >();
const separatorRef = createRef< HTMLDivElement >();
const itemRef = createRef< HTMLDivElement >();
const clearRef = createRef< HTMLButtonElement >();
const emptyRef = createRef< HTMLDivElement >();
Expand Down Expand Up @@ -103,6 +104,7 @@ describe( 'Autocomplete', () => {
</Autocomplete.Collection>
</Autocomplete.ListBody>
</Autocomplete.List>
<Autocomplete.Separator ref={ separatorRef } />
<Autocomplete.Clear ref={ clearRef } />
</Autocomplete.Popup>
</Autocomplete.Root>
Expand All @@ -118,6 +120,7 @@ describe( 'Autocomplete', () => {
} );
expect( listRef.current ).toBeInstanceOf( HTMLDivElement );
expect( listBodyRef.current ).toBeInstanceOf( HTMLDivElement );
expect( separatorRef.current ).toBeInstanceOf( HTMLDivElement );
expect( itemRef.current ).toBeInstanceOf( HTMLDivElement );
expect( clearRef.current ).toBeInstanceOf( HTMLButtonElement );
expect( emptyRef.current ).toBeInstanceOf( HTMLDivElement );
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/form/primitives/autocomplete/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,11 @@ export type AutocompleteRowProps = ComponentProps<
children?: React.ReactNode;
};

export type AutocompleteSeparatorProps = Omit<
ComponentProps< typeof _Autocomplete.Separator >,
'orientation'
>;

export type AutocompleteStatusProps = ComponentProps<
typeof _Autocomplete.Status
> & {
Expand Down
1 change: 1 addition & 0 deletions packages/ui/src/form/primitives/combobox/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ export { Portal } from './portal';
export { Positioner } from './positioner';
export { Popup } from './popup';
export { Root } from './root';
export { Separator } from './separator';
export { Status } from './status';
export { Trigger } from './trigger';
export { useFilteredItems } from './use-filtered-items';
Expand Down
27 changes: 27 additions & 0 deletions packages/ui/src/form/primitives/combobox/separator.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Combobox as _Combobox } from '@base-ui/react/combobox';
import clsx from 'clsx';
import { forwardRef } from '@wordpress/element';
import itemPopupStyles from '../../../utils/css/item-popup.module.css';
import type { ComboboxSeparatorProps } from './types';

/**
* Renders a visual separator between combobox items or groups.
*/
export const Separator = forwardRef< HTMLDivElement, ComboboxSeparatorProps >(
function ComboboxSeparator(
{
className,
orientation: _orientation,
...props
}: _Combobox.Separator.Props,
ref
) {
return (
<_Combobox.Separator
ref={ ref }
className={ clsx( itemPopupStyles.separator, className ) }
{ ...props }
/>
);
}
);
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { useRef, useState } from '@wordpress/element';
import { Fragment, useRef, useState } from '@wordpress/element';
import * as Combobox from '../index';
import { Spinner } from '../../../../spinner';
import { Stack } from '../../../../stack';
Expand Down Expand Up @@ -31,6 +31,7 @@ const meta: Meta< typeof Combobox.Root > = {
'Combobox.Item': Combobox.Item,
'Combobox.ItemLabel': Combobox.ItemLabel,
'Combobox.ItemDescription': Combobox.ItemDescription,
'Combobox.Separator': Combobox.Separator,
'Combobox.Value': Combobox.Value,
'Combobox.Chips': Combobox.Chips,
'Combobox.ChipWithRemove': Combobox.ChipWithRemove,
Expand Down Expand Up @@ -468,6 +469,41 @@ export const Grouped: Story = {
},
};

/**
* Use `Combobox.Separator` to set choices apart from each other.
*/
export const WithSeparator: Story = {
args: {
items: [ ...ITEMS.slice( 0, 3 ), { value: 'other', label: 'Other' } ],
children: [
<Combobox.Trigger aria-label="Fruit" key="trigger" />,
<Combobox.Popup aria-label="Fruit" key="popup">
<div style={ inputWrapperStyle }>
<Combobox.Input aria-label="Search" placeholder="Search" />
</div>
<Combobox.List>
<Combobox.ListBody>
<Combobox.Collection>
{ ( item: FixtureItem, index: number ) => (
<Fragment key={ item.value }>
{ item.value === 'other' && index > 0 && (
<Combobox.Separator />
) }
<Combobox.Item value={ item }>
<Combobox.ItemLabel>
{ item.label }
</Combobox.ItemLabel>
</Combobox.Item>
</Fragment>
) }
</Combobox.Collection>
</Combobox.ListBody>
</Combobox.List>
</Combobox.Popup>,
],
},
};

/**
* Popovers in Gutenberg are managed with explicit z-index values, which can create
* situations where a popover renders below another popover, when you want it to be rendered above.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ describe( 'Combobox', () => {
const inputRef = createRef< HTMLInputElement >();
const listRef = createRef< HTMLDivElement >();
const listBodyRef = createRef< HTMLDivElement >();
const separatorRef = createRef< HTMLDivElement >();
const listFooterRef = createRef< HTMLDivElement >();
const itemRef = createRef< HTMLDivElement >();
const chipsRef = createRef< HTMLDivElement >();
Expand Down Expand Up @@ -124,6 +125,7 @@ describe( 'Combobox', () => {
) }
</Combobox.Collection>
</Combobox.ListBody>
<Combobox.Separator ref={ separatorRef } />
<Combobox.ListFooter ref={ listFooterRef } />
</Combobox.List>
</Combobox.Popup>
Expand All @@ -145,6 +147,7 @@ describe( 'Combobox', () => {
expect( inputRef.current ).toBeInstanceOf( HTMLInputElement );
expect( listRef.current ).toBeInstanceOf( HTMLDivElement );
expect( listBodyRef.current ).toBeInstanceOf( HTMLDivElement );
expect( separatorRef.current ).toBeInstanceOf( HTMLDivElement );
expect( listFooterRef.current ).toBeInstanceOf( HTMLDivElement );
expect( itemRef.current ).toBeInstanceOf( HTMLDivElement );
expect( chipsRef.current ).toBeInstanceOf( HTMLDivElement );
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/form/primitives/combobox/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,11 @@ export type ComboboxRootProps<
Multiple extends boolean | undefined = false,
> = _Combobox.Root.Props< Value, Multiple >;

export type ComboboxSeparatorProps = Omit<
ComponentProps< typeof _Combobox.Separator >,
'orientation'
>;

export type ComboboxStatusProps = ComponentProps< typeof _Combobox.Status > & {
children?: React.ReactNode;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { ItemLabel } from '../combobox/item-label';
import { ChipWithRemove } from '../combobox/chip-with-remove';
import { Group } from '../combobox/group';
import { GroupLabel } from '../combobox/group-label';
import { Separator } from '../combobox/separator';
import { Collection } from '../combobox/collection';
import { useFilteredItems } from '../combobox/use-filtered-items';
import { SearchableChipSelect as _SearchableChipSelect } from './searchable-chip-select';
Expand All @@ -28,6 +29,7 @@ export const SearchableChipSelect = Object.assign( _SearchableChipSelect, {
ChipWithRemove,
Group,
GroupLabel,
Separator,
Collection,
useFilteredItems,
} );
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { Fragment } from '@wordpress/element';
import { SearchableChipSelect } from '../';
import {
GROUPED_ITEMS,
Expand All @@ -16,6 +17,7 @@ const meta: Meta< typeof SearchableChipSelect > = {
subcomponents: {
'SearchableChipSelect.Group': SearchableChipSelect.Group,
'SearchableChipSelect.GroupLabel': SearchableChipSelect.GroupLabel,
'SearchableChipSelect.Separator': SearchableChipSelect.Separator,
'SearchableChipSelect.Item': SearchableChipSelect.Item,
'SearchableChipSelect.ItemLabel': SearchableChipSelect.ItemLabel,
'SearchableChipSelect.ItemDescription':
Expand Down Expand Up @@ -144,6 +146,28 @@ export const Grouped: Story = {
},
};

/**
* Use `SearchableChipSelect.Separator` to set choices apart from each other.
*/
export const WithSeparator: Story = {
args: {
...Default.args,
items: [ ...ITEMS.slice( 0, 3 ), { value: 'other', label: 'Other' } ],
children: ( item: FixtureItem, index: number ) => (
<Fragment key={ item.value }>
{ item.value === 'other' && index > 0 && (
<SearchableChipSelect.Separator />
) }
<SearchableChipSelect.Item value={ item }>
<SearchableChipSelect.ItemLabel>
{ item.label }
</SearchableChipSelect.ItemLabel>
</SearchableChipSelect.Item>
</Fragment>
),
},
};

/**
* Use the `showClearButton` prop to hide the clear button.
*/
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/form/primitives/searchable-select/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ItemDescription } from '../combobox/item-description';
import { ItemLabel } from '../combobox/item-label';
import { Group } from '../combobox/group';
import { GroupLabel } from '../combobox/group-label';
import { Separator } from '../combobox/separator';
import { Collection } from '../combobox/collection';
import { useFilteredItems } from '../combobox/use-filtered-items';
import { SearchableSelect as _SearchableSelect } from './searchable-select';
Expand All @@ -25,6 +26,7 @@ export const SearchableSelect = Object.assign( _SearchableSelect, {
ItemDescription,
Group,
GroupLabel,
Separator,
Collection,
useFilteredItems,
} );
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
import { fn } from 'storybook/test';
import { Fragment } from '@wordpress/element';
import { SearchableSelect } from '../';
import {
GROUPED_ITEMS,
Expand All @@ -15,6 +16,7 @@ const meta: Meta< typeof SearchableSelect > = {
subcomponents: {
'SearchableSelect.Group': SearchableSelect.Group,
'SearchableSelect.GroupLabel': SearchableSelect.GroupLabel,
'SearchableSelect.Separator': SearchableSelect.Separator,
'SearchableSelect.Item': SearchableSelect.Item,
'SearchableSelect.ItemLabel': SearchableSelect.ItemLabel,
'SearchableSelect.ItemDescription': SearchableSelect.ItemDescription,
Expand Down Expand Up @@ -156,3 +158,25 @@ export const Grouped: Story = {
),
},
};

/**
* Use `SearchableSelect.Separator` to set choices apart from each other.
*/
export const WithSeparator: Story = {
args: {
...Default.args,
items: [ ...ITEMS.slice( 0, 3 ), { value: 'other', label: 'Other' } ],
children: ( item: FixtureItem, index: number ) => (
<Fragment key={ item.value }>
{ item.value === 'other' && index > 0 && (
<SearchableSelect.Separator />
) }
<SearchableSelect.Item value={ item }>
<SearchableSelect.ItemLabel>
{ item.label }
</SearchableSelect.ItemLabel>
</SearchableSelect.Item>
</Fragment>
),
},
};
1 change: 1 addition & 0 deletions packages/ui/src/form/primitives/select/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,5 @@ export { Popup } from './popup';
export { Portal } from './portal';
export { Positioner } from './positioner';
export { Root } from './root';
export { Separator } from './separator';
export { Trigger } from './trigger';
Loading
Loading