diff --git a/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.js b/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.js
index d28162f9690..657c417c588 100644
--- a/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.js
+++ b/plugins/woocommerce-admin/client/dashboard/components/settings/general/store-address.js
@@ -4,10 +4,15 @@
*/
import { __ } from '@wordpress/i18n';
import { decodeEntities } from '@wordpress/html-entities';
-import { SelectControl, TextControl } from 'newspack-components';
+import { TextControl } from 'newspack-components';
import { useMemo } from 'react';
import { getSetting } from '@woocommerce/wc-admin-settings';
+/**
+ * Internal depdencies
+ */
+import { SelectControl } from '@woocommerce/components';
+
const { countries } = getSetting( 'dataEndpoints', { countries: {} } );
/**
* Form validation.
@@ -43,7 +48,7 @@ export function getCountryStateOptions() {
const countryStateOptions = countries.reduce( ( acc, country ) => {
if ( ! country.states.length ) {
acc.push( {
- value: country.code,
+ key: country.code,
label: decodeEntities( country.name ),
} );
@@ -52,7 +57,7 @@ export function getCountryStateOptions() {
const countryStates = country.states.map( state => {
return {
- value: country.code + ':' + state.code,
+ key: country.code + ':' + state.code,
label: decodeEntities( country.name ) + ' -- ' + decodeEntities( state.name ),
};
} );
@@ -62,8 +67,6 @@ export function getCountryStateOptions() {
return acc;
}, [] );
- countryStateOptions.unshift( { value: '', label: '' } );
-
return countryStateOptions;
}
@@ -95,6 +98,7 @@ export function StoreAddress( props ) {
label={ __( 'Country / State', 'woocommerce-admin' ) }
required
options={ countryStateOptions }
+ isSearchable
{ ...getInputProps( 'countryState' ) }
/>
diff --git a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/business-details.js b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/business-details.js
index 388146fc34e..f3032aa1f09 100644
--- a/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/business-details.js
+++ b/plugins/woocommerce-admin/client/dashboard/profile-wizard/steps/business-details.js
@@ -19,7 +19,7 @@ import { getSetting, CURRENCY as currency } from '@woocommerce/wc-admin-settings
/**
* Internal dependencies
*/
-import { H, Card, SimpleSelectControl, Form } from '@woocommerce/components';
+import { H, Card, SelectControl, Form } from '@woocommerce/components';
import withSelect from 'wc-api/with-select';
import { recordEvent } from 'lib/tracks';
import { formatCurrency } from '@woocommerce/currency';
@@ -200,52 +200,52 @@ class BusinessDetails extends Component {
render() {
const productCountOptions = [
{
- value: '1-10',
+ key: '1-10',
label: this.getNumberRangeString( 1, 10 ),
},
{
- value: '11-100',
+ key: '11-100',
label: this.getNumberRangeString( 11, 100 ),
},
{
- value: '101-1000',
+ key: '101-1000',
label: this.getNumberRangeString( 101, 1000 ),
},
{
- value: '1000+',
+ key: '1000+',
label: this.getNumberRangeString( 1000 ),
},
];
const revenueOptions = [
{
- value: 'none',
+ key: 'none',
label: sprintf(
_x( "%s (I'm just getting started)", '$0 revenue amount', 'woocommerce-admin' ),
formatCurrency( 0 )
),
},
{
- value: 'up-to-2500',
+ key: 'up-to-2500',
label: sprintf(
_x( 'Up to %s', 'Up to a certain revenue amount', 'woocommerce-admin' ),
formatCurrency( 2500 )
),
},
{
- value: '2500-10000',
+ key: '2500-10000',
label: this.getNumberRangeString( 2500, 10000, formatCurrency ),
},
{
- value: '10000-50000',
+ key: '10000-50000',
label: this.getNumberRangeString( 10000, 50000, formatCurrency ),
},
{
- value: '50000-250000',
+ key: '50000-250000',
label: this.getNumberRangeString( 50000, 250000, formatCurrency ),
},
{
- value: 'more-than-250000',
+ key: 'more-than-250000',
label: sprintf(
_x( 'More than %s', 'More than a certain revenue amount', 'woocommerce-admin' ),
formatCurrency( 250000 )
@@ -255,19 +255,19 @@ class BusinessDetails extends Component {
const sellingVenueOptions = [
{
- value: 'no',
+ key: 'no',
label: __( 'No', 'woocommerce-admin' ),
},
{
- value: 'other',
+ key: 'other',
label: __( 'Yes, on another platform', 'woocommerce-admin' ),
},
{
- value: 'brick-mortar',
+ key: 'brick-mortar',
label: __( 'Yes, in person at physical stores and/or events', 'woocommerce-admin' ),
},
{
- value: 'brick-mortar-other',
+ key: 'brick-mortar-other',
label: __(
'Yes, on another platform and in person at physical stores and/or events',
'woocommerce-admin'
@@ -277,23 +277,23 @@ class BusinessDetails extends Component {
const otherPlatformOptions = [
{
- value: 'shopify',
+ key: 'shopify',
label: __( 'Shopify', 'woocommerce-admin' ),
},
{
- value: 'bigcommerce',
+ key: 'bigcommerce',
label: __( 'BigCommerce', 'woocommerce-admin' ),
},
{
- value: 'magento',
+ key: 'magento',
label: __( 'Magento', 'woocommerce-admin' ),
},
{
- value: 'wix',
+ key: 'wix',
label: __( 'Wix', 'woocommerce-admin' ),
},
{
- value: 'other',
+ key: 'other',
label: __( 'Other', 'woocommerce-admin' ),
},
];
@@ -320,14 +320,14 @@ class BusinessDetails extends Component {
-
- ` component to ``.
+- Added `isSearchable` prop to `` to allow simple select dropdowns.
+- Removed the `` component.
+
# 4.0.0
- Added a new `` component.
- Changed the `
` `description` prop to `content` and allowed content nodes to be passed in addition to strings.
diff --git a/plugins/woocommerce-admin/packages/components/package.json b/plugins/woocommerce-admin/packages/components/package.json
index 0fb3d84953f..25662e996c8 100644
--- a/plugins/woocommerce-admin/packages/components/package.json
+++ b/plugins/woocommerce-admin/packages/components/package.json
@@ -1,6 +1,6 @@
{
"name": "@woocommerce/components",
- "version": "3.2.0",
+ "version": "4.1.0",
"description": "UI components for WooCommerce.",
"author": "Automattic",
"license": "GPL-3.0-or-later",
diff --git a/plugins/woocommerce-admin/packages/components/src/autocomplete/README.md b/plugins/woocommerce-admin/packages/components/src/autocomplete/README.md
deleted file mode 100644
index 7c8e1d5b0ff..00000000000
--- a/plugins/woocommerce-admin/packages/components/src/autocomplete/README.md
+++ /dev/null
@@ -1,52 +0,0 @@
-Autocomplete
-===
-
-A search box which filters options while typing,
-allowing a user to select from an option from a filtered list.
-
-## Usage
-
-```jsx
-const options = [
- {
- key: 'apple',
- label: 'Apple',
- value: { id: 'apple' },
- },
- {
- key: 'apricot',
- label: 'Apricot',
- value: { id: 'apricot' },
- },
-];
-
- setState( { singleSelected: selected } ) }
- options={ options }
- placeholder="Start typing to filter options..."
- selected={ singleSelected }
-/>
-```
-
-### Props
-
-Name | Type | Default | Description
---- | --- | --- | ---
-`className` | string | `null` | Class name applied to parent div
-`excludeSelectedOptions` | boolean | `true` | Exclude already selected options from the options list
-`onFilter` | function | `identity` | Add or remove items to the list of options after filtering, passed the array of filtered options and should return an array of options.
-`getSearchExpression` | function | `identity` | Function to add regex expression to the filter the results, passed the search query
-`help` | string\|node | `null` | Help text to be appended beneath the input
-`inlineTags` | boolean | `false` | Render tags inside input, otherwise render below input
-`label` | string | `null` | A label to use for the main input
-`onChange` | function | `noop` | Function called when selected results change, passed result list
-`onSearch` | function | `noop` | Function to run after the search query is updated, passed the search query
-`options` | array | `null` | (required) An array of objects for the options list. The option along with its key, label and value will be returned in the onChange event
-`placeholder` | string | `null` | A placeholder for the search input
-`selected` | array | `[]` | An array of objects describing selected values. If the label of the selected value is omitted, the Tag of that value will not be rendered inside the search box
-`maxResults` | number | `0` | A limit for the number of results shown in the options menu. Set to 0 for no limit
-`multiple` | boolean | `false` | Allow multiple option selections
-`showClearButton` | boolean | `false` | Render a 'Clear' button next to the input box to remove its contents
-`hideBeforeSearch` | boolean | `false` | Only show list options after typing a search query
-`staticList` | boolean | `false` | Render results list positioned statically instead of absolutely
diff --git a/plugins/woocommerce-admin/packages/components/src/autocomplete/docs/example.js b/plugins/woocommerce-admin/packages/components/src/autocomplete/docs/example.js
deleted file mode 100644
index d842eee2bd2..00000000000
--- a/plugins/woocommerce-admin/packages/components/src/autocomplete/docs/example.js
+++ /dev/null
@@ -1,90 +0,0 @@
-/**
- * Internal dependencies
- */
-import { Autocomplete } from '@woocommerce/components';
-
-/**
- * External dependencies
- */
-import { withState } from '@wordpress/compose';
-
-const options = [
- {
- key: 'apple',
- label: 'Apple',
- value: { id: 'apple' },
- },
- {
- key: 'apricot',
- label: 'Apricot',
- value: { id: 'apricot' },
- },
- {
- key: 'banana',
- label: 'Banana',
- keywords: [ 'best', 'fruit' ],
- value: { id: 'banana' },
- },
- {
- key: 'blueberry',
- label: 'Blueberry',
- value: { id: 'blueberry' },
- },
- {
- key: 'cherry',
- label: 'Cherry',
- value: { id: 'cherry' },
- },
- {
- key: 'cantaloupe',
- label: 'Cantaloupe',
- value: { id: 'cantaloupe' },
- },
- {
- key: 'dragonfruit',
- label: 'Dragon Fruit',
- value: { id: 'dragonfruit' },
- },
- {
- key: 'elderberry',
- label: 'Elderberry',
- value: { id: 'elderberry' },
- },
-];
-
-export default withState( {
- singleSelected: [],
- multipleSelected: [],
- inlineSelected: [],
-} )( ( { singleSelected, multipleSelected, inlineSelected, setState } ) => (
-
-
setState( { singleSelected: selected } ) }
- options={ options }
- placeholder="Start typing to filter options..."
- selected={ singleSelected }
- />
-
- setState( { inlineSelected: selected } ) }
- options={ options }
- placeholder="Start typing to filter options..."
- selected={ inlineSelected }
- />
-
- setState( { multipleSelected: selected } ) }
- options={ options }
- placeholder="Start typing to filter options..."
- selected={ multipleSelected }
- showClearButton
- />
-
-) );
diff --git a/plugins/woocommerce-admin/packages/components/src/autocomplete/test/index.js b/plugins/woocommerce-admin/packages/components/src/autocomplete/test/index.js
deleted file mode 100644
index 5daffca5c82..00000000000
--- a/plugins/woocommerce-admin/packages/components/src/autocomplete/test/index.js
+++ /dev/null
@@ -1,161 +0,0 @@
-/** @format */
-/**
- * External dependencies
- */
-import { mount } from 'enzyme';
-import { Button } from '@wordpress/components';
-
-/**
- * Internal dependencies
- */
-import { Autocomplete } from '../index';
-
-describe( 'Autocomplete', () => {
- const optionClassname = 'woocommerce-autocomplete__option';
- const query = 'lorem';
- const options = [
- { key: '1', label: 'lorem 1', value: { id: '1' } },
- { key: '2', label: 'lorem 2', value: { id: '2' } },
- { key: '3', label: 'bar', value: { id: '3' } },
- ];
-
- it( 'returns matching elements', () => {
- const autocomplete = mount(
-
- );
- autocomplete.setState( {
- query,
- } );
-
- autocomplete.instance().search( query );
- autocomplete.update();
-
- expect( autocomplete.find( Button ).filter( '.' + optionClassname ).length ).toBe( 2 );
- } );
-
- it( 'doesn\'t return matching excluded elements', () => {
- const autocomplete = mount(
-
- );
- autocomplete.setState( {
- query,
- } );
-
- autocomplete.instance().search( query );
- autocomplete.update();
-
- expect( autocomplete.find( Button ).filter( '.' + optionClassname ).length ).toBe( 1 );
- } );
-
- it( 'trims spaces from input', () => {
- const autocomplete = mount(
-
- );
- autocomplete.setState( {
- query,
- } );
-
- autocomplete.instance().search( ' ' + query + ' ' );
- autocomplete.update();
-
- expect( autocomplete.find( Button ).filter( '.' + optionClassname ).length ).toBe( 2 );
- } );
-
- it( 'limits results', () => {
- const autocomplete = mount(
-
- );
- autocomplete.setState( {
- query,
- } );
-
- autocomplete.instance().search( query );
- autocomplete.update();
-
- expect( autocomplete.find( Button ).filter( '.' + optionClassname ).length ).toBe( 1 );
- } );
-
- it( 'shows options initially', () => {
- const autocomplete = mount(
-
- );
-
- autocomplete.instance().search( '' );
- autocomplete.update();
-
- expect( autocomplete.find( Button ).filter( '.' + optionClassname ).length ).toBe( 3 );
- } );
-
- it( 'shows options after query', () => {
- const autocomplete = mount(
-
- );
-
- autocomplete.instance().search( '' );
- autocomplete.update();
-
- expect( autocomplete.find( Button ).filter( '.' + optionClassname ).length ).toBe( 0 );
-
- autocomplete.instance().search( query );
- autocomplete.update();
-
- expect( autocomplete.find( Button ).filter( '.' + optionClassname ).length ).toBe( 2 );
- } );
-
- it( 'appends an option after filtering', () => {
- const autocomplete = mount(
- filteredOptions.concat( [ { key: 'new-option', label: 'New options' } ] ) }
- />
- );
-
- autocomplete.instance().search( query );
- autocomplete.update();
-
- expect( autocomplete.find( Button ).filter( '.' + optionClassname ).length ).toBe( 3 );
- } );
-
- it( 'changes the options on search', () => {
- const queriedOptions = [];
- const queryOptions = ( searchedQuery ) => {
- if ( searchedQuery === 'test' ) {
- queriedOptions.push( { key: 'test-option', label: 'Test option' } );
- }
- };
- const autocomplete = mount(
- queriedOptions }
- />
- );
-
- autocomplete.instance().search( '' );
- autocomplete.update();
-
- expect( autocomplete.find( Button ).filter( '.' + optionClassname ).length ).toBe( 0 );
-
- autocomplete.instance().search( 'test' );
- autocomplete.update();
-
- expect( autocomplete.find( Button ).filter( '.' + optionClassname ).length ).toBe( 1 );
- } );
-} );
diff --git a/plugins/woocommerce-admin/packages/components/src/index.js b/plugins/woocommerce-admin/packages/components/src/index.js
index 88f60b2a779..c713cd661e9 100644
--- a/plugins/woocommerce-admin/packages/components/src/index.js
+++ b/plugins/woocommerce-admin/packages/components/src/index.js
@@ -7,7 +7,6 @@ import 'react-dates/initialize';
export { default as AdvancedFilters } from './advanced-filters';
export { default as AnimationSlider } from './animation-slider';
-export { default as Autocomplete } from './autocomplete';
export { default as Chart } from './chart';
export { default as ChartPlaceholder } from './chart/placeholder';
export { default as Card } from './card';
@@ -42,8 +41,8 @@ export { default as SearchListControl } from './search-list-control';
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 { default as ScrollTo } from './scroll-to';
-export { default as SimpleSelectControl } from './simple-select-control';
export { default as SplitButton } from './split-button';
export { default as Spinner } from './spinner';
export { default as Stepper } from './stepper';
diff --git a/plugins/woocommerce-admin/packages/components/src/select-control/README.md b/plugins/woocommerce-admin/packages/components/src/select-control/README.md
new file mode 100644
index 00000000000..9ce0bc7b9d9
--- /dev/null
+++ b/plugins/woocommerce-admin/packages/components/src/select-control/README.md
@@ -0,0 +1,51 @@
+# SelectControl
+
+A search box which filters options while typing,
+allowing a user to select from an option from a filtered list.
+
+## Usage
+
+```jsx
+const options = [
+ {
+ key: 'apple',
+ label: 'Apple',
+ value: { id: 'apple' },
+ },
+ {
+ key: 'apricot',
+ label: 'Apricot',
+ value: { id: 'apricot' },
+ },
+];
+
+ setState( { singleSelected: selected } ) }
+ options={ options }
+ placeholder="Start typing to filter options..."
+ selected={ singleSelected }
+/>;
+```
+
+### Props
+
+| Name | Type | Default | Description |
+| ------------------------ | ------------ | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `className` | string | `null` | Class name applied to parent div |
+| `excludeSelectedOptions` | boolean | `true` | Exclude already selected options from the options list |
+| `onFilter` | function | `identity` | Add or remove items to the list of options after filtering, passed the array of filtered options and should return an array of options. |
+| `getSearchExpression` | function | `identity` | Function to add regex expression to the filter the results, passed the search query |
+| `help` | string\|node | `null` | Help text to be appended beneath the input |
+| `inlineTags` | boolean | `false` | Render tags inside input, otherwise render below input |
+| `label` | string | `null` | A label to use for the main input |
+| `onChange` | function | `noop` | Function called when selected results change, passed result list |
+| `onSearch` | function | `noop` | Function to run after the search query is updated, passed the search query |
+| `options` | array | `null` | (required) An array of objects for the options list. The option along with its key, label and value will be returned in the onChange event |
+| `placeholder` | string | `null` | A placeholder for the search input |
+| `selected` | array | `[]` | An array of objects describing selected values. If the label of the selected value is omitted, the Tag of that value will not be rendered inside the search box |
+| `maxResults` | number | `0` | A limit for the number of results shown in the options menu. Set to 0 for no limit |
+| `multiple` | boolean | `false` | Allow multiple option selections |
+| `showClearButton` | boolean | `false` | Render a 'Clear' button next to the input box to remove its contents |
+| `hideBeforeSearch` | boolean | `false` | Only show list options after typing a search query |
+| `staticList` | boolean | `false` | Render results list positioned statically instead of absolutely |
diff --git a/plugins/woocommerce-admin/packages/components/src/autocomplete/control.js b/plugins/woocommerce-admin/packages/components/src/select-control/control.js
similarity index 55%
rename from plugins/woocommerce-admin/packages/components/src/autocomplete/control.js
rename to plugins/woocommerce-admin/packages/components/src/select-control/control.js
index 96e5675d12f..83e6734be9b 100644
--- a/plugins/woocommerce-admin/packages/components/src/autocomplete/control.js
+++ b/plugins/woocommerce-admin/packages/components/src/select-control/control.js
@@ -3,7 +3,7 @@
* External dependencies
*/
import { __ } from '@wordpress/i18n';
-import { BACKSPACE } from '@wordpress/keycodes';
+import { BACKSPACE, DOWN, UP } from '@wordpress/keycodes';
import { Component, createRef } from '@wordpress/element';
import classnames from 'classnames';
import PropTypes from 'prop-types';
@@ -16,7 +16,7 @@ import Tags from './tags';
/**
* A search control to allow user input to filter the options.
*/
-class SearchControl extends Component {
+class Control extends Component {
constructor( props ) {
super( props );
this.state = {
@@ -38,9 +38,15 @@ class SearchControl extends Component {
}
onFocus( onSearch ) {
+ const { isSearchable, setExpanded } = this.props;
+
return event => {
this.setState( { isActive: true } );
- onSearch( event.target.value );
+ if ( isSearchable ) {
+ onSearch( event.target.value );
+ } else {
+ setExpanded( true );
+ }
};
}
@@ -49,11 +55,42 @@ class SearchControl extends Component {
}
onKeyDown( event ) {
- const { selected, onChange, query } = this.props;
+ const {
+ decrementSelectedIndex,
+ incrementSelectedIndex,
+ selected,
+ onChange,
+ query,
+ setExpanded,
+ } = this.props;
if ( BACKSPACE === event.keyCode && ! query && selected.length ) {
onChange( [ ...selected.slice( 0, -1 ) ] );
}
+
+ if ( DOWN === event.keyCode ) {
+ incrementSelectedIndex();
+ setExpanded( true );
+ event.preventDefault();
+ event.stopPropagation();
+ }
+
+ if ( UP === event.keyCode ) {
+ decrementSelectedIndex();
+ setExpanded( true );
+ event.preventDefault();
+ event.stopPropagation();
+ }
+ }
+
+ renderButton() {
+ const { multiple, selected } = this.props;
+
+ if ( multiple || ! selected.length ) {
+ return null;
+ }
+
+ return { selected[ 0 ].label }
;
}
renderInput() {
@@ -63,46 +100,50 @@ class SearchControl extends Component {
inlineTags,
instanceId,
isExpanded,
+ isSearchable,
listboxId,
onSearch,
placeholder,
- query,
} = this.props;
const { isActive } = this.state;
- return ;
+ return (
+
+ );
+ }
+
+ getInputValue() {
+ const { isSearchable, multiple, query, selected } = this.props;
+ const selectedValue = selected.length ? selected[ 0 ].label : '';
+
+ if ( ! isSearchable && multiple ) {
+ return '';
+ }
+
+ return isSearchable ? query : selectedValue;
}
render() {
- const {
- hasTags,
- help,
- inlineTags,
- instanceId,
- label,
- query,
- } = this.props;
+ const { hasTags, help, inlineTags, instanceId, isSearchable, label, query } = this.props;
const { isActive } = this.state;
return (
@@ -113,40 +154,42 @@ class SearchControl extends Component {
// for the benefit of sighted users.
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
{
this.input.current.focus();
} }
>
-
search
+ { isSearchable &&
search }
{ inlineTags &&
}
- { !! label &&
+ { !! label && (
- }
+ ) }
{ this.renderInput() }
- { inlineTags &&
- { __( 'Move backward for selected items', 'woocommerce-admin' ) }
- }
- { !! help &&
+ { inlineTags && (
+
+ { __( 'Move backward for selected items', 'woocommerce-admin' ) }
+
+ ) }
+ { !! help && (
{ help }
- }
+ ) }
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
@@ -154,7 +197,7 @@ class SearchControl extends Component {
}
}
-SearchControl.propTypes = {
+Control.propTypes = {
/**
* Bool to determine if tags should be rendered.
*/
@@ -162,16 +205,17 @@ SearchControl.propTypes = {
/**
* Help text to be appended beneath the input.
*/
- help: PropTypes.oneOfType( [
- PropTypes.string,
- PropTypes.node,
- ] ),
+ help: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
/**
* Render tags inside input, otherwise render below input.
*/
inlineTags: PropTypes.bool,
/**
- * ID of the main Autocomplete instance.
+ * Allow the select options to be filtered by search input.
+ */
+ isSearchable: PropTypes.bool,
+ /**
+ * ID of the main SelectControl instance.
*/
instanceId: PropTypes.number,
/**
@@ -205,13 +249,10 @@ SearchControl.propTypes = {
*/
selected: PropTypes.arrayOf(
PropTypes.shape( {
- key: PropTypes.oneOfType( [
- PropTypes.number,
- PropTypes.string,
- ] ).isRequired,
+ key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] ).isRequired,
label: PropTypes.string,
} )
),
};
-export default SearchControl;
+export default Control;
diff --git a/plugins/woocommerce-admin/packages/components/src/select-control/docs/example.js b/plugins/woocommerce-admin/packages/components/src/select-control/docs/example.js
new file mode 100644
index 00000000000..b0123bb5d91
--- /dev/null
+++ b/plugins/woocommerce-admin/packages/components/src/select-control/docs/example.js
@@ -0,0 +1,121 @@
+/**
+ * Internal dependencies
+ */
+import { SelectControl } from '@woocommerce/components';
+
+/**
+ * External dependencies
+ */
+import { withState } from '@wordpress/compose';
+
+const options = [
+ {
+ key: 'apple',
+ label: 'Apple',
+ value: { id: 'apple' },
+ },
+ {
+ key: 'apricot',
+ label: 'Apricot',
+ value: { id: 'apricot' },
+ },
+ {
+ key: 'banana',
+ label: 'Banana',
+ keywords: [ 'best', 'fruit' ],
+ value: { id: 'banana' },
+ },
+ {
+ key: 'blueberry',
+ label: 'Blueberry',
+ value: { id: 'blueberry' },
+ },
+ {
+ key: 'cherry',
+ label: 'Cherry',
+ value: { id: 'cherry' },
+ },
+ {
+ key: 'cantaloupe',
+ label: 'Cantaloupe',
+ value: { id: 'cantaloupe' },
+ },
+ {
+ key: 'dragonfruit',
+ label: 'Dragon Fruit',
+ value: { id: 'dragonfruit' },
+ },
+ {
+ key: 'elderberry',
+ label: 'Elderberry',
+ value: { id: 'elderberry' },
+ },
+];
+
+export default withState( {
+ simpleSelected: [],
+ simpleMultipleSelected: [],
+ singleSelected: [],
+ multipleSelected: [],
+ inlineSelected: [],
+} )(
+ ( {
+ simpleSelected,
+ simpleMultipleSelected,
+ singleSelected,
+ multipleSelected,
+ inlineSelected,
+ setState,
+ } ) => (
+
+ setState( { simpleSelected: selected } ) }
+ options={ options }
+ placeholder="Start typing to filter options..."
+ selected={ simpleSelected }
+ />
+
+ setState( { simpleMultipleSelected: selected } ) }
+ options={ options }
+ placeholder="Start typing to filter options..."
+ selected={ simpleMultipleSelected }
+ />
+
+ setState( { singleSelected: selected } ) }
+ options={ options }
+ placeholder="Start typing to filter options..."
+ selected={ singleSelected }
+ />
+
+ setState( { inlineSelected: selected } ) }
+ options={ options }
+ placeholder="Start typing to filter options..."
+ selected={ inlineSelected }
+ />
+
+ setState( { multipleSelected: selected } ) }
+ options={ options }
+ placeholder="Start typing to filter options..."
+ selected={ multipleSelected }
+ showClearButton
+ />
+
+ )
+);
diff --git a/plugins/woocommerce-admin/packages/components/src/autocomplete/index.js b/plugins/woocommerce-admin/packages/components/src/select-control/index.js
similarity index 61%
rename from plugins/woocommerce-admin/packages/components/src/autocomplete/index.js
rename to plugins/woocommerce-admin/packages/components/src/select-control/index.js
index 3f4e1bbf1cf..1ab0ff3ecbf 100644
--- a/plugins/woocommerce-admin/packages/components/src/autocomplete/index.js
+++ b/plugins/woocommerce-admin/packages/components/src/select-control/index.js
@@ -15,36 +15,41 @@ import { withInstanceId, compose } from '@wordpress/compose';
*/
import List from './list';
import Tags from './tags';
-import SearchControl from './control';
+import Control from './control';
/**
* A search box which filters options while typing,
* allowing a user to select from an option from a filtered list.
*/
-export class Autocomplete extends Component {
+export class SelectControl extends Component {
static getInitialState() {
return {
- filteredOptions: [],
- selectedIndex: 0,
+ isExpanded: false,
query: '',
};
}
constructor( props ) {
super( props );
- this.state = this.constructor.getInitialState();
+ this.state = {
+ ...this.constructor.getInitialState(),
+ filteredOptions: [],
+ selectedIndex: 0,
+ };
this.bindNode = this.bindNode.bind( this );
+ this.decrementSelectedIndex = this.decrementSelectedIndex.bind( this );
+ this.incrementSelectedIndex = this.incrementSelectedIndex.bind( this );
this.search = this.search.bind( this );
this.selectOption = this.selectOption.bind( this );
- this.updateSelectedIndex = this.updateSelectedIndex.bind( this );
+ this.setExpanded = this.setExpanded.bind( this );
}
bindNode( node ) {
this.node = node;
}
- reset( selected = this.props.selected ) {
+ reset( selected = this.getSelected() ) {
const { multiple } = this.props;
const initialState = this.constructor.getInitialState();
@@ -60,12 +65,6 @@ export class Autocomplete extends Component {
this.reset();
}
- isExpanded() {
- const { filteredOptions, query } = this.state;
-
- return filteredOptions.length > 0 || query;
- }
-
hasTags() {
const { multiple, selected } = this.props;
@@ -76,22 +75,54 @@ export class Autocomplete extends Component {
return selected.some( item => Boolean( item.label ) );
}
+ getSelected() {
+ const { multiple, options, selected } = this.props;
+
+ // Return the passed value if an array is provided.
+ if ( multiple || Array.isArray( selected ) ) {
+ return selected;
+ }
+
+ const selectedOption = options.find( option => option.key === selected );
+ return selectedOption ? [ selectedOption ] : [];
+ }
+
selectOption( option ) {
const { multiple, onChange, selected } = this.props;
const { query } = this.state;
const newSelected = multiple ? [ ...selected, option ] : [ option ];
- // Check if this is already selected
- const isSelected = findIndex( selected, { key: option.key } );
- if ( -1 === isSelected ) {
- onChange( newSelected, query );
+ // Trigger a change if the selected value is different and pass back
+ // an array or string depending on the original value.
+ if ( Array.isArray( selected ) ) {
+ const isSelected = findIndex( selected, { key: option.key } );
+ if ( -1 === isSelected ) {
+ onChange( newSelected, query );
+ }
+ } else if ( selected !== option.key ) {
+ onChange( option.key, query );
}
this.reset( newSelected );
}
- updateSelectedIndex( value ) {
- this.setState( { selectedIndex: value } );
+ decrementSelectedIndex() {
+ const { selectedIndex } = this.state;
+ const options = this.getOptions();
+ const nextSelectedIndex =
+ null !== selectedIndex
+ ? ( selectedIndex === 0 ? options.length : selectedIndex ) - 1
+ : options.length - 1;
+
+ this.setState( { selectedIndex: nextSelectedIndex } );
+ }
+
+ incrementSelectedIndex() {
+ const { selectedIndex } = this.state;
+ const options = this.getOptions();
+ const nextSelectedIndex = null !== selectedIndex ? ( selectedIndex + 1 ) % options.length : 0;
+
+ this.setState( { selectedIndex: nextSelectedIndex } );
}
announce( filteredOptions ) {
@@ -117,9 +148,21 @@ export class Autocomplete extends Component {
}
}
+ getOptions() {
+ const { isSearchable, options } = this.props;
+ const { filteredOptions } = this.state;
+ return isSearchable ? filteredOptions : options;
+ }
+
getFilteredOptions( query ) {
- const { excludeSelectedOptions, getSearchExpression, maxResults, onFilter, options, selected } = this.props;
- const selectedKeys = selected.map( option => option.key );
+ const {
+ excludeSelectedOptions,
+ getSearchExpression,
+ maxResults,
+ onFilter,
+ options,
+ } = this.props;
+ const selectedKeys = this.getSelected().map( option => option.key );
const filtered = [];
// Create a regular expression to filter the options.
@@ -155,42 +198,50 @@ export class Autocomplete extends Component {
return onFilter( filtered );
}
+ setExpanded( value ) {
+ this.setState( { isExpanded: value } );
+ }
+
search( query ) {
const { hideBeforeSearch, onSearch, options } = this.props;
onSearch( query );
// Get all options if `hideBeforeSearch` is enabled and query is not null.
- const filteredOptions = null !== query && ! query.length && ! hideBeforeSearch
- ? options
- : this.getFilteredOptions( query );
- this.setState( { selectedIndex: 0, filteredOptions, query: query || '' }, () => this.announce( filteredOptions ) );
+ const filteredOptions =
+ null !== query && ! query.length && ! hideBeforeSearch
+ ? options
+ : this.getFilteredOptions( query );
+ this.setState(
+ {
+ selectedIndex: 0,
+ filteredOptions,
+ isExpanded: Boolean( filteredOptions.length ),
+ query: query || '',
+ },
+ () => this.announce( filteredOptions )
+ );
}
render() {
- const {
- className,
- inlineTags,
- instanceId,
- options,
- } = this.props;
- const { selectedIndex } = this.state;
+ const { className, inlineTags, instanceId, isSearchable, options } = this.props;
+ const { isExpanded, selectedIndex } = this.state;
- const isExpanded = this.isExpanded();
const hasTags = this.hasTags();
const { key: selectedKey = '' } = options[ selectedIndex ] || {};
- const listboxId = isExpanded ? `woocommerce-autocomplete__listbox-${ instanceId }` : null;
+ const listboxId = isExpanded ? `woocommerce-select-control__listbox-${ instanceId }` : null;
const activeId = isExpanded
- ? `woocommerce-autocomplete__option-${ instanceId }-${ selectedKey }`
+ ? `woocommerce-select-control__option-${ instanceId }-${ selectedKey }`
: null;
return (
-
- { ! inlineTags && hasTags && }
- { isExpanded &&
+ { ! inlineTags && hasTags && }
+ { isExpanded && (
- }
+ ) }
);
}
}
-Autocomplete.propTypes = {
+SelectControl.propTypes = {
/**
* Class name applied to parent div.
*/
@@ -238,14 +296,15 @@ Autocomplete.propTypes = {
/**
* Help text to be appended beneath the input.
*/
- help: PropTypes.oneOfType( [
- PropTypes.string,
- PropTypes.node,
- ] ),
+ help: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
/**
* Render tags inside input, otherwise render below input.
*/
inlineTags: PropTypes.bool,
+ /**
+ * Allow the select options to be filtered by search input.
+ */
+ isSearchable: PropTypes.bool,
/**
* A label to use for the main input.
*/
@@ -265,10 +324,7 @@ Autocomplete.propTypes = {
options: PropTypes.arrayOf(
PropTypes.shape( {
isDisabled: PropTypes.bool,
- key: PropTypes.oneOfType( [
- PropTypes.number,
- PropTypes.string,
- ] ).isRequired,
+ key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] ).isRequired,
keywords: PropTypes.arrayOf( PropTypes.string ),
label: PropTypes.string,
value: PropTypes.any,
@@ -279,19 +335,19 @@ Autocomplete.propTypes = {
*/
placeholder: PropTypes.string,
/**
- * An array of objects describing selected values. If the label of the selected
- * value is omitted, the Tag of that value will not be rendered inside the
- * search box.
+ * An array of objects describing selected values or optionally a string for a single value.
+ * If the label of the selected value is omitted, the Tag of that value will not
+ * be rendered inside the search box.
*/
- selected: PropTypes.arrayOf(
- PropTypes.shape( {
- key: PropTypes.oneOfType( [
- PropTypes.number,
- PropTypes.string,
- ] ).isRequired,
- label: PropTypes.string,
- } )
- ),
+ selected: PropTypes.oneOfType( [
+ PropTypes.string,
+ PropTypes.arrayOf(
+ PropTypes.shape( {
+ key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] ).isRequired,
+ label: PropTypes.string,
+ } )
+ ),
+ ] ),
/**
* A limit for the number of results shown in the options menu. Set to 0 for no limit.
*/
@@ -314,10 +370,11 @@ Autocomplete.propTypes = {
staticList: PropTypes.bool,
};
-Autocomplete.defaultProps = {
+SelectControl.defaultProps = {
excludeSelectedOptions: true,
getSearchExpression: identity,
inlineTags: false,
+ isSearchable: false,
onChange: noop,
onFilter: identity,
onSearch: noop,
@@ -333,4 +390,4 @@ export default compose( [
withSpokenMessages,
withInstanceId,
withFocusOutside, // this MUST be the innermost HOC as it calls handleFocusOutside
-] )( Autocomplete );
+] )( SelectControl );
diff --git a/plugins/woocommerce-admin/packages/components/src/autocomplete/list.js b/plugins/woocommerce-admin/packages/components/src/select-control/list.js
similarity index 57%
rename from plugins/woocommerce-admin/packages/components/src/autocomplete/list.js
rename to plugins/woocommerce-admin/packages/components/src/select-control/list.js
index e045e58f1e7..e4e07b598ba 100644
--- a/plugins/woocommerce-admin/packages/components/src/autocomplete/list.js
+++ b/plugins/woocommerce-admin/packages/components/src/select-control/list.js
@@ -23,12 +23,16 @@ class List extends Component {
}
componentDidUpdate( prevProps ) {
- const { filteredOptions } = this.props;
+ const { options, selectedIndex } = this.props;
// Remove old option refs to avoid memory leaks.
- if ( ! isEqual( filteredOptions, prevProps.filteredOptions ) ) {
+ if ( ! isEqual( options, prevProps.options ) ) {
this.optionRefs = {};
}
+
+ if ( selectedIndex !== prevProps.selectedIndex ) {
+ this.scrollToOption( selectedIndex );
+ }
}
getOptionRef( index ) {
@@ -52,59 +56,63 @@ class List extends Component {
scrollToOption( index ) {
const listbox = this.listbox.current;
- if ( listbox.scrollHeight > listbox.clientHeight ) {
- const option = this.optionRefs[ index ].current;
- const scrollBottom = listbox.clientHeight + listbox.scrollTop;
- const elementBottom = option.offsetTop + option.offsetHeight;
- if ( elementBottom > scrollBottom ) {
- listbox.scrollTop = elementBottom - listbox.clientHeight;
- } else if ( option.offsetTop < listbox.scrollTop ) {
- listbox.scrollTop = option.offsetTop;
- }
+ if ( listbox.scrollHeight <= listbox.clientHeight ) {
+ return;
+ }
+
+ if ( ! this.optionRefs[ index ] ) {
+ return;
+ }
+
+ const option = this.optionRefs[ index ].current;
+ const scrollBottom = listbox.clientHeight + listbox.scrollTop;
+ const elementBottom = option.offsetTop + option.offsetHeight;
+ if ( elementBottom > scrollBottom ) {
+ listbox.scrollTop = elementBottom - listbox.clientHeight;
+ } else if ( option.offsetTop < listbox.scrollTop ) {
+ listbox.scrollTop = option.offsetTop;
}
}
handleKeyDown( event ) {
- const { filteredOptions, onChange, onSearch, selectedIndex } = this.props;
- if ( filteredOptions.length === 0 ) {
+ const {
+ decrementSelectedIndex,
+ incrementSelectedIndex,
+ options,
+ onSearch,
+ selectedIndex,
+ setExpanded,
+ } = this.props;
+ if ( options.length === 0 ) {
return;
}
- let nextSelectedIndex;
switch ( event.keyCode ) {
case UP:
- nextSelectedIndex = null !== selectedIndex
- ? ( selectedIndex === 0 ? filteredOptions.length : selectedIndex ) - 1
- : filteredOptions.length - 1;
- onChange( nextSelectedIndex );
- this.scrollToOption( nextSelectedIndex );
+ decrementSelectedIndex();
event.preventDefault();
event.stopPropagation();
break;
case DOWN:
- nextSelectedIndex = null !== selectedIndex
- ? ( selectedIndex + 1 ) % filteredOptions.length
- : 0;
- onChange( nextSelectedIndex );
- this.scrollToOption( nextSelectedIndex );
+ incrementSelectedIndex();
event.preventDefault();
event.stopPropagation();
break;
case ENTER:
- this.select( filteredOptions[ selectedIndex ] );
+ this.select( options[ selectedIndex ] );
event.preventDefault();
event.stopPropagation();
break;
case LEFT:
case RIGHT:
- onChange( null );
+ setExpanded( false );
break;
case ESCAPE:
- onChange( null );
+ setExpanded( false );
onSearch( null );
return;
@@ -133,30 +141,30 @@ class List extends Component {
}
render() {
- const { filteredOptions, instanceId, listboxId, selectedIndex, staticList } = this.props;
- const listboxClasses = classnames( 'woocommerce-autocomplete__listbox', {
+ const { instanceId, listboxId, options, selectedIndex, staticList } = this.props;
+ const listboxClasses = classnames( 'woocommerce-select-control__listbox', {
'is-static': staticList,
} );
return (
- { filteredOptions.map( ( option, index ) => (
-
- ) ) }
+ { options.map( ( option, index ) => (
+
+ ) ) }
);
}
@@ -164,22 +172,7 @@ class List extends Component {
List.propTypes = {
/**
- * Array of filtered options to display.
- */
- filteredOptions: PropTypes.arrayOf(
- PropTypes.shape( {
- isDisabled: PropTypes.bool,
- key: PropTypes.oneOfType( [
- PropTypes.number,
- PropTypes.string,
- ] ).isRequired,
- keywords: PropTypes.arrayOf( PropTypes.string ),
- label: PropTypes.string,
- value: PropTypes.any,
- } )
- ).isRequired,
- /**
- * ID of the main Autocomplete instance.
+ * ID of the main SelectControl instance.
*/
instanceId: PropTypes.number,
/**
@@ -190,14 +183,22 @@ List.propTypes = {
* Parent node to bind keyboard events to.
*/
node: PropTypes.instanceOf( Element ).isRequired,
- /**
- * Function called when selected results change, passed result list.
- */
- onChange: PropTypes.func,
/**
* Function to execute when an option is selected.
*/
onSelect: PropTypes.func,
+ /**
+ * Array of options to display.
+ */
+ options: PropTypes.arrayOf(
+ PropTypes.shape( {
+ isDisabled: PropTypes.bool,
+ key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] ).isRequired,
+ keywords: PropTypes.arrayOf( PropTypes.string ),
+ label: PropTypes.string,
+ value: PropTypes.any,
+ } )
+ ).isRequired,
/**
* Integer for the currently selected item.
*/
diff --git a/plugins/woocommerce-admin/packages/components/src/autocomplete/style.scss b/plugins/woocommerce-admin/packages/components/src/select-control/style.scss
similarity index 78%
rename from plugins/woocommerce-admin/packages/components/src/autocomplete/style.scss
rename to plugins/woocommerce-admin/packages/components/src/select-control/style.scss
index 98b5c6be494..d2e4a18ecad 100644
--- a/plugins/woocommerce-admin/packages/components/src/autocomplete/style.scss
+++ b/plugins/woocommerce-admin/packages/components/src/select-control/style.scss
@@ -1,4 +1,6 @@
-.woocommerce-autocomplete {
+/** @format */
+
+.woocommerce-select-control {
position: relative;
.components-base-control {
@@ -11,7 +13,7 @@
padding: $gap-small $gap;
position: relative;
- .woocommerce-autocomplete__tags {
+ .woocommerce-select-control__tags {
margin: $gap-small $gap-smallest 0 0;
}
@@ -24,16 +26,16 @@
align-items: center;
flex: 1;
margin-bottom: 0;
+ max-width: 100%;
}
.components-base-control__label {
- left: 52px;
position: absolute;
color: $studio-gray-50;
font-size: 16px;
}
- .woocommerce-autocomplete__control-input {
+ .woocommerce-select-control__control-input {
font-size: 16px;
border: 0;
box-shadow: none;
@@ -43,10 +45,15 @@
padding-right: 0;
width: 100%;
line-height: 24px;
+ text-align: left;
&::-webkit-search-cancel-button {
display: none;
}
+
+ &:focus {
+ outline: none;
+ }
}
i {
@@ -61,14 +68,13 @@
}
&.with-value .components-base-control__label,
- &.is-active .components-base-control__label,
&.has-tags .components-base-control__label {
font-size: 12px;
margin-top: -$gap-small;
}
}
- .woocommerce-autocomplete__tags {
+ .woocommerce-select-control__tags {
position: relative;
margin: $gap-small 0;
@@ -81,7 +87,7 @@
max-height: 24px;
}
- .woocommerce-autocomplete__clear {
+ .woocommerce-select-control__clear {
position: absolute;
right: 0;
top: calc(50% - 10px);
@@ -91,7 +97,7 @@
}
}
- .woocommerce-autocomplete__listbox {
+ .woocommerce-select-control__listbox {
background: $studio-white;
display: flex;
flex-direction: column;
@@ -111,7 +117,7 @@
}
}
- .woocommerce-autocomplete__option {
+ .woocommerce-select-control__option {
padding: $gap;
min-height: 56px;
font-size: 16px;
@@ -121,4 +127,15 @@
background: $studio-gray-0;
}
}
+
+ &.is-searchable {
+ .components-base-control__label {
+ left: 52px;
+ }
+
+ .components-base-control.is-active .components-base-control__label {
+ font-size: 12px;
+ margin-top: -$gap-small;
+ }
+ }
}
diff --git a/plugins/woocommerce-admin/packages/components/src/autocomplete/tags.js b/plugins/woocommerce-admin/packages/components/src/select-control/tags.js
similarity index 83%
rename from plugins/woocommerce-admin/packages/components/src/autocomplete/tags.js
rename to plugins/woocommerce-admin/packages/components/src/select-control/tags.js
index e47c9b86224..295a3458b5c 100644
--- a/plugins/woocommerce-admin/packages/components/src/autocomplete/tags.js
+++ b/plugins/woocommerce-admin/packages/components/src/select-control/tags.js
@@ -43,7 +43,7 @@ class Tags extends Component {
return null;
}
- const classes = classnames( 'woocommerce-autocomplete__tags', {
+ const classes = classnames( 'woocommerce-select-control__tags', {
'has-clear': showClearButton,
} );
@@ -69,14 +69,12 @@ class Tags extends Component {
/>
);
} ) }
- { showClearButton && }
+ { showClearButton && (
+
+ ) }
);
}
@@ -98,10 +96,7 @@ Tags.propTypes = {
*/
selected: PropTypes.arrayOf(
PropTypes.shape( {
- key: PropTypes.oneOfType( [
- PropTypes.number,
- PropTypes.string,
- ] ).isRequired,
+ key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] ).isRequired,
label: PropTypes.string,
} )
),
diff --git a/plugins/woocommerce-admin/packages/components/src/select-control/test/index.js b/plugins/woocommerce-admin/packages/components/src/select-control/test/index.js
new file mode 100644
index 00000000000..60fde02bdb5
--- /dev/null
+++ b/plugins/woocommerce-admin/packages/components/src/select-control/test/index.js
@@ -0,0 +1,159 @@
+/** @format */
+/**
+ * External dependencies
+ */
+import { mount } from 'enzyme';
+import { Button } from '@wordpress/components';
+
+/**
+ * Internal dependencies
+ */
+import { SelectControl } from '../index';
+
+describe( 'SelectControl', () => {
+ const optionClassname = 'woocommerce-select-control__option';
+ const query = 'lorem';
+ const options = [
+ { key: '1', label: 'lorem 1', value: { id: '1' } },
+ { key: '2', label: 'lorem 2', value: { id: '2' } },
+ { key: '3', label: 'bar', value: { id: '3' } },
+ ];
+
+ it( 'returns all elements', () => {
+ const selectControl = mount( );
+ selectControl.setState( {
+ query,
+ } );
+
+ selectControl.instance().search( query );
+ selectControl.update();
+
+ expect( selectControl.find( Button ).filter( '.' + optionClassname ).length ).toBe( 3 );
+ } );
+
+ it( 'returns matching elements', () => {
+ const selectControl = mount( );
+ selectControl.setState( {
+ query,
+ } );
+
+ selectControl.instance().search( query );
+ selectControl.update();
+
+ expect( selectControl.find( Button ).filter( '.' + optionClassname ).length ).toBe( 2 );
+ } );
+
+ it( "doesn't return matching excluded elements", () => {
+ const selectControl = mount(
+
+ );
+ selectControl.setState( {
+ query,
+ } );
+
+ selectControl.instance().search( query );
+ selectControl.update();
+
+ expect( selectControl.find( Button ).filter( '.' + optionClassname ).length ).toBe( 1 );
+ } );
+
+ it( 'trims spaces from input', () => {
+ const selectControl = mount( );
+ selectControl.setState( {
+ query,
+ } );
+
+ selectControl.instance().search( ' ' + query + ' ' );
+ selectControl.update();
+
+ expect( selectControl.find( Button ).filter( '.' + optionClassname ).length ).toBe( 2 );
+ } );
+
+ it( 'limits results', () => {
+ const selectControl = mount(
+
+ );
+ selectControl.setState( {
+ query,
+ } );
+
+ selectControl.instance().search( query );
+ selectControl.update();
+
+ expect( selectControl.find( Button ).filter( '.' + optionClassname ).length ).toBe( 1 );
+ } );
+
+ it( 'shows options initially', () => {
+ const selectControl = mount( );
+
+ selectControl.instance().search( '' );
+ selectControl.update();
+
+ expect( selectControl.find( Button ).filter( '.' + optionClassname ).length ).toBe( 3 );
+ } );
+
+ it( 'shows options after query', () => {
+ const selectControl = mount(
+
+ );
+
+ selectControl.instance().search( '' );
+ selectControl.update();
+
+ expect( selectControl.find( Button ).filter( '.' + optionClassname ).length ).toBe( 0 );
+
+ selectControl.instance().search( query );
+ selectControl.update();
+
+ expect( selectControl.find( Button ).filter( '.' + optionClassname ).length ).toBe( 2 );
+ } );
+
+ it( 'appends an option after filtering', () => {
+ const selectControl = mount(
+
+ filteredOptions.concat( [ { key: 'new-option', label: 'New options' } ] )
+ }
+ />
+ );
+
+ selectControl.instance().search( query );
+ selectControl.update();
+
+ expect( selectControl.find( Button ).filter( '.' + optionClassname ).length ).toBe( 3 );
+ } );
+
+ it( 'changes the options on search', () => {
+ const queriedOptions = [];
+ const queryOptions = searchedQuery => {
+ if ( searchedQuery === 'test' ) {
+ queriedOptions.push( { key: 'test-option', label: 'Test option' } );
+ }
+ };
+ const selectControl = mount(
+ queriedOptions }
+ />
+ );
+
+ selectControl.instance().search( '' );
+ selectControl.update();
+
+ expect( selectControl.find( Button ).filter( '.' + optionClassname ).length ).toBe( 0 );
+
+ selectControl.instance().search( 'test' );
+ selectControl.update();
+
+ expect( selectControl.find( Button ).filter( '.' + optionClassname ).length ).toBe( 1 );
+ } );
+} );
diff --git a/plugins/woocommerce-admin/packages/components/src/simple-select-control/README.md b/plugins/woocommerce-admin/packages/components/src/simple-select-control/README.md
deleted file mode 100644
index 38a97a9c5d0..00000000000
--- a/plugins/woocommerce-admin/packages/components/src/simple-select-control/README.md
+++ /dev/null
@@ -1,45 +0,0 @@
-SimpleSelectControl
-===
-
-A component for displaying a material styled 'simple' select control.
-
-## Usage
-
-```jsx
-const petOptions = [
- {
- value: 'cat',
- label: 'Cat',
- },
- {
- value: 'dog',
- label: 'Dog',
- },
-];
-
- this.setState( { pet: value } ) }
- options={ petOptions }
- value={ pet }
-/>
-```
-
-### Props
-
-Name | Type | Default | Description
---- | --- | --- | ---
-`className` | String | `null` | Additional class name to style the component
-`label` | String | `null` | A label to use for the main select element
-`options` | Array | `null` | An array of options to use for the dropddown
-`onChange` | Function | `null` | A function that receives the value of the new option that is being selected as input
-`value` | String | `null` | The currently value of the select element
-`help` | One of type: string, node | `null` | If this property is added, a help text will be generated using help property as the content
-
-### `options` structure
-
-The `options` array needs to be composed of objects with properties:
-
-- `value`: String - Input value for this option.
-- `label`: String - Label for this option.
-- `disabled`: Boolean - Disable the option.
\ No newline at end of file
diff --git a/plugins/woocommerce-admin/packages/components/src/simple-select-control/docs/example.js b/plugins/woocommerce-admin/packages/components/src/simple-select-control/docs/example.js
deleted file mode 100644
index 4090f6c8b84..00000000000
--- a/plugins/woocommerce-admin/packages/components/src/simple-select-control/docs/example.js
+++ /dev/null
@@ -1,56 +0,0 @@
-/** @format */
-/**
- * Internal dependencies
- */
-import { SimpleSelectControl } from '@woocommerce/components';
-
-/**
- * External dependencies
- */
-import { Component } from '@wordpress/element';
-
-export default class MySimpleSelectControl extends Component {
- constructor() {
- super();
- this.state = {
- pet: '',
- };
- }
-
- render() {
- const { pet } = this.state;
-
- const petOptions = [
- {
- value: 'cat',
- label: 'Cat',
- },
- {
- value: 'dog',
- label: 'Dog',
- },
- {
- value: 'bunny',
- label: 'Bunny',
- },
- {
- value: 'snake',
- label: 'Snake',
- },
- {
- value: 'chinchilla',
- label: 'Chinchilla',
- disabled: true,
- },
- ];
-
- return (
- this.setState( { pet: value } ) }
- options={ petOptions }
- value={ pet }
- />
- );
- }
-}
diff --git a/plugins/woocommerce-admin/packages/components/src/simple-select-control/index.js b/plugins/woocommerce-admin/packages/components/src/simple-select-control/index.js
deleted file mode 100644
index cd322083c19..00000000000
--- a/plugins/woocommerce-admin/packages/components/src/simple-select-control/index.js
+++ /dev/null
@@ -1,190 +0,0 @@
-/**
- * External dependencies
- */
-import { __, sprintf } from '@wordpress/i18n';
-import { Dropdown, Button, NavigableMenu, withFocusOutside } from '@wordpress/components';
-import { Fragment, Component } from '@wordpress/element';
-import { map, find } from 'lodash';
-import classNames from 'classnames';
-import PropTypes from 'prop-types';
-import { withInstanceId } from '@wordpress/compose';
-
-/**
- * A component for displaying a material styled 'simple' select control.
- */
-class SimpleSelectControl extends Component {
- constructor( props ) {
- super( props );
- this.state = {
- currentValue: props.value,
- isFocused: false,
- };
- }
-
- componentDidUpdate( prevProps ) {
- if (
- this.props.value !== prevProps.value &&
- this.state.currentValue !== this.props.value
- ) {
- /* eslint-disable react/no-did-update-set-state */
- this.setState( {
- currentValue: this.props.value,
- } );
- /* eslint-enable react/no-did-update-set-state */
- }
- }
-
- handleFocusOutside() {
- this.setState( { isFocused: false } );
- }
-
- handleOnClick( onToggle ) {
- this.setState( { isFocused: true } );
- if ( 'function' === typeof onToggle ) {
- onToggle();
- }
- const { onClick } = this.props;
- if ( 'function' === typeof onClick ) {
- onClick();
- }
- }
-
- handleOnFocus() {
- this.setState( { isFocused: true } );
- const { onFocus } = this.props;
- if ( 'function' === typeof onFocus ) {
- onFocus();
- }
- }
-
- onChange( value ) {
- this.props.onChange( value );
- this.setState( { currentValue: value } );
- }
-
- render() {
- const { options, label, className, instanceId, help } = this.props;
- const { currentValue, isFocused } = this.state;
- const onChange = ( value ) => {
- this.onChange( value );
- this.handleFocusOutside();
- };
-
- const isEmpty = currentValue === '' || currentValue === null;
- const currentOption = find( options, ( { value } ) => value === currentValue );
-
- const id = `simple-select-control-${ instanceId }`;
-
- return (
- (
-
-
- { !! help && { help }
}
-
- ) }
- renderContent={ ( { onClose } ) => (
-
- { map( options, ( option ) => {
- const optionValue = option.value;
- const optionLabel = option.label;
- const optionDisabled = option.disabled || false;
- const isSelected = ( currentValue === optionValue );
- return (
-
- );
- } ) }
-
- ) }
- />
- );
- }
-}
-
-SimpleSelectControl.propTypes = {
- /**
- * Additional class name to style the component.
- */
- className: PropTypes.string,
- /**
- * A label to use for the main select element.
- */
- label: PropTypes.string,
- /**
- * An array of options to use for the dropddown.
- */
- options: PropTypes.arrayOf(
- PropTypes.shape( {
- /**
- * Input value for this option.
- */
- value: PropTypes.string,
- /**
- * Label for this option.
- */
- label: PropTypes.string,
- /**
- * Disable the option.
- */
- disabled: PropTypes.bool,
- } )
- ),
- /**
- * A function that receives the value of the new option that is being selected as input.
- */
- onChange: PropTypes.func,
- /**
- * The currently value of the select element.
- */
- value: PropTypes.string,
- /**
- * If this property is added, a help text will be generated using help property as the content.
- */
- help: PropTypes.oneOfType( [
- PropTypes.string,
- PropTypes.node,
- ] ),
-};
-
-export default withFocusOutside( withInstanceId( SimpleSelectControl ) );
diff --git a/plugins/woocommerce-admin/packages/components/src/simple-select-control/style.scss b/plugins/woocommerce-admin/packages/components/src/simple-select-control/style.scss
deleted file mode 100644
index f35519070c2..00000000000
--- a/plugins/woocommerce-admin/packages/components/src/simple-select-control/style.scss
+++ /dev/null
@@ -1,125 +0,0 @@
-.woocommerce-simple-select-control__dropdown {
- position: relative;
- width: 100%;
- height: 56px;
- border: 1px solid #b0b5b8;
- box-shadow: none;
- box-sizing: border-box;
- border-radius: 3px;
- background: $studio-white;
- font-size: 16px;
- line-height: 54px;
- font-weight: 400;
- margin-top: $gap;
- margin-bottom: $gap;
-
- .woocommerce-simple-select-control__selector {
- &.components-button {
- border: 0;
- background: transparent;
- box-shadow: none;
- justify-content: unset;
- margin-left: 0;
- position: absolute;
- top: 0;
- height: 100%;
- width: 100%;
- left: 0;
- padding-left: $gap;
- padding-right: $gap-largest;
-
- &:focus:not(:disabled) {
- box-shadow: none;
- outline: none;
- background-color: transparent;
- outline-offset: initial;
- }
-
- &::after {
- display: block;
- pointer-events: none;
- position: absolute;
- float: right;
- line-height: 56px;
- font-family: dashicons, sans-serif;
- font-size: 20px;
- content: '\f140';
- z-index: 101;
- height: 24px;
- width: 24px;
- margin-top: 0;
- top: 0;
- right: $gap;
- bottom: 16px;
- color: $studio-black;
- }
- }
- }
-
- .woocommerce-simple-select-control__dropdown-content {
- &.components-popover.is-bottom {
- margin-top: -$gap-largest * 2;
- z-index: 999;
-
- &::before,
- &::after {
- display: none;
- }
- }
-
- .components-button {
- display: block;
- position: relative;
- padding: 10px 20px 10px 40px;
- width: 100%;
- text-align: left;
-
- &.is-selected {
- background: $studio-gray-100;
- }
- }
- }
-
- &.is-empty {
- .woocommerce-simple-select-control__selector {
- &.components-button {
- font-size: 16px;
- line-height: 54px;
- height: 54px;
- z-index: 100;
- color: #636d75;
- }
- }
- }
-
- &.is-active {
- box-shadow: 0 0 0 2px #673d99;
- border-color: transparent;
- }
-
- &.has-value {
- .woocommerce-simple-select-control__selector {
- display: flex;
- flex-direction: column;
- &.components-button {
- &::after {
- bottom: 23px;
- }
- }
- }
-
- .woocommerce-simple-select-control__label {
- margin-top: -$gap-smallest;
- font-size: 12px;
- line-height: 16px;
- margin-top: 8px;
- color: #636d75;
- }
-
- .woocommerce-simple-select-control__value {
- color: #2b2d2f;
- font-size: 16px;
- white-space: nowrap;
- }
- }
-}
diff --git a/plugins/woocommerce-admin/packages/components/src/style.scss b/plugins/woocommerce-admin/packages/components/src/style.scss
index 48c6a40f8f3..e7256d57564 100644
--- a/plugins/woocommerce-admin/packages/components/src/style.scss
+++ b/plugins/woocommerce-admin/packages/components/src/style.scss
@@ -2,7 +2,6 @@
* Internal Dependencies
*/
@import 'animation-slider/style.scss';
-@import 'autocomplete/style.scss';
@import 'calendar/style.scss';
@import 'card/style.scss';
@import 'chart/style.scss';
@@ -27,7 +26,7 @@
@import 'search-list-control/style.scss';
@import 'section-header/style.scss';
@import 'segmented-selection/style.scss';
-@import 'simple-select-control/style.scss';
+@import 'select-control/style.scss';
@import 'split-button/style.scss';
@import 'stepper/style.scss';
@import 'spinner/style.scss';