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

CustomSelect: Adapt component for legacy props #57902

Merged
merged 52 commits into from
Feb 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
64c2947
Move components to own files
brookewp Dec 12, 2023
4250494
Add legacy adapter
brookewp Jan 18, 2024
d3da53a
Add legacy tests
brookewp Dec 12, 2023
c3d6a80
Update naming and remove unnecessary tests
brookewp Dec 12, 2023
4c9f32f
Add legacy props to adapter
brookewp Jan 16, 2024
7d9bc28
Create new component to forward store
brookewp Jan 17, 2024
d2b30d6
Create legacy component
brookewp Jan 17, 2024
905b9fe
Remove useDeprecatedProps hook and update adapter
brookewp Jan 17, 2024
940cdaf
Separate stories
brookewp Jan 17, 2024
82b33d1
Update stories and types
brookewp Jan 18, 2024
f1b5320
Convert function into variable instead
brookewp Jan 18, 2024
c3f4a23
Update legacy changeObject to match existing properties
brookewp Jan 18, 2024
a98e38c
Add tests for onChange function
brookewp Jan 19, 2024
09e9c47
add rest of legacy props
brookewp Jan 20, 2024
80158a6
Update sizing
brookewp Jan 20, 2024
2ccdc91
Memoize selected render value
brookewp Jan 23, 2024
46283b5
Remove deprecated prop to fix test failures
brookewp Jan 23, 2024
2526b8e
Add `unmountOnHide` and require `defaultValue` as a result
brookewp Jan 23, 2024
a8b4e8a
Update styling for experimental hint
brookewp Jan 24, 2024
bc1cbe7
Connect CustomSelectButton to context system for legacy sizing
brookewp Jan 25, 2024
002bd2e
Update sizing logic and types
brookewp Jan 26, 2024
85993fd
Fix styling to match legacy including hints
brookewp Jan 26, 2024
1524180
Clean up props
brookewp Jan 26, 2024
eb4e946
Omit ‘onChange’ from WordPressComponentProps to prevent conflict
brookewp Jan 29, 2024
fdabba7
Update checkmark icon
brookewp Jan 31, 2024
2bc69ef
Update select arrow
brookewp Jan 31, 2024
63c3dd4
Update types
brookewp Jan 31, 2024
1280774
Add static typing test
brookewp Jan 31, 2024
d25764b
Update story for better manual testing
brookewp Jan 31, 2024
18e15e9
Control mount state for legacy keyboard behaviour
brookewp Jan 31, 2024
e0caf2d
Remove export that is no longer needed
brookewp Jan 31, 2024
cafbdf8
Update legacy onChange type
brookewp Jan 31, 2024
7b86a97
Update tests
brookewp Feb 1, 2024
bc7027e
Update naming
brookewp Feb 1, 2024
4f9f6f9
Try mounting on first render to avoid required defaultValue
brookewp Feb 1, 2024
71b2c1a
Add WordPressComponentProps to default export
brookewp Feb 1, 2024
be17780
Update types
brookewp Feb 2, 2024
287c354
Replace RTL/userEvent with ariakit/test
brookewp Feb 2, 2024
7eb2134
Remove unmountOnHide and related logic for first iteration
brookewp Feb 5, 2024
c794d06
Update docs
brookewp Feb 6, 2024
5c58f78
Update naming
brookewp Feb 6, 2024
c73ce18
Merge new tests and update to ariakit/test
brookewp Feb 6, 2024
795df54
Fix typo in readme
mirka Feb 7, 2024
4171c67
Legacy: Clean up stories
mirka Feb 9, 2024
8d70b4f
Default: Clean up stories
mirka Feb 9, 2024
cb88b8b
Add todo comment about BaseControl
mirka Feb 9, 2024
d2dfe8b
Fix styles
mirka Feb 9, 2024
ddc8e6b
Rename styled components for consistency
mirka Feb 9, 2024
faa6d7b
Fix typo in readme
mirka Feb 9, 2024
797e139
Merge branch 'trunk' into add/legacy-adapter-customselectcontrol
mirka Feb 9, 2024
f8446f8
Rename for clarity
mirka Feb 9, 2024
3a2be41
Update changelog
mirka Feb 9, 2024
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
4 changes: 4 additions & 0 deletions packages/components/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

## Unreleased

### Experimental

- `CustomSelectControlV2`: Adapt component for legacy usage ([#57902](https://github.com/WordPress/gutenberg/pull/57902)).

## 26.0.0 (2024-02-09)

### Breaking Changes
Expand Down
104 changes: 97 additions & 7 deletions packages/components/src/custom-select-control-v2/README.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,93 @@
# CustomSelect

<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.

## Development guidelines

### Usage

#### Uncontrolled Mode

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.

```jsx
const UncontrolledCustomSelect = () => (
<CustomSelect label="Colors">
<CustomSelectItem value="Blue">
{ /* The `defaultValue` since it wasn't defined */ }
<span style={ { color: 'blue' } }>Blue</span>
</CustomSelectItem>
<CustomSelectItem value="Purple">
<span style={ { color: 'purple' } }>Purple</span>
</CustomSelectItem>
<CustomSelectItem value="Pink">
<span style={ { color: 'deeppink' } }>Pink</span>
</CustomSelectItem>
</CustomSelect>
);
```

#### Controlled Mode

CustomSelect can also be used in a controlled mode, where the parent component specifies the `value` and the `onChange` props to control selection.

```jsx
const ControlledCustomSelect = () => {
const [ value, setValue ] = useState< string | string[] >();

const renderControlledValue = ( renderValue: string | string[] ) => (
<>
{ /* Custom JSX to display `renderValue` item */ }
</>
);

return (
<CustomSelect
{ ...props }
onChange={ ( nextValue ) => {
setValue( nextValue );
props.onChange?.( nextValue );
} }
value={ value }
>
{ [ 'blue', 'purple', 'pink' ].map( ( option ) => (
<CustomSelectItem key={ option } value={ option }>
{ renderControlledValue( option ) }
</CustomSelectItem>
) ) }
</CustomSelect>
);
};
```

#### Multiple Selection

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.

```jsx
const MultiSelectCustomSelect = () => (
<CustomSelect defaultValue={ [ 'blue', 'pink' ] } label="Colors">
{ [ 'blue', 'purple', 'pink' ].map( ( item ) => (
<CustomSelectItem key={ item } value={ item }>
{ item }
</CustomSelectItem>
) ) }
</CustomSelect>
);
```

### Components and Sub-components

CustomSelect is comprised of two individual components:

- `CustomSelect`: a wrapper component and context provider. It is responsible for managing the state of the `CustomSelectItem` children.
- `CustomSelectItem`: renders a single select item. The first `CustomSelectItem` child will be used as the `defaultValue` when `defaultValue` is undefined.

#### Props

The component accepts the following props:
Expand All @@ -16,37 +98,45 @@ The child elements. This should be composed of CustomSelect.Item components.

- Required: yes

##### `defaultValue`: `string`
##### `defaultValue`: `string | string[]`

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

- Required: no

##### `hideLabelFromVision`: `boolean`

Used to visually hide the label. It will always be visible to screen readers.

- Required: no
- Default: `false`

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

Label for the control.

- Required: yes

##### `onChange`: `( newValue: string ) => void`
##### `onChange`: `( newValue: string | string[] ) => void`

A function that receives the new value of the input.

- Required: no

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

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

- Required: no

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

The size of the control.

- Required: no
- Default: `'default'`

##### `value`: `string`
##### `value`: `string | string[]`

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/**
* WordPress dependencies
*/
import { useContext } from '@wordpress/element';
import { Icon, check } from '@wordpress/icons';
/**
* Internal dependencies
*/
import type { CustomSelectItemProps } from './types';
import type { WordPressComponentProps } from '../context';
import * as Styled from './styles';
import { CustomSelectContext } from './custom-select';

export function CustomSelectItem( {
children,
...props
}: WordPressComponentProps< CustomSelectItemProps, 'div', false > ) {
const customSelectContext = useContext( CustomSelectContext );
return (
<Styled.SelectItem store={ customSelectContext?.store } { ...props }>
{ children ?? props.value }
<Styled.SelectedItemCheck>
<Icon icon={ check } />
</Styled.SelectedItemCheck>
</Styled.SelectItem>
);
}

export default CustomSelectItem;
122 changes: 122 additions & 0 deletions packages/components/src/custom-select-control-v2/custom-select.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
/**
* WordPress dependencies
*/
import { createContext, useMemo } from '@wordpress/element';
import { __, sprintf } from '@wordpress/i18n';
import { Icon, chevronDown } from '@wordpress/icons';

/**
* Internal dependencies
*/
import { VisuallyHidden } from '..';
import * as Styled from './styles';
import type {
CustomSelectContext as CustomSelectContextType,
CustomSelectStore,
CustomSelectButtonProps,
_CustomSelectProps,
} from './types';
import {
contextConnectWithoutRef,
useContextSystem,
type WordPressComponentProps,
} from '../context';

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

function defaultRenderSelectedValue(
value: CustomSelectButtonProps[ '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;
}

const UnconnectedCustomSelectButton = (
props: Omit<
WordPressComponentProps<
CustomSelectButtonProps & CustomSelectStore,
'button',
false
>,
'onChange'
>
) => {
const {
renderSelectedValue,
size = 'default',
store,
...restProps
} = useContextSystem( props, 'CustomSelectControlButton' );

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

const computedRenderSelectedValue = useMemo(
() => renderSelectedValue ?? defaultRenderSelectedValue,
[ renderSelectedValue ]
);

return (
<Styled.Select
{ ...restProps }
size={ size }
hasCustomRenderProp={ !! renderSelectedValue }
store={ store }
// to match legacy behavior where using arrow keys
// move selection rather than open the popover
showOnKeyDown={ false }
>
<div>{ computedRenderSelectedValue( currentValue ) }</div>
<Icon icon={ chevronDown } size={ 18 } />
</Styled.Select>
);
};

const CustomSelectButton = contextConnectWithoutRef(
UnconnectedCustomSelectButton,
'CustomSelectControlButton'
);

function _CustomSelect( props: _CustomSelectProps & CustomSelectStore ) {
const {
children,
hideLabelFromVision = false,
label,
store,
...restProps
} = props;

return (
<>
{ hideLabelFromVision ? ( // TODO: Replace with BaseControl
<VisuallyHidden as="label">{ label }</VisuallyHidden>
) : (
<Styled.SelectLabel store={ store }>
{ label }
</Styled.SelectLabel>
) }
<CustomSelectButton { ...restProps } store={ store } />
<Styled.SelectPopover gutter={ 12 } store={ store } sameWidth>
<CustomSelectContext.Provider value={ { store } }>
{ children }
</CustomSelectContext.Provider>
</Styled.SelectPopover>
</>
);
}

export default _CustomSelect;
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/**
* External dependencies
*/
// eslint-disable-next-line no-restricted-imports
import * as Ariakit from '@ariakit/react';
/**
* Internal dependencies
*/
import _CustomSelect from '../custom-select';
import type { CustomSelectProps } from '../types';

function CustomSelect( props: CustomSelectProps ) {
const { defaultValue, onChange, value, ...restProps } = props;
// Forward props + store from v2 implementation
const store = Ariakit.useSelectStore( {
setValue: ( nextValue ) => onChange?.( nextValue ),
defaultValue,
value,
} );

return <_CustomSelect { ...restProps } store={ store } />;
}

export default CustomSelect;
Loading
Loading