Add SearchControl component (#34159)
* Add initial SearchControl component * Add async example * Reorganize getItemPropsType type * Create separate MenuItem component * Update items to use value/label pairs * Add fuzzy matching example * Use MenuItem component in example * Add callback method example and onSelect prop * Add custom render example * Add styling * Simplify Menu component * Add changelog entry * Update SelectControl to DeprecatedSelectControl * Rename SearchControl to SelectControl * Add readme * Add placeholder prop * Add icon to combox box * Rename deprecated SelectControl classes * Add changelog entries * Pass menu props to menu to fix ref issues * Update lock file * Rebase and update lock file * Fix up IDs in e2e tests * Make list structure more semantic * Fix update conflict with pnpm-lock * Move new SelectControl to experimental * Change experimental class name to avoid style conflicts * Fix up latest lock file from rebase * Remove onboarding e2e changes * Update changelogs * Update lock file again * Update pnpm-lock and fix lint error Co-authored-by: Lourens Schep <lourensschep@gmail.com>
This commit is contained in:
parent
e9d27c8949
commit
32de8bee6f
|
@ -0,0 +1,4 @@
|
||||||
|
Significance: major
|
||||||
|
Type: add
|
||||||
|
|
||||||
|
Create new experimental SelectControl component
|
|
@ -59,6 +59,7 @@
|
||||||
"d3-shape": "^1.3.7",
|
"d3-shape": "^1.3.7",
|
||||||
"d3-time-format": "^2.3.0",
|
"d3-time-format": "^2.3.0",
|
||||||
"dompurify": "^2.3.6",
|
"dompurify": "^2.3.6",
|
||||||
|
"downshift": "^6.1.9",
|
||||||
"emoji-flags": "^1.3.0",
|
"emoji-flags": "^1.3.0",
|
||||||
"gridicons": "^3.4.0",
|
"gridicons": "^3.4.0",
|
||||||
"memoize-one": "^6.0.0",
|
"memoize-one": "^6.0.0",
|
||||||
|
|
|
@ -0,0 +1,74 @@
|
||||||
|
SelectControl
|
||||||
|
===
|
||||||
|
|
||||||
|
A component that allows searching and selection of one or more items, providing accessibility for item and menu interaction.
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
`SelectControl` expects an array of item objects with `value` and `label` properties by default. However, using the `itemToString` prop will allow you to pass a function to determine the label used and customize this object's shape.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
const [ selected, setSelected ] = useState< SelectedType >( [] );
|
||||||
|
|
||||||
|
const items = [
|
||||||
|
{ value: 'item-1', label: 'Item 1' },
|
||||||
|
{ value: 'item-2', label: 'Item 2' },
|
||||||
|
];
|
||||||
|
|
||||||
|
<SelectControl
|
||||||
|
multiple
|
||||||
|
items={ items }
|
||||||
|
label="My select control"
|
||||||
|
selected={ selected }
|
||||||
|
onSelect={ ( item ) => item && setSelected( [ ...selected, item ] ) }
|
||||||
|
onRemove={ () => setSelected( selected.filter( ( i ) => i !== item ) ) }
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
By default, the menu will render selectable items based on the provided items, but by passing a child function you can determine the render of those items.
|
||||||
|
|
||||||
|
```jsx
|
||||||
|
<SelectControl
|
||||||
|
label="Custom render"
|
||||||
|
items={ items }
|
||||||
|
selected={ selected }
|
||||||
|
onSelect={ ( item ) => setSelectedItem( item ) }
|
||||||
|
onRemove={ () => setSelectedItem( null ) }
|
||||||
|
>
|
||||||
|
{ ( {
|
||||||
|
items,
|
||||||
|
isOpen,
|
||||||
|
highlightedIndex,
|
||||||
|
getItemProps,
|
||||||
|
getMenuProps,
|
||||||
|
} ) => {
|
||||||
|
return (
|
||||||
|
<ul { ...getMenuProps() }>
|
||||||
|
{ isOpen && items.map( ( item, index: number ) => (
|
||||||
|
<li
|
||||||
|
key={ `${ item.value }${ index }` }
|
||||||
|
{ ...getItemProps() }
|
||||||
|
>
|
||||||
|
{ item.label }
|
||||||
|
</li>
|
||||||
|
) ) }
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
} }
|
||||||
|
</SelectControl>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Props
|
||||||
|
|
||||||
|
Name | Type | Default | Description
|
||||||
|
--- | --- | --- | ---
|
||||||
|
`children` | Function | `( { items, highlightedIndex, getItemProps, getMenuProps, isOpen } ) => JSX.Element` | A function that renders the menu and menu items
|
||||||
|
`multiple` | Boolean | `false` | Whether the input should allow multiple selections or a single selection
|
||||||
|
`items` | Array | `undefined` | The items used in the dropdown as an array of objects with `value` and `label` properties
|
||||||
|
`label` | String | `undefined` | A string shown above the input
|
||||||
|
`itemToString` | Function | `( item ) => item.label` | A function used to determine how a selected item should be shown
|
||||||
|
`getFilteredItems` | Function | `( allItems, inputValue, selectedItems ) => allItems.filter( ( item ) => selectedItems.indexOf( item ) < 0 && item.label.toLowerCase().startsWith( inputValue.toLowerCase() ) )` | A function to determine how items should be filtered based on user input and previously selected items
|
||||||
|
`onInputChange` | Function | `() => null` | A callback that fires when the user input has changed
|
||||||
|
`onRemove` | Function | `() => null` | A callback that fires when a selected item has been removed
|
||||||
|
`onSelect` | Function | `() => null` | A callback that fires when an item has been selected
|
||||||
|
`selected` | Array or Item | `undefined` | An array of selected items or a single selected item
|
|
@ -0,0 +1,15 @@
|
||||||
|
.woocommerce-experimental-select-control__combox-box {
|
||||||
|
flex-grow: 1;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: $gap-smaller 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-experimental-select-control__combox-box-icon {
|
||||||
|
padding-left: 10px;
|
||||||
|
margin-right: -2px;
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createElement } from 'react';
|
||||||
|
import { Icon, search } from '@wordpress/icons';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { Props } from './types';
|
||||||
|
|
||||||
|
type ComboBoxProps = {
|
||||||
|
comboBoxProps: Props;
|
||||||
|
inputProps: Props;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ComboBox = ( { comboBoxProps, inputProps }: ComboBoxProps ) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{ ...comboBoxProps }
|
||||||
|
className="woocommerce-experimental-select-control__combox-box"
|
||||||
|
>
|
||||||
|
<input { ...inputProps } />
|
||||||
|
<Icon
|
||||||
|
className="woocommerce-experimental-select-control__combox-box-icon"
|
||||||
|
icon={ search }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
export * from './select-control';
|
|
@ -0,0 +1,5 @@
|
||||||
|
.woocommerce-experimental-select-control__menu-item {
|
||||||
|
padding: $gap-small;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createElement, ReactElement } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { ItemType, getItemPropsType } from './types';
|
||||||
|
|
||||||
|
type MenuItemProps = {
|
||||||
|
index: number;
|
||||||
|
isActive: boolean;
|
||||||
|
item: ItemType;
|
||||||
|
children: ReactElement | string;
|
||||||
|
getItemProps: getItemPropsType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MenuItem = ( {
|
||||||
|
children,
|
||||||
|
getItemProps,
|
||||||
|
index,
|
||||||
|
isActive,
|
||||||
|
item,
|
||||||
|
}: MenuItemProps ) => {
|
||||||
|
return (
|
||||||
|
<li
|
||||||
|
style={ isActive ? { backgroundColor: '#bde4ff' } : {} }
|
||||||
|
{ ...getItemProps( { item, index } ) }
|
||||||
|
className="woocommerce-experimental-select-control__menu-item"
|
||||||
|
>
|
||||||
|
{ children }
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,17 @@
|
||||||
|
.woocommerce-experimental-select-control__menu {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
top: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
display: none;
|
||||||
|
|
||||||
|
&.is-open {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-experimental-select-control__menu-inner {
|
||||||
|
background: $studio-white;
|
||||||
|
border: 1px solid $studio-gray-5;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { createElement, ReactElement } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { getMenuPropsType } from './types';
|
||||||
|
|
||||||
|
type MenuProps = {
|
||||||
|
children?: JSX.Element | JSX.Element[];
|
||||||
|
getMenuProps: getMenuPropsType;
|
||||||
|
isOpen: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Menu = ( { children, getMenuProps, isOpen }: MenuProps ) => {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
{ ...getMenuProps() }
|
||||||
|
className={ classnames(
|
||||||
|
'woocommerce-experimental-select-control__menu',
|
||||||
|
{
|
||||||
|
'is-open': isOpen,
|
||||||
|
}
|
||||||
|
) }
|
||||||
|
>
|
||||||
|
{ isOpen &&
|
||||||
|
( ! Array.isArray( children ) || !! children.length ) && (
|
||||||
|
<ul className="woocommerce-experimental-select-control__menu-inner">
|
||||||
|
{ children }
|
||||||
|
</ul>
|
||||||
|
) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,32 @@
|
||||||
|
@import './combo-box.scss';
|
||||||
|
@import './menu.scss';
|
||||||
|
@import './menu-item.scss';
|
||||||
|
|
||||||
|
.woocommerce-experimental-select-control {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.woocommerce-experimental-select-control__combo-box-wrapper {
|
||||||
|
border: 1px solid $studio-gray-20;
|
||||||
|
border-radius: 3px;
|
||||||
|
background: $studio-white;
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: $gap-small;
|
||||||
|
padding-right: $gap-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-focused .woocommerce-experimental-select-control__combo-box-wrapper {
|
||||||
|
box-shadow: 0 0 0 1px var(--wp-admin-theme-color);
|
||||||
|
border-color: var(--wp-admin-theme-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.woocommerce-experimental-select-control__input {
|
||||||
|
border: 0;
|
||||||
|
box-shadow: none;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,163 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import classnames from 'classnames';
|
||||||
|
import { createElement } from 'react';
|
||||||
|
import { useCombobox, useMultipleSelection } from 'downshift';
|
||||||
|
import { useState, Fragment } from '@wordpress/element';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { ChildrenType, ItemType } from './types';
|
||||||
|
import { SelectedItems } from './selected-items';
|
||||||
|
import { ComboBox } from './combo-box';
|
||||||
|
import { Menu } from './menu';
|
||||||
|
import { MenuItem } from './menu-item';
|
||||||
|
import {
|
||||||
|
itemToString as defaultItemToString,
|
||||||
|
getFilteredItems as defaultGetFilteredItems,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
type SelectControlProps = {
|
||||||
|
children?: ChildrenType;
|
||||||
|
items: ItemType[];
|
||||||
|
label: string;
|
||||||
|
initialSelectedItems?: ItemType[];
|
||||||
|
itemToString?: ( item: ItemType | null ) => string;
|
||||||
|
getFilteredItems?: (
|
||||||
|
allItems: ItemType[],
|
||||||
|
inputValue: string,
|
||||||
|
selectedItems: ItemType[]
|
||||||
|
) => ItemType[];
|
||||||
|
multiple?: boolean;
|
||||||
|
onInputChange?: ( value: string | undefined ) => void;
|
||||||
|
onRemove?: ( item: ItemType ) => void;
|
||||||
|
onSelect?: ( selected: ItemType ) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
selected: ItemType | ItemType[] | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectControl = ( {
|
||||||
|
children = ( {
|
||||||
|
items,
|
||||||
|
highlightedIndex,
|
||||||
|
getItemProps,
|
||||||
|
getMenuProps,
|
||||||
|
isOpen,
|
||||||
|
} ) => {
|
||||||
|
return (
|
||||||
|
<Menu getMenuProps={ getMenuProps } isOpen={ isOpen }>
|
||||||
|
{ items.map( ( item, index: number ) => (
|
||||||
|
<MenuItem
|
||||||
|
key={ `${ item.value }${ index }` }
|
||||||
|
index={ index }
|
||||||
|
isActive={ highlightedIndex === index }
|
||||||
|
item={ item }
|
||||||
|
getItemProps={ getItemProps }
|
||||||
|
>
|
||||||
|
{ item.label }
|
||||||
|
</MenuItem>
|
||||||
|
) ) }
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
multiple = false,
|
||||||
|
items,
|
||||||
|
label,
|
||||||
|
itemToString = defaultItemToString,
|
||||||
|
getFilteredItems = defaultGetFilteredItems,
|
||||||
|
onInputChange = () => null,
|
||||||
|
onRemove = () => null,
|
||||||
|
onSelect = () => null,
|
||||||
|
placeholder,
|
||||||
|
selected,
|
||||||
|
}: SelectControlProps ) => {
|
||||||
|
const [ isFocused, setIsFocused ] = useState( false );
|
||||||
|
const [ inputValue, setInputValue ] = useState( '' );
|
||||||
|
const { getSelectedItemProps, getDropdownProps } = useMultipleSelection();
|
||||||
|
let selectedItems = selected === null ? [] : selected;
|
||||||
|
selectedItems = Array.isArray( selectedItems )
|
||||||
|
? selectedItems
|
||||||
|
: [ selectedItems ].filter( Boolean );
|
||||||
|
const filteredItems = getFilteredItems( items, inputValue, selectedItems );
|
||||||
|
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
getLabelProps,
|
||||||
|
getMenuProps,
|
||||||
|
getInputProps,
|
||||||
|
getComboboxProps,
|
||||||
|
highlightedIndex,
|
||||||
|
getItemProps,
|
||||||
|
} = useCombobox( {
|
||||||
|
inputValue,
|
||||||
|
items: filteredItems,
|
||||||
|
itemToString,
|
||||||
|
selectedItem: null,
|
||||||
|
onStateChange: ( { inputValue: value, type, selectedItem } ) => {
|
||||||
|
switch ( type ) {
|
||||||
|
case useCombobox.stateChangeTypes.InputChange:
|
||||||
|
onInputChange( value );
|
||||||
|
setInputValue( value || '' );
|
||||||
|
|
||||||
|
break;
|
||||||
|
case useCombobox.stateChangeTypes.InputKeyDownEnter:
|
||||||
|
case useCombobox.stateChangeTypes.ItemClick:
|
||||||
|
case useCombobox.stateChangeTypes.InputBlur:
|
||||||
|
if ( selectedItem ) {
|
||||||
|
onSelect( selectedItem );
|
||||||
|
setInputValue(
|
||||||
|
multiple ? '' : itemToString( selectedItem )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
} );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={ classnames( 'woocommerce-experimental-select-control', {
|
||||||
|
'is-focused': isFocused,
|
||||||
|
} ) }
|
||||||
|
>
|
||||||
|
{ /* Downshift's getLabelProps handles the necessary label attributes. */ }
|
||||||
|
{ /* eslint-disable jsx-a11y/label-has-for */ }
|
||||||
|
<label { ...getLabelProps() }>{ label }</label>
|
||||||
|
{ /* eslint-enable jsx-a11y/label-has-for */ }
|
||||||
|
<div className="woocommerce-experimental-select-control__combo-box-wrapper">
|
||||||
|
{ multiple && (
|
||||||
|
<SelectedItems
|
||||||
|
items={ selectedItems }
|
||||||
|
itemToString={ itemToString }
|
||||||
|
getSelectedItemProps={ getSelectedItemProps }
|
||||||
|
onRemove={ onRemove }
|
||||||
|
/>
|
||||||
|
) }
|
||||||
|
<ComboBox
|
||||||
|
comboBoxProps={ getComboboxProps() }
|
||||||
|
inputProps={ getInputProps( {
|
||||||
|
...getDropdownProps( { preventKeyAction: isOpen } ),
|
||||||
|
className:
|
||||||
|
'woocommerce-experimental-select-control__input',
|
||||||
|
onFocus: () => setIsFocused( true ),
|
||||||
|
onBlur: () => setIsFocused( false ),
|
||||||
|
placeholder,
|
||||||
|
} ) }
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{ children( {
|
||||||
|
items: filteredItems,
|
||||||
|
highlightedIndex,
|
||||||
|
getItemProps,
|
||||||
|
getMenuProps,
|
||||||
|
isOpen,
|
||||||
|
} ) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,51 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { createElement } from 'react';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { ItemType } from './types';
|
||||||
|
import Tag from '../tag';
|
||||||
|
|
||||||
|
type SelectedItemsProps = {
|
||||||
|
items: ItemType[];
|
||||||
|
itemToString: ( item: ItemType | null ) => string;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore These are the types provided by Downshift.
|
||||||
|
getSelectedItemProps: ( { selectedItem: any, index: any } ) => {
|
||||||
|
[ key: string ]: string;
|
||||||
|
};
|
||||||
|
onRemove: ( item: ItemType ) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectedItems = ( {
|
||||||
|
items,
|
||||||
|
itemToString,
|
||||||
|
getSelectedItemProps,
|
||||||
|
onRemove,
|
||||||
|
}: SelectedItemsProps ) => {
|
||||||
|
return (
|
||||||
|
<div className="woocommerce-experimental-select-control__selected-items">
|
||||||
|
{ items.map( ( item, index ) => (
|
||||||
|
<span
|
||||||
|
key={ `selected-item-${ index }` }
|
||||||
|
className="woocommerce-experimental-select-control__selected-item"
|
||||||
|
{ ...getSelectedItemProps( {
|
||||||
|
selectedItem: item,
|
||||||
|
index,
|
||||||
|
} ) }
|
||||||
|
>
|
||||||
|
{ /* eslint-disable-next-line @typescript-eslint/ban-ts-comment */ }
|
||||||
|
{ /* @ts-ignore Additional props are not required. */ }
|
||||||
|
<Tag
|
||||||
|
id={ item.value }
|
||||||
|
remove={ () => () => onRemove( item ) }
|
||||||
|
label={ itemToString( item ) }
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
) ) }
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -0,0 +1,233 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { CheckboxControl, Spinner } from '@wordpress/components';
|
||||||
|
import React, { createElement } from 'react';
|
||||||
|
import { useState } from '@wordpress/element';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { ItemType, SelectedType } from '../types';
|
||||||
|
import { MenuItem } from '../menu-item';
|
||||||
|
import { SelectControl } from '../';
|
||||||
|
import { Menu } from '../menu';
|
||||||
|
|
||||||
|
const sampleItems = [
|
||||||
|
{ value: 'apple', label: 'Apple' },
|
||||||
|
{ value: 'pear', label: 'Pear' },
|
||||||
|
{ value: 'orange', label: 'Orange' },
|
||||||
|
{ value: 'grape', label: 'Grape' },
|
||||||
|
{ value: 'banana', label: 'Banana' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const Single: React.FC = () => {
|
||||||
|
const [ selected, setSelected ] = useState< SelectedType >( null );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
Selected: { JSON.stringify( selected ) }
|
||||||
|
<SelectControl
|
||||||
|
items={ sampleItems }
|
||||||
|
label="Single value"
|
||||||
|
selected={ selected }
|
||||||
|
onSelect={ ( item ) => item && setSelected( item ) }
|
||||||
|
onRemove={ () => setSelected( null ) }
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Multiple: React.FC = () => {
|
||||||
|
const [ selected, setSelected ] = useState< ItemType[] >( [] );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SelectControl
|
||||||
|
multiple
|
||||||
|
items={ sampleItems }
|
||||||
|
label="Multiple values"
|
||||||
|
selected={ selected }
|
||||||
|
onSelect={ ( item ) =>
|
||||||
|
Array.isArray( selected ) &&
|
||||||
|
setSelected( [ ...selected, item ] )
|
||||||
|
}
|
||||||
|
onRemove={ ( item ) =>
|
||||||
|
setSelected( selected.filter( ( i ) => i !== item ) )
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FuzzyMatching: React.FC = () => {
|
||||||
|
const [ selected, setSelected ] = useState< ItemType[] >( [] );
|
||||||
|
|
||||||
|
const getFilteredItems = (
|
||||||
|
allItems: ItemType[],
|
||||||
|
inputValue: string,
|
||||||
|
selectedItems: ItemType[]
|
||||||
|
) => {
|
||||||
|
const pattern =
|
||||||
|
'.*' + inputValue.toLowerCase().split( '' ).join( '.*' ) + '.*';
|
||||||
|
const re = new RegExp( pattern );
|
||||||
|
|
||||||
|
return allItems.filter( ( item ) => {
|
||||||
|
if ( selectedItems.indexOf( item ) >= 0 ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return re.test( item.label.toLowerCase() );
|
||||||
|
} );
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectControl
|
||||||
|
multiple
|
||||||
|
getFilteredItems={ getFilteredItems }
|
||||||
|
items={ sampleItems }
|
||||||
|
label="Fuzzy matching"
|
||||||
|
selected={ selected }
|
||||||
|
onSelect={ ( item ) => setSelected( [ ...selected, item ] ) }
|
||||||
|
onRemove={ ( item ) =>
|
||||||
|
setSelected( selected.filter( ( i ) => i !== item ) )
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Async: React.FC = () => {
|
||||||
|
const [ selectedItem, setSelectedItem ] = useState< SelectedType >( null );
|
||||||
|
const [ fetchedItems, setFetchedItems ] = useState< ItemType[] >( [] );
|
||||||
|
const [ isFetching, setIsFetching ] = useState( false );
|
||||||
|
|
||||||
|
const fetchItems = ( value: string | undefined ) => {
|
||||||
|
setIsFetching( true );
|
||||||
|
setTimeout( () => {
|
||||||
|
const results = sampleItems.sort( () => 0.5 - Math.random() );
|
||||||
|
setFetchedItems( results );
|
||||||
|
setIsFetching( false );
|
||||||
|
}, 1500 );
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SelectControl
|
||||||
|
label="Async"
|
||||||
|
items={ fetchedItems }
|
||||||
|
onInputChange={ fetchItems }
|
||||||
|
selected={ selectedItem }
|
||||||
|
onSelect={ ( item ) => setSelectedItem( item ) }
|
||||||
|
onRemove={ () => setSelectedItem( null ) }
|
||||||
|
placeholder="Start typing..."
|
||||||
|
>
|
||||||
|
{ ( {
|
||||||
|
items,
|
||||||
|
isOpen,
|
||||||
|
highlightedIndex,
|
||||||
|
getItemProps,
|
||||||
|
getMenuProps,
|
||||||
|
} ) => {
|
||||||
|
return (
|
||||||
|
<Menu isOpen={ isOpen } getMenuProps={ getMenuProps }>
|
||||||
|
{ isFetching ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
items.map( ( item, index: number ) => (
|
||||||
|
<MenuItem
|
||||||
|
key={ `${ item.value }${ index }` }
|
||||||
|
index={ index }
|
||||||
|
isActive={ highlightedIndex === index }
|
||||||
|
item={ item }
|
||||||
|
getItemProps={ getItemProps }
|
||||||
|
>
|
||||||
|
{ item.label }
|
||||||
|
</MenuItem>
|
||||||
|
) )
|
||||||
|
) }
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
} }
|
||||||
|
</SelectControl>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const CustomRender: React.FC = () => {
|
||||||
|
const [ selected, setSelected ] = useState< ItemType[] >( [] );
|
||||||
|
|
||||||
|
const onRemove = ( item ) => {
|
||||||
|
setSelected( selected.filter( ( i ) => i !== item ) );
|
||||||
|
};
|
||||||
|
|
||||||
|
const onSelect = ( item ) => {
|
||||||
|
const isSelected = selected.find( ( i ) => i === item );
|
||||||
|
if ( isSelected ) {
|
||||||
|
onRemove( item );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelected( [ ...selected, item ] );
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<SelectControl
|
||||||
|
multiple
|
||||||
|
label="Custom render"
|
||||||
|
items={ sampleItems }
|
||||||
|
getFilteredItems={ ( allItems ) => allItems }
|
||||||
|
selected={ selected }
|
||||||
|
onSelect={ onSelect }
|
||||||
|
onRemove={ onRemove }
|
||||||
|
>
|
||||||
|
{ ( {
|
||||||
|
items,
|
||||||
|
highlightedIndex,
|
||||||
|
getItemProps,
|
||||||
|
getMenuProps,
|
||||||
|
} ) => {
|
||||||
|
return (
|
||||||
|
<Menu isOpen={ true } getMenuProps={ getMenuProps }>
|
||||||
|
{ items.map( ( item, index: number ) => {
|
||||||
|
const isSelected = selected.includes( item );
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={ `${ item.value }${ index }` }
|
||||||
|
index={ index }
|
||||||
|
isActive={ highlightedIndex === index }
|
||||||
|
item={ item }
|
||||||
|
getItemProps={ getItemProps }
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<CheckboxControl
|
||||||
|
onChange={ () => null }
|
||||||
|
checked={ isSelected }
|
||||||
|
label={
|
||||||
|
<span
|
||||||
|
style={ {
|
||||||
|
fontWeight:
|
||||||
|
isSelected
|
||||||
|
? 'bold'
|
||||||
|
: 'normal',
|
||||||
|
} }
|
||||||
|
>
|
||||||
|
{ item.label }
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
} ) }
|
||||||
|
</Menu>
|
||||||
|
);
|
||||||
|
} }
|
||||||
|
</SelectControl>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'WooCommerce Admin/experimental/SelectControl',
|
||||||
|
component: SelectControl,
|
||||||
|
};
|
|
@ -0,0 +1,47 @@
|
||||||
|
/**
|
||||||
|
* External dependencies
|
||||||
|
*/
|
||||||
|
import { ReactElement, Component } from 'react';
|
||||||
|
import {
|
||||||
|
UseComboboxGetItemPropsOptions,
|
||||||
|
UseComboboxGetMenuPropsOptions,
|
||||||
|
GetPropsCommonOptions,
|
||||||
|
} from 'downshift';
|
||||||
|
|
||||||
|
export type ItemType = {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type SelectedType = ItemType | null;
|
||||||
|
|
||||||
|
export type Props = {
|
||||||
|
[ key: string ]: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type getItemPropsType = (
|
||||||
|
options: UseComboboxGetItemPropsOptions< ItemType >
|
||||||
|
// These are the types provided by Downshift.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
) => any;
|
||||||
|
|
||||||
|
export type getMenuPropsType = (
|
||||||
|
options?: UseComboboxGetMenuPropsOptions,
|
||||||
|
otherOptions?: GetPropsCommonOptions
|
||||||
|
// These are the types provided by Downshift.
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
) => any;
|
||||||
|
|
||||||
|
export type ChildrenProps = {
|
||||||
|
items: ItemType[];
|
||||||
|
isOpen: boolean;
|
||||||
|
highlightedIndex: number;
|
||||||
|
getItemProps: getItemPropsType;
|
||||||
|
getMenuProps: getMenuPropsType;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ChildrenType = ( {
|
||||||
|
items,
|
||||||
|
isOpen,
|
||||||
|
highlightedIndex,
|
||||||
|
}: ChildrenProps ) => ReactElement | Component;
|
|
@ -0,0 +1,20 @@
|
||||||
|
/**
|
||||||
|
* Internal dependencies
|
||||||
|
*/
|
||||||
|
import { ItemType, SelectedType } from './types';
|
||||||
|
|
||||||
|
export const itemToString = ( item: ItemType | null ) => {
|
||||||
|
return item ? item.label : '';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getFilteredItems = (
|
||||||
|
allItems: ItemType[],
|
||||||
|
inputValue: string,
|
||||||
|
selectedItems: ItemType[]
|
||||||
|
) => {
|
||||||
|
return allItems.filter(
|
||||||
|
( item ) =>
|
||||||
|
selectedItems.indexOf( item ) < 0 &&
|
||||||
|
item.label.toLowerCase().startsWith( inputValue.toLowerCase() )
|
||||||
|
);
|
||||||
|
};
|
|
@ -37,6 +37,8 @@ export { default as SearchListItem } from './search-list-control/item';
|
||||||
export { default as SectionHeader } from './section-header';
|
export { default as SectionHeader } from './section-header';
|
||||||
export { default as SegmentedSelection } from './segmented-selection';
|
export { default as SegmentedSelection } from './segmented-selection';
|
||||||
export { default as SelectControl } from './select-control';
|
export { default as SelectControl } from './select-control';
|
||||||
|
export { SelectControl as __experimentalSelectControl } from './experimental-select-control';
|
||||||
|
export { MenuItem as __experimentalSelectControlMenuItem } from './experimental-select-control/menu-item';
|
||||||
export { default as ScrollTo } from './scroll-to';
|
export { default as ScrollTo } from './scroll-to';
|
||||||
export { SortableList } from './sortable-list';
|
export { SortableList } from './sortable-list';
|
||||||
export { default as Spinner } from './spinner';
|
export { default as Spinner } from './spinner';
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
@import 'dropdown-button/style.scss';
|
@import 'dropdown-button/style.scss';
|
||||||
@import 'ellipsis-menu/style.scss';
|
@import 'ellipsis-menu/style.scss';
|
||||||
@import 'empty-content/style.scss';
|
@import 'empty-content/style.scss';
|
||||||
|
@import 'experimental-select-control/select-control.scss';
|
||||||
@import 'advanced-filters/style.scss';
|
@import 'advanced-filters/style.scss';
|
||||||
@import 'date-range-filter-picker/style.scss';
|
@import 'date-range-filter-picker/style.scss';
|
||||||
@import 'filter-picker/style.scss';
|
@import 'filter-picker/style.scss';
|
||||||
|
|
|
@ -16,7 +16,7 @@ import {
|
||||||
Spinner,
|
Spinner,
|
||||||
} from '@wordpress/components';
|
} from '@wordpress/components';
|
||||||
import { withDispatch, withSelect } from '@wordpress/data';
|
import { withDispatch, withSelect } from '@wordpress/data';
|
||||||
import { SelectControl, Form, TextControl } from '@woocommerce/components';
|
import { Form, TextControl, SelectControl } from '@woocommerce/components';
|
||||||
import {
|
import {
|
||||||
ONBOARDING_STORE_NAME,
|
ONBOARDING_STORE_NAME,
|
||||||
PLUGINS_STORE_NAME,
|
PLUGINS_STORE_NAME,
|
||||||
|
|
377
pnpm-lock.yaml
377
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue