Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Try Ariakit Select for new CustomSelectControl component #55790

Merged
merged 36 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d3573ae
Create base component using ariakit
brookewp Nov 7, 2023
9d34f51
Simplify story example
brookewp Nov 7, 2023
9000c5a
Add options for children and value with stories
brookewp Nov 7, 2023
0024d32
Update types
brookewp Nov 7, 2023
bc0683f
Add uncontrolled story and adapt value
brookewp Nov 9, 2023
2a3c220
Remove duplicate import
brookewp Nov 9, 2023
2b9174d
Add size prop
brookewp Nov 9, 2023
63267af
Changes from pairing session with ciampo
brookewp Nov 9, 2023
017bd82
Rename and cleanup
brookewp Nov 9, 2023
c6cffa6
Update styles
brookewp Nov 10, 2023
27b9854
Add context
brookewp Nov 10, 2023
add8e6d
Add README
brookewp Nov 10, 2023
d343165
Update manifest
brookewp Nov 10, 2023
3edc792
Update types and move context provider
brookewp Nov 10, 2023
7ef27ea
Add back types for multi-selection
brookewp Nov 15, 2023
d35a044
Remove file for moved component
brookewp Nov 17, 2023
ede0ef9
Add translation for placeholder
brookewp Nov 17, 2023
c3e4d29
Add check and cleanup styles
brookewp Nov 17, 2023
f771fad
Add multiselect example
brookewp Nov 17, 2023
3b5f968
Require type children for CustomSelectItem
brookewp Nov 17, 2023
e2ee037
Add small size and update sizing logic
brookewp Nov 17, 2023
b7b2fff
Update stories with ciampo’s suggested changes
brookewp Nov 17, 2023
e68ea07
Update styles to match legacy CustomSelectControl
brookewp Nov 17, 2023
27fc5fd
Cleanup based on PR feedback
brookewp Nov 18, 2023
24e08ad
Children as optional
brookewp Nov 20, 2023
f866d36
Incorporate ciampo’s suggestions to refine styles
brookewp Nov 20, 2023
ceb6b45
Make value required for CustomSelectItem and fix naming in comment
brookewp Nov 20, 2023
23c0f91
Merge branch 'trunk' into try/customselect-ariakit
brookewp Nov 20, 2023
0b8250a
Update changelog
brookewp Nov 20, 2023
c29569a
Refine styles and add styling to active item
brookewp Nov 21, 2023
39b7650
Only render label if defined
brookewp Nov 21, 2023
ff02e44
Cleanup story
brookewp Nov 21, 2023
b975f51
Add suggestions to WIP README and types
brookewp Nov 21, 2023
04faa3f
Make label required
brookewp Nov 23, 2023
df54fe8
Improve logic for default rendered value
brookewp Nov 23, 2023
45689ab
Update stories
brookewp Nov 23, 2023
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
6 changes: 6 additions & 0 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -785,6 +785,12 @@
"markdown_source": "../packages/components/src/confirm-dialog/README.md",
"parent": "components"
},
{
"title": "CustomSelectControlV2",
"slug": "custom-select-control-v2",
"markdown_source": "../packages/components/src/custom-select-control-v2/README.md",
"parent": "components"
},
{
"title": "CustomSelectControl",
"slug": "custom-select-control",
Expand Down
4 changes: 4 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

- `Tabs`: Memoize and expose the component context ([#56224](https://github.com/WordPress/gutenberg/pull/56224)).

### Internal

- Introduce experimental new version of `CustomSelectControl` based on `ariakit` ([#55790](https://github.com/WordPress/gutenberg/pull/55790))

## 25.12.0 (2023-11-16)

### Bug Fix
Expand Down
73 changes: 73 additions & 0 deletions packages/components/src/custom-select-control-v2/README.md
brookewp marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<div class="callout callout-alert">
This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
</div>

### `CustomSelect`

Used to render a customizable select control component.

#### Props

The component accepts the following props:

##### `children`: `React.ReactNode`

The child elements. This should be composed of CustomSelect.Item components.

- Required: yes

##### `defaultValue`: `string`

An optional default value for the control. If left `undefined`, the first non-disabled item will be used.

- Required: no

##### `label`: `string`

Label for the control.

- Required: yes

##### `onChange`: `( newValue: string ) => void`

A function that receives the new value of the input.

- Required: no

##### `renderSelectedValue`: `( selectValue: string ) => React.ReactNode`

Can be used to render select UI with custom styled values.

- Required: no

##### `size`: `'default' | 'large'`

The size of the control.

- Required: no
ciampo marked this conversation as resolved.
Show resolved Hide resolved

##### `value`: `string`

Can be used to externally control the value of the control.

- Required: no

### `CustomSelectItem`

Used to render a select item.

#### Props

The component accepts the following props:

##### `value`: `string`
ciampo marked this conversation as resolved.
Show resolved Hide resolved

The value of the select item. This will be used as the children if children are left `undefined`.

- Required: yes

##### `children`: `React.ReactNode`

The children to display for each select item. The `value` will be used if left `undefined`.

- Required: no
99 changes: 99 additions & 0 deletions packages/components/src/custom-select-control-v2/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* External dependencies
*/
// eslint-disable-next-line no-restricted-imports
import * as Ariakit from '@ariakit/react';
/**
* WordPress dependencies
*/
import { createContext, useContext } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';

/**
* Internal dependencies
*/
import * as Styled from './styles';
import type {
CustomSelectProps,
CustomSelectItemProps,
CustomSelectContext as CustomSelectContextType,
} from './types';

export const CustomSelectContext =
createContext< CustomSelectContextType >( undefined );

function defaultRenderSelectedValue( value: CustomSelectProps[ 'value' ] ) {
const isValueEmpty = Array.isArray( value )
? value.length === 0
: value === undefined || value === null;

if ( isValueEmpty ) {
return __( 'Select an item' );
}

if ( Array.isArray( value ) ) {
return value.length === 1
? value[ 0 ]
: // translators: %s: number of items selected (it will always be 2 or more items)
sprintf( __( '%s items selected' ), value.length );
}

return value;
}

export function CustomSelect( props: CustomSelectProps ) {
brookewp marked this conversation as resolved.
Show resolved Hide resolved
const {
children,
defaultValue,
label,
onChange,
size = 'default',
value,
renderSelectedValue = defaultRenderSelectedValue,
} = props;

const store = Ariakit.useSelectStore( {
setValue: ( nextValue ) => onChange?.( nextValue ),
defaultValue,
value,
} );

const { value: currentValue } = store.useState();

return (
<>
<Styled.CustomSelectLabel store={ store }>
brookewp marked this conversation as resolved.
Show resolved Hide resolved
{ label }
</Styled.CustomSelectLabel>
<Styled.CustomSelectButton
size={ size }
hasCustomRenderProp={ !! renderSelectedValue }
store={ store }
>
{ renderSelectedValue( currentValue ) }
<Ariakit.SelectArrow />
</Styled.CustomSelectButton>
<Styled.CustomSelectPopover gutter={ 12 } store={ store } sameWidth>
brookewp marked this conversation as resolved.
Show resolved Hide resolved
ciampo marked this conversation as resolved.
Show resolved Hide resolved
<CustomSelectContext.Provider value={ { store } }>
{ children }
</CustomSelectContext.Provider>
</Styled.CustomSelectPopover>
</>
);
}

export function CustomSelectItem( {
children,
...props
}: CustomSelectItemProps ) {
const customSelectContext = useContext( CustomSelectContext );
return (
<Styled.CustomSelectItem
brookewp marked this conversation as resolved.
Show resolved Hide resolved
brookewp marked this conversation as resolved.
Show resolved Hide resolved
store={ customSelectContext?.store }
{ ...props }
>
{ children ?? props.value }
<Ariakit.SelectItemCheck />
ciampo marked this conversation as resolved.
Show resolved Hide resolved
</Styled.CustomSelectItem>
);
}
brookewp marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
* External dependencies
*/
import type { Meta, StoryFn } from '@storybook/react';

/**
* WordPress dependencies
*/
import { useState } from '@wordpress/element';

/**
* Internal dependencies
*/
import { CustomSelect, CustomSelectItem } from '..';

const meta: Meta< typeof CustomSelect > = {
title: 'Components (Experimental)/CustomSelectControl v2',
component: CustomSelect,
subcomponents: {
// @ts-expect-error - See https://github.com/storybookjs/storybook/issues/23170
CustomSelectItem,
},
argTypes: {
children: { control: { type: null } },
renderSelectedValue: { control: { type: null } },
brookewp marked this conversation as resolved.
Show resolved Hide resolved
value: { control: { type: null } },
},
parameters: {
actions: { argTypesRegex: '^on.*' },
controls: { expanded: true },
docs: {
canvas: { sourceState: 'shown' },
source: { excludeDecorators: true },
},
},
decorators: [
( Story ) => (
<div
style={ {
minHeight: '150px',
} }
>
<Story />
</div>
),
],
};
export default meta;

const Template: StoryFn< typeof CustomSelect > = ( props ) => {
return <CustomSelect { ...props } />;
};

const ControlledTemplate: StoryFn< typeof CustomSelect > = ( props ) => {
const [ value, setValue ] = useState< string | string[] >();
return (
<CustomSelect
{ ...props }
onChange={ ( nextValue ) => {
setValue( nextValue );
props.onChange?.( nextValue );
} }
value={ value }
/>
);
};

export const Default = Template.bind( {} );
Default.args = {
label: 'Label',
children: (
<>
<CustomSelectItem value="Small">
<span style={ { fontSize: '75%' } }>Small</span>
</CustomSelectItem>
<CustomSelectItem value="Something bigger">
<span style={ { fontSize: '200%' } }>Something bigger</span>
</CustomSelectItem>
</>
),
};

/**
* Multiple selection can be enabled by using an array for the `value` and
* `defaultValue` props. The argument of the `onChange` function will also
* change accordingly.
*/
export const MultiSelect = Template.bind( {} );
brookewp marked this conversation as resolved.
Show resolved Hide resolved
MultiSelect.args = {
ciampo marked this conversation as resolved.
Show resolved Hide resolved
defaultValue: [ 'lavender', 'tangerine' ],
label: 'Select Colors',
renderSelectedValue: ( currentValue: string | string[] ) => {
if ( ! Array.isArray( currentValue ) ) {
return currentValue;
}
if ( currentValue.length === 0 ) return 'No colors selected';
if ( currentValue.length === 1 ) return currentValue[ 0 ];
return `${ currentValue.length } colors selected`;
},
children: (
<>
{ [
'amber',
'aquamarine',
'flamingo pink',
'lavender',
'maroon',
'tangerine',
].map( ( item ) => (
<CustomSelectItem key={ item } value={ item }>
{ item }
</CustomSelectItem>
) ) }
</>
),
};

const renderControlledValue = ( gravatar: string | string[] ) => {
const avatar = `https://gravatar.com/avatar?d=${ gravatar }`;
return (
<div style={ { display: 'flex', alignItems: 'center' } }>
<img
style={ { maxHeight: '75px', marginRight: '10px' } }
key={ avatar }
src={ avatar }
alt=""
aria-hidden="true"
/>
<span>{ gravatar }</span>
</div>
);
};

export const Controlled = ControlledTemplate.bind( {} );
Controlled.args = {
label: 'Default Gravatars',
renderSelectedValue: renderControlledValue,
children: (
<>
{ [ 'mystery-person', 'identicon', 'wavatar', 'retro' ].map(
( option ) => (
<CustomSelectItem key={ option } value={ option }>
{ renderControlledValue( option ) }
</CustomSelectItem>
)
) }
</>
),
};
Loading
Loading