-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Try Ariakit Select for new CustomSelectControl component (#55790)
* Create base component using ariakit * Simplify story example * Add options for children and value with stories * Update types * Add uncontrolled story and adapt value * Remove duplicate import * Add size prop * Changes from pairing session with ciampo * Rename and cleanup * Update styles * Add context * Add README * Update manifest * Update types and move context provider * Add back types for multi-selection * Remove file for moved component * Add translation for placeholder * Add check and cleanup styles * Add multiselect example * Require type children for CustomSelectItem * Add small size and update sizing logic * Update stories with ciampo’s suggested changes * Update styles to match legacy CustomSelectControl * Cleanup based on PR feedback * Children as optional * Incorporate ciampo’s suggestions to refine styles * Make value required for CustomSelectItem and fix naming in comment * Update changelog * Refine styles and add styling to active item * Only render label if defined * Cleanup story * Add suggestions to WIP README and types * Make label required * Improve logic for default rendered value * Update stories
- Loading branch information
Showing
7 changed files
with
470 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
73 changes: 73 additions & 0 deletions
73
packages/components/src/custom-select-control-v2/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
##### `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` | ||
|
||
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
99
packages/components/src/custom-select-control-v2/index.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 ) { | ||
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 }> | ||
{ label } | ||
</Styled.CustomSelectLabel> | ||
<Styled.CustomSelectButton | ||
size={ size } | ||
hasCustomRenderProp={ !! renderSelectedValue } | ||
store={ store } | ||
> | ||
{ renderSelectedValue( currentValue ) } | ||
<Ariakit.SelectArrow /> | ||
</Styled.CustomSelectButton> | ||
<Styled.CustomSelectPopover gutter={ 12 } store={ store } sameWidth> | ||
<CustomSelectContext.Provider value={ { store } }> | ||
{ children } | ||
</CustomSelectContext.Provider> | ||
</Styled.CustomSelectPopover> | ||
</> | ||
); | ||
} | ||
|
||
export function CustomSelectItem( { | ||
children, | ||
...props | ||
}: CustomSelectItemProps ) { | ||
const customSelectContext = useContext( CustomSelectContext ); | ||
return ( | ||
<Styled.CustomSelectItem | ||
store={ customSelectContext?.store } | ||
{ ...props } | ||
> | ||
{ children ?? props.value } | ||
<Ariakit.SelectItemCheck /> | ||
</Styled.CustomSelectItem> | ||
); | ||
} |
149 changes: 149 additions & 0 deletions
149
packages/components/src/custom-select-control-v2/stories/index.story.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 } }, | ||
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( {} ); | ||
MultiSelect.args = { | ||
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> | ||
) | ||
) } | ||
</> | ||
), | ||
}; |
Oops, something went wrong.