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-time-format": "^2.3.0",
|
||||
"dompurify": "^2.3.6",
|
||||
"downshift": "^6.1.9",
|
||||
"emoji-flags": "^1.3.0",
|
||||
"gridicons": "^3.4.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 SegmentedSelection } from './segmented-selection';
|
||||
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 { SortableList } from './sortable-list';
|
||||
export { default as Spinner } from './spinner';
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
@import 'dropdown-button/style.scss';
|
||||
@import 'ellipsis-menu/style.scss';
|
||||
@import 'empty-content/style.scss';
|
||||
@import 'experimental-select-control/select-control.scss';
|
||||
@import 'advanced-filters/style.scss';
|
||||
@import 'date-range-filter-picker/style.scss';
|
||||
@import 'filter-picker/style.scss';
|
||||
|
|
|
@ -16,7 +16,7 @@ import {
|
|||
Spinner,
|
||||
} from '@wordpress/components';
|
||||
import { withDispatch, withSelect } from '@wordpress/data';
|
||||
import { SelectControl, Form, TextControl } from '@woocommerce/components';
|
||||
import { Form, TextControl, SelectControl } from '@woocommerce/components';
|
||||
import {
|
||||
ONBOARDING_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