Skip to content

Commit 5939c41

Browse files
brookewpbrookewpmirkaciampodiegohaz
authored
CustomSelect: Adapt component for legacy props (WordPress#57902)
* Move components to own files * Add legacy adapter * Add legacy tests * Update naming and remove unnecessary tests * Add legacy props to adapter * Create new component to forward store * Create legacy component * Remove useDeprecatedProps hook and update adapter * Separate stories * Update stories and types * Convert function into variable instead * Update legacy changeObject to match existing properties * Add tests for onChange function * add rest of legacy props * Update sizing * Memoize selected render value * Remove deprecated prop to fix test failures * Add `unmountOnHide` and require `defaultValue` as a result See: ariakit/ariakit#3374 (comment) * Update styling for experimental hint * Connect CustomSelectButton to context system for legacy sizing * Update sizing logic and types * Fix styling to match legacy including hints * Clean up props * Omit ‘onChange’ from WordPressComponentProps to prevent conflict * Update checkmark icon * Update select arrow * Update types * Add static typing test * Update story for better manual testing * Control mount state for legacy keyboard behaviour * Remove export that is no longer needed * Update legacy onChange type * Update tests * Update naming * Try mounting on first render to avoid required defaultValue * Add WordPressComponentProps to default export * Update types * Replace RTL/userEvent with ariakit/test * Remove unmountOnHide and related logic for first iteration * Update docs * Update naming * Merge new tests and update to ariakit/test * Fix typo in readme * Legacy: Clean up stories * Default: Clean up stories * Add todo comment about BaseControl * Fix styles * Rename styled components for consistency * Fix typo in readme * Rename for clarity * Update changelog --------- Co-authored-by: brookewp <[email protected]> Co-authored-by: mirka <[email protected]> Co-authored-by: ciampo <[email protected]> Co-authored-by: diegohaz <[email protected]>
1 parent 19622fa commit 5939c41

File tree

13 files changed

+1568
-347
lines changed

13 files changed

+1568
-347
lines changed

packages/components/CHANGELOG.md

+4
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Experimental
6+
7+
- `CustomSelectControlV2`: Adapt component for legacy usage ([#57902](https://github.com/WordPress/gutenberg/pull/57902)).
8+
59
## 26.0.0 (2024-02-09)
610

711
### Breaking Changes

packages/components/src/custom-select-control-v2/README.md

+97-7
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,93 @@
1+
# CustomSelect
2+
13
<div class="callout callout-alert">
24
This feature is still experimental. “Experimental” means this is an early implementation subject to drastic and breaking changes.
35
</div>
46

5-
### `CustomSelect`
6-
77
Used to render a customizable select control component.
88

9+
## Development guidelines
10+
11+
### Usage
12+
13+
#### Uncontrolled Mode
14+
15+
CustomSelect can be used in an uncontrolled mode, where the component manages its own state. In this mode, the `defaultValue` prop can be used to set the initial selected value. If this prop is not set, the first value from the children will be selected by default.
16+
17+
```jsx
18+
const UncontrolledCustomSelect = () => (
19+
<CustomSelect label="Colors">
20+
<CustomSelectItem value="Blue">
21+
{ /* The `defaultValue` since it wasn't defined */ }
22+
<span style={ { color: 'blue' } }>Blue</span>
23+
</CustomSelectItem>
24+
<CustomSelectItem value="Purple">
25+
<span style={ { color: 'purple' } }>Purple</span>
26+
</CustomSelectItem>
27+
<CustomSelectItem value="Pink">
28+
<span style={ { color: 'deeppink' } }>Pink</span>
29+
</CustomSelectItem>
30+
</CustomSelect>
31+
);
32+
```
33+
34+
#### Controlled Mode
35+
36+
CustomSelect can also be used in a controlled mode, where the parent component specifies the `value` and the `onChange` props to control selection.
37+
38+
```jsx
39+
const ControlledCustomSelect = () => {
40+
const [ value, setValue ] = useState< string | string[] >();
41+
42+
const renderControlledValue = ( renderValue: string | string[] ) => (
43+
<>
44+
{ /* Custom JSX to display `renderValue` item */ }
45+
</>
46+
);
47+
48+
return (
49+
<CustomSelect
50+
{ ...props }
51+
onChange={ ( nextValue ) => {
52+
setValue( nextValue );
53+
props.onChange?.( nextValue );
54+
} }
55+
value={ value }
56+
>
57+
{ [ 'blue', 'purple', 'pink' ].map( ( option ) => (
58+
<CustomSelectItem key={ option } value={ option }>
59+
{ renderControlledValue( option ) }
60+
</CustomSelectItem>
61+
) ) }
62+
</CustomSelect>
63+
);
64+
};
65+
```
66+
67+
#### Multiple Selection
68+
69+
Multiple selection can be enabled by using an array for the `value` and
70+
`defaultValue` props. The argument of the `onChange` function will also change accordingly.
71+
72+
```jsx
73+
const MultiSelectCustomSelect = () => (
74+
<CustomSelect defaultValue={ [ 'blue', 'pink' ] } label="Colors">
75+
{ [ 'blue', 'purple', 'pink' ].map( ( item ) => (
76+
<CustomSelectItem key={ item } value={ item }>
77+
{ item }
78+
</CustomSelectItem>
79+
) ) }
80+
</CustomSelect>
81+
);
82+
```
83+
84+
### Components and Sub-components
85+
86+
CustomSelect is comprised of two individual components:
87+
88+
- `CustomSelect`: a wrapper component and context provider. It is responsible for managing the state of the `CustomSelectItem` children.
89+
- `CustomSelectItem`: renders a single select item. The first `CustomSelectItem` child will be used as the `defaultValue` when `defaultValue` is undefined.
90+
991
#### Props
1092
1193
The component accepts the following props:
@@ -16,37 +98,45 @@ The child elements. This should be composed of CustomSelect.Item components.
1698
1799
- Required: yes
18100
19-
##### `defaultValue`: `string`
101+
##### `defaultValue`: `string | string[]`
20102
21103
An optional default value for the control. If left `undefined`, the first non-disabled item will be used.
22104
23105
- Required: no
24106
107+
##### `hideLabelFromVision`: `boolean`
108+
109+
Used to visually hide the label. It will always be visible to screen readers.
110+
111+
- Required: no
112+
- Default: `false`
113+
25114
##### `label`: `string`
26115
27116
Label for the control.
28117
29118
- Required: yes
30119
31-
##### `onChange`: `( newValue: string ) => void`
120+
##### `onChange`: `( newValue: string | string[] ) => void`
32121
33122
A function that receives the new value of the input.
34123
35124
- Required: no
36125
37-
##### `renderSelectedValue`: `( selectValue: string ) => React.ReactNode`
126+
##### `renderSelectedValue`: `( selectValue: string | string[] ) => React.ReactNode`
38127
39128
Can be used to render select UI with custom styled values.
40129
41130
- Required: no
42131
43-
##### `size`: `'default' | 'large'`
132+
##### `size`: `'default' | 'compact'`
44133
45134
The size of the control.
46135
47136
- Required: no
137+
- Default: `'default'`
48138
49-
##### `value`: `string`
139+
##### `value`: `string | string[]`
50140
51141
Can be used to externally control the value of the control.
52142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { useContext } from '@wordpress/element';
5+
import { Icon, check } from '@wordpress/icons';
6+
/**
7+
* Internal dependencies
8+
*/
9+
import type { CustomSelectItemProps } from './types';
10+
import type { WordPressComponentProps } from '../context';
11+
import * as Styled from './styles';
12+
import { CustomSelectContext } from './custom-select';
13+
14+
export function CustomSelectItem( {
15+
children,
16+
...props
17+
}: WordPressComponentProps< CustomSelectItemProps, 'div', false > ) {
18+
const customSelectContext = useContext( CustomSelectContext );
19+
return (
20+
<Styled.SelectItem store={ customSelectContext?.store } { ...props }>
21+
{ children ?? props.value }
22+
<Styled.SelectedItemCheck>
23+
<Icon icon={ check } />
24+
</Styled.SelectedItemCheck>
25+
</Styled.SelectItem>
26+
);
27+
}
28+
29+
export default CustomSelectItem;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* WordPress dependencies
3+
*/
4+
import { createContext, useMemo } from '@wordpress/element';
5+
import { __, sprintf } from '@wordpress/i18n';
6+
import { Icon, chevronDown } from '@wordpress/icons';
7+
8+
/**
9+
* Internal dependencies
10+
*/
11+
import { VisuallyHidden } from '..';
12+
import * as Styled from './styles';
13+
import type {
14+
CustomSelectContext as CustomSelectContextType,
15+
CustomSelectStore,
16+
CustomSelectButtonProps,
17+
_CustomSelectProps,
18+
} from './types';
19+
import {
20+
contextConnectWithoutRef,
21+
useContextSystem,
22+
type WordPressComponentProps,
23+
} from '../context';
24+
25+
export const CustomSelectContext =
26+
createContext< CustomSelectContextType >( undefined );
27+
28+
function defaultRenderSelectedValue(
29+
value: CustomSelectButtonProps[ 'value' ]
30+
) {
31+
const isValueEmpty = Array.isArray( value )
32+
? value.length === 0
33+
: value === undefined || value === null;
34+
35+
if ( isValueEmpty ) {
36+
return __( 'Select an item' );
37+
}
38+
39+
if ( Array.isArray( value ) ) {
40+
return value.length === 1
41+
? value[ 0 ]
42+
: // translators: %s: number of items selected (it will always be 2 or more items)
43+
sprintf( __( '%s items selected' ), value.length );
44+
}
45+
46+
return value;
47+
}
48+
49+
const UnconnectedCustomSelectButton = (
50+
props: Omit<
51+
WordPressComponentProps<
52+
CustomSelectButtonProps & CustomSelectStore,
53+
'button',
54+
false
55+
>,
56+
'onChange'
57+
>
58+
) => {
59+
const {
60+
renderSelectedValue,
61+
size = 'default',
62+
store,
63+
...restProps
64+
} = useContextSystem( props, 'CustomSelectControlButton' );
65+
66+
const { value: currentValue } = store.useState();
67+
68+
const computedRenderSelectedValue = useMemo(
69+
() => renderSelectedValue ?? defaultRenderSelectedValue,
70+
[ renderSelectedValue ]
71+
);
72+
73+
return (
74+
<Styled.Select
75+
{ ...restProps }
76+
size={ size }
77+
hasCustomRenderProp={ !! renderSelectedValue }
78+
store={ store }
79+
// to match legacy behavior where using arrow keys
80+
// move selection rather than open the popover
81+
showOnKeyDown={ false }
82+
>
83+
<div>{ computedRenderSelectedValue( currentValue ) }</div>
84+
<Icon icon={ chevronDown } size={ 18 } />
85+
</Styled.Select>
86+
);
87+
};
88+
89+
const CustomSelectButton = contextConnectWithoutRef(
90+
UnconnectedCustomSelectButton,
91+
'CustomSelectControlButton'
92+
);
93+
94+
function _CustomSelect( props: _CustomSelectProps & CustomSelectStore ) {
95+
const {
96+
children,
97+
hideLabelFromVision = false,
98+
label,
99+
store,
100+
...restProps
101+
} = props;
102+
103+
return (
104+
<>
105+
{ hideLabelFromVision ? ( // TODO: Replace with BaseControl
106+
<VisuallyHidden as="label">{ label }</VisuallyHidden>
107+
) : (
108+
<Styled.SelectLabel store={ store }>
109+
{ label }
110+
</Styled.SelectLabel>
111+
) }
112+
<CustomSelectButton { ...restProps } store={ store } />
113+
<Styled.SelectPopover gutter={ 12 } store={ store } sameWidth>
114+
<CustomSelectContext.Provider value={ { store } }>
115+
{ children }
116+
</CustomSelectContext.Provider>
117+
</Styled.SelectPopover>
118+
</>
119+
);
120+
}
121+
122+
export default _CustomSelect;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/**
2+
* External dependencies
3+
*/
4+
// eslint-disable-next-line no-restricted-imports
5+
import * as Ariakit from '@ariakit/react';
6+
/**
7+
* Internal dependencies
8+
*/
9+
import _CustomSelect from '../custom-select';
10+
import type { CustomSelectProps } from '../types';
11+
12+
function CustomSelect( props: CustomSelectProps ) {
13+
const { defaultValue, onChange, value, ...restProps } = props;
14+
// Forward props + store from v2 implementation
15+
const store = Ariakit.useSelectStore( {
16+
setValue: ( nextValue ) => onChange?.( nextValue ),
17+
defaultValue,
18+
value,
19+
} );
20+
21+
return <_CustomSelect { ...restProps } store={ store } />;
22+
}
23+
24+
export default CustomSelect;

0 commit comments

Comments
 (0)