Replace select controls with new SelectControl (Autocomplete) component (https://github.com/woocommerce/woocommerce-admin/pull/2997)
* Rename Autocomplete component to SelectControl * Add isSearchable prop to SelectControl * Remove SimpleSelectControl component * Refactor list expansion and key behavior * Bump changelog and version
This commit is contained in:
parent
19a69b7789
commit
1563971836
|
@ -4,10 +4,15 @@
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { decodeEntities } from '@wordpress/html-entities';
|
import { decodeEntities } from '@wordpress/html-entities';
|
||||||
import { SelectControl, TextControl } from 'newspack-components';
|
import { TextControl } from 'newspack-components';
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { getSetting } from '@woocommerce/wc-admin-settings';
|
import { getSetting } from '@woocommerce/wc-admin-settings';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Internal depdencies
|
||||||
|
*/
|
||||||
|
import { SelectControl } from '@woocommerce/components';
|
||||||
|
|
||||||
const { countries } = getSetting( 'dataEndpoints', { countries: {} } );
|
const { countries } = getSetting( 'dataEndpoints', { countries: {} } );
|
||||||
/**
|
/**
|
||||||
* Form validation.
|
* Form validation.
|
||||||
|
@ -43,7 +48,7 @@ export function getCountryStateOptions() {
|
||||||
const countryStateOptions = countries.reduce( ( acc, country ) => {
|
const countryStateOptions = countries.reduce( ( acc, country ) => {
|
||||||
if ( ! country.states.length ) {
|
if ( ! country.states.length ) {
|
||||||
acc.push( {
|
acc.push( {
|
||||||
value: country.code,
|
key: country.code,
|
||||||
label: decodeEntities( country.name ),
|
label: decodeEntities( country.name ),
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
@ -52,7 +57,7 @@ export function getCountryStateOptions() {
|
||||||
|
|
||||||
const countryStates = country.states.map( state => {
|
const countryStates = country.states.map( state => {
|
||||||
return {
|
return {
|
||||||
value: country.code + ':' + state.code,
|
key: country.code + ':' + state.code,
|
||||||
label: decodeEntities( country.name ) + ' -- ' + decodeEntities( state.name ),
|
label: decodeEntities( country.name ) + ' -- ' + decodeEntities( state.name ),
|
||||||
};
|
};
|
||||||
} );
|
} );
|
||||||
|
@ -62,8 +67,6 @@ export function getCountryStateOptions() {
|
||||||
return acc;
|
return acc;
|
||||||
}, [] );
|
}, [] );
|
||||||
|
|
||||||
countryStateOptions.unshift( { value: '', label: '' } );
|
|
||||||
|
|
||||||
return countryStateOptions;
|
return countryStateOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,6 +98,7 @@ export function StoreAddress( props ) {
|
||||||
label={ __( 'Country / State', 'woocommerce-admin' ) }
|
label={ __( 'Country / State', 'woocommerce-admin' ) }
|
||||||
required
|
required
|
||||||
options={ countryStateOptions }
|
options={ countryStateOptions }
|
||||||
|
isSearchable
|
||||||
{ ...getInputProps( 'countryState' ) }
|
{ ...getInputProps( 'countryState' ) }
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ import { getSetting, CURRENCY as currency } from '@woocommerce/wc-admin-settings
|
||||||
/**
|
/**
|
||||||
* Internal dependencies
|
* 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 withSelect from 'wc-api/with-select';
|
||||||
import { recordEvent } from 'lib/tracks';
|
import { recordEvent } from 'lib/tracks';
|
||||||
import { formatCurrency } from '@woocommerce/currency';
|
import { formatCurrency } from '@woocommerce/currency';
|
||||||
|
@ -200,52 +200,52 @@ class BusinessDetails extends Component {
|
||||||
render() {
|
render() {
|
||||||
const productCountOptions = [
|
const productCountOptions = [
|
||||||
{
|
{
|
||||||
value: '1-10',
|
key: '1-10',
|
||||||
label: this.getNumberRangeString( 1, 10 ),
|
label: this.getNumberRangeString( 1, 10 ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '11-100',
|
key: '11-100',
|
||||||
label: this.getNumberRangeString( 11, 100 ),
|
label: this.getNumberRangeString( 11, 100 ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '101-1000',
|
key: '101-1000',
|
||||||
label: this.getNumberRangeString( 101, 1000 ),
|
label: this.getNumberRangeString( 101, 1000 ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '1000+',
|
key: '1000+',
|
||||||
label: this.getNumberRangeString( 1000 ),
|
label: this.getNumberRangeString( 1000 ),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const revenueOptions = [
|
const revenueOptions = [
|
||||||
{
|
{
|
||||||
value: 'none',
|
key: 'none',
|
||||||
label: sprintf(
|
label: sprintf(
|
||||||
_x( "%s (I'm just getting started)", '$0 revenue amount', 'woocommerce-admin' ),
|
_x( "%s (I'm just getting started)", '$0 revenue amount', 'woocommerce-admin' ),
|
||||||
formatCurrency( 0 )
|
formatCurrency( 0 )
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'up-to-2500',
|
key: 'up-to-2500',
|
||||||
label: sprintf(
|
label: sprintf(
|
||||||
_x( 'Up to %s', 'Up to a certain revenue amount', 'woocommerce-admin' ),
|
_x( 'Up to %s', 'Up to a certain revenue amount', 'woocommerce-admin' ),
|
||||||
formatCurrency( 2500 )
|
formatCurrency( 2500 )
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '2500-10000',
|
key: '2500-10000',
|
||||||
label: this.getNumberRangeString( 2500, 10000, formatCurrency ),
|
label: this.getNumberRangeString( 2500, 10000, formatCurrency ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '10000-50000',
|
key: '10000-50000',
|
||||||
label: this.getNumberRangeString( 10000, 50000, formatCurrency ),
|
label: this.getNumberRangeString( 10000, 50000, formatCurrency ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: '50000-250000',
|
key: '50000-250000',
|
||||||
label: this.getNumberRangeString( 50000, 250000, formatCurrency ),
|
label: this.getNumberRangeString( 50000, 250000, formatCurrency ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'more-than-250000',
|
key: 'more-than-250000',
|
||||||
label: sprintf(
|
label: sprintf(
|
||||||
_x( 'More than %s', 'More than a certain revenue amount', 'woocommerce-admin' ),
|
_x( 'More than %s', 'More than a certain revenue amount', 'woocommerce-admin' ),
|
||||||
formatCurrency( 250000 )
|
formatCurrency( 250000 )
|
||||||
|
@ -255,19 +255,19 @@ class BusinessDetails extends Component {
|
||||||
|
|
||||||
const sellingVenueOptions = [
|
const sellingVenueOptions = [
|
||||||
{
|
{
|
||||||
value: 'no',
|
key: 'no',
|
||||||
label: __( 'No', 'woocommerce-admin' ),
|
label: __( 'No', 'woocommerce-admin' ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'other',
|
key: 'other',
|
||||||
label: __( 'Yes, on another platform', 'woocommerce-admin' ),
|
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' ),
|
label: __( 'Yes, in person at physical stores and/or events', 'woocommerce-admin' ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'brick-mortar-other',
|
key: 'brick-mortar-other',
|
||||||
label: __(
|
label: __(
|
||||||
'Yes, on another platform and in person at physical stores and/or events',
|
'Yes, on another platform and in person at physical stores and/or events',
|
||||||
'woocommerce-admin'
|
'woocommerce-admin'
|
||||||
|
@ -277,23 +277,23 @@ class BusinessDetails extends Component {
|
||||||
|
|
||||||
const otherPlatformOptions = [
|
const otherPlatformOptions = [
|
||||||
{
|
{
|
||||||
value: 'shopify',
|
key: 'shopify',
|
||||||
label: __( 'Shopify', 'woocommerce-admin' ),
|
label: __( 'Shopify', 'woocommerce-admin' ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'bigcommerce',
|
key: 'bigcommerce',
|
||||||
label: __( 'BigCommerce', 'woocommerce-admin' ),
|
label: __( 'BigCommerce', 'woocommerce-admin' ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'magento',
|
key: 'magento',
|
||||||
label: __( 'Magento', 'woocommerce-admin' ),
|
label: __( 'Magento', 'woocommerce-admin' ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'wix',
|
key: 'wix',
|
||||||
label: __( 'Wix', 'woocommerce-admin' ),
|
label: __( 'Wix', 'woocommerce-admin' ),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: 'other',
|
key: 'other',
|
||||||
label: __( 'Other', 'woocommerce-admin' ),
|
label: __( 'Other', 'woocommerce-admin' ),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
@ -320,14 +320,14 @@ class BusinessDetails extends Component {
|
||||||
</p>
|
</p>
|
||||||
<Card>
|
<Card>
|
||||||
<Fragment>
|
<Fragment>
|
||||||
<SimpleSelectControl
|
<SelectControl
|
||||||
label={ __( 'How many products do you plan to add?', 'woocommerce-admin' ) }
|
label={ __( 'How many products do you plan to add?', 'woocommerce-admin' ) }
|
||||||
options={ productCountOptions }
|
options={ productCountOptions }
|
||||||
required
|
required
|
||||||
{ ...getInputProps( 'product_count' ) }
|
{ ...getInputProps( 'product_count' ) }
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SimpleSelectControl
|
<SelectControl
|
||||||
label={ __( 'Currently selling elsewhere?', 'woocommerce-admin' ) }
|
label={ __( 'Currently selling elsewhere?', 'woocommerce-admin' ) }
|
||||||
options={ sellingVenueOptions }
|
options={ sellingVenueOptions }
|
||||||
required
|
required
|
||||||
|
@ -337,7 +337,7 @@ class BusinessDetails extends Component {
|
||||||
{ [ 'other', 'brick-mortar', 'brick-mortar-other' ].includes(
|
{ [ 'other', 'brick-mortar', 'brick-mortar-other' ].includes(
|
||||||
values.selling_venues
|
values.selling_venues
|
||||||
) && (
|
) && (
|
||||||
<SimpleSelectControl
|
<SelectControl
|
||||||
label={ __( "What's your current annual revenue?", 'woocommerce-admin' ) }
|
label={ __( "What's your current annual revenue?", 'woocommerce-admin' ) }
|
||||||
options={ revenueOptions }
|
options={ revenueOptions }
|
||||||
required
|
required
|
||||||
|
@ -346,7 +346,7 @@ class BusinessDetails extends Component {
|
||||||
) }
|
) }
|
||||||
|
|
||||||
{ [ 'other', 'brick-mortar-other' ].includes( values.selling_venues ) && (
|
{ [ 'other', 'brick-mortar-other' ].includes( values.selling_venues ) && (
|
||||||
<SimpleSelectControl
|
<SelectControl
|
||||||
label={ __( 'Which platform is the store using?', 'woocommerce-admin' ) }
|
label={ __( 'Which platform is the store using?', 'woocommerce-admin' ) }
|
||||||
options={ otherPlatformOptions }
|
options={ otherPlatformOptions }
|
||||||
required
|
required
|
||||||
|
|
|
@ -263,6 +263,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.woocommerce-select-control__control {
|
||||||
|
margin: $gap 0;
|
||||||
|
padding-right: $gap + 24px;
|
||||||
|
|
||||||
|
&.is-active {
|
||||||
|
box-shadow: 0 0 0 1px $studio-woocommerce-purple-60;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
display: block;
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: pointer;
|
||||||
|
position: absolute;
|
||||||
|
float: right;
|
||||||
|
line-height: 56px;
|
||||||
|
font-family: dashicons, sans-serif;
|
||||||
|
font-size: 20px;
|
||||||
|
content: '\f140';
|
||||||
|
z-index: 1;
|
||||||
|
height: 24px;
|
||||||
|
width: 24px;
|
||||||
|
margin-top: 0;
|
||||||
|
top: 0;
|
||||||
|
right: $gap;
|
||||||
|
bottom: $gap;
|
||||||
|
color: $studio-black;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#wpadminbar {
|
#wpadminbar {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
[
|
[
|
||||||
{ "component": "AdvancedFilters" },
|
{ "component": "AdvancedFilters" },
|
||||||
{ "component": "AnimationSlider" },
|
{ "component": "AnimationSlider" },
|
||||||
{ "component": "Autocomplete" },
|
|
||||||
{ "component": "Calendar" },
|
{ "component": "Calendar" },
|
||||||
{ "component": "Card" },
|
{ "component": "Card" },
|
||||||
{ "component": "Chart" },
|
{ "component": "Chart" },
|
||||||
|
@ -28,7 +27,7 @@
|
||||||
{ "component": "SearchListControl" },
|
{ "component": "SearchListControl" },
|
||||||
{ "component": "Section" },
|
{ "component": "Section" },
|
||||||
{ "component": "SegmentedSelection" },
|
{ "component": "SegmentedSelection" },
|
||||||
{ "component": "SimpleSelectControl" },
|
{ "component": "SelectControl" },
|
||||||
{ "component": "Spinner" },
|
{ "component": "Spinner" },
|
||||||
{ "component": "SplitButton" },
|
{ "component": "SplitButton" },
|
||||||
{ "component": "Stepper" },
|
{ "component": "Stepper" },
|
||||||
|
|
|
@ -10,7 +10,6 @@
|
||||||
* [Package components](components/packages/)
|
* [Package components](components/packages/)
|
||||||
* [AdvancedFilters](components/packages/advanced-filters/README.md)
|
* [AdvancedFilters](components/packages/advanced-filters/README.md)
|
||||||
* [AnimationSlider](components/packages/animation-slider/README.md)
|
* [AnimationSlider](components/packages/animation-slider/README.md)
|
||||||
* [Autocomplete](components/packages/autocomplete/README.md)
|
|
||||||
* [Calendar](components/packages/calendar/README.md)
|
* [Calendar](components/packages/calendar/README.md)
|
||||||
* [Card](components/packages/card/README.md)
|
* [Card](components/packages/card/README.md)
|
||||||
* [Chart](components/packages/chart/README.md)
|
* [Chart](components/packages/chart/README.md)
|
||||||
|
@ -39,7 +38,7 @@
|
||||||
* [SectionHeader](components/packages/section-header/README.md)
|
* [SectionHeader](components/packages/section-header/README.md)
|
||||||
* [Section](components/packages/section/README.md)
|
* [Section](components/packages/section/README.md)
|
||||||
* [SegmentedSelection](components/packages/segmented-selection/README.md)
|
* [SegmentedSelection](components/packages/segmented-selection/README.md)
|
||||||
* [SimpleSelectControl](components/packages/simple-select-control/README.md)
|
* [SelectControl](components/packages/select-control/README.md)
|
||||||
* [Spinner](components/packages/spinner/README.md)
|
* [Spinner](components/packages/spinner/README.md)
|
||||||
* [SplitButton](components/packages/split-button/README.md)
|
* [SplitButton](components/packages/split-button/README.md)
|
||||||
* [Stepper](components/packages/stepper/README.md)
|
* [Stepper](components/packages/stepper/README.md)
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
# 4.1.0 (Unreleased)
|
||||||
|
- Renamed the `<Autocomplete />` component to `<SelectControl />`.
|
||||||
|
- Added `isSearchable` prop to `<SelectControl />` to allow simple select dropdowns.
|
||||||
|
- Removed the `<SimpleSelectControl />` component.
|
||||||
|
|
||||||
# 4.0.0
|
# 4.0.0
|
||||||
- Added a new `<ScrollTo />` component.
|
- Added a new `<ScrollTo />` component.
|
||||||
- Changed the `<List />` `description` prop to `content` and allowed content nodes to be passed in addition to strings.
|
- Changed the `<List />` `description` prop to `content` and allowed content nodes to be passed in addition to strings.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@woocommerce/components",
|
"name": "@woocommerce/components",
|
||||||
"version": "3.2.0",
|
"version": "4.1.0",
|
||||||
"description": "UI components for WooCommerce.",
|
"description": "UI components for WooCommerce.",
|
||||||
"author": "Automattic",
|
"author": "Automattic",
|
||||||
"license": "GPL-3.0-or-later",
|
"license": "GPL-3.0-or-later",
|
||||||
|
|
|
@ -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' },
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
<Autocomplete
|
|
||||||
label="Single value"
|
|
||||||
onChange={ ( selected ) => 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
|
|
|
@ -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 } ) => (
|
|
||||||
<div>
|
|
||||||
<Autocomplete
|
|
||||||
label="Single value"
|
|
||||||
onChange={ ( selected ) => setState( { singleSelected: selected } ) }
|
|
||||||
options={ options }
|
|
||||||
placeholder="Start typing to filter options..."
|
|
||||||
selected={ singleSelected }
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<Autocomplete
|
|
||||||
label="Inline tags"
|
|
||||||
multiple
|
|
||||||
inlineTags
|
|
||||||
onChange={ ( selected ) => setState( { inlineSelected: selected } ) }
|
|
||||||
options={ options }
|
|
||||||
placeholder="Start typing to filter options..."
|
|
||||||
selected={ inlineSelected }
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<Autocomplete
|
|
||||||
hideBeforeSearch
|
|
||||||
label="Hidden options before search"
|
|
||||||
multiple
|
|
||||||
onChange={ ( selected ) => setState( { multipleSelected: selected } ) }
|
|
||||||
options={ options }
|
|
||||||
placeholder="Start typing to filter options..."
|
|
||||||
selected={ multipleSelected }
|
|
||||||
showClearButton
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
) );
|
|
|
@ -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
|
|
||||||
options={ options }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
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
|
|
||||||
options={ options }
|
|
||||||
selected={ [ options[ 1 ] ] }
|
|
||||||
excludeSelectedOptions
|
|
||||||
multiple
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
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
|
|
||||||
options={ options }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
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
|
|
||||||
options={ options }
|
|
||||||
maxResults={ 1 }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
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
|
|
||||||
options={ options }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
autocomplete.instance().search( '' );
|
|
||||||
autocomplete.update();
|
|
||||||
|
|
||||||
expect( autocomplete.find( Button ).filter( '.' + optionClassname ).length ).toBe( 3 );
|
|
||||||
} );
|
|
||||||
|
|
||||||
it( 'shows options after query', () => {
|
|
||||||
const autocomplete = mount(
|
|
||||||
<Autocomplete
|
|
||||||
options={ options }
|
|
||||||
hideBeforeSearch
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
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(
|
|
||||||
<Autocomplete
|
|
||||||
options={ options }
|
|
||||||
onFilter={ ( filteredOptions ) => 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(
|
|
||||||
<Autocomplete
|
|
||||||
options={ queriedOptions }
|
|
||||||
onSearch={ queryOptions }
|
|
||||||
onFilter={ () => 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 );
|
|
||||||
} );
|
|
||||||
} );
|
|
|
@ -7,7 +7,6 @@ import 'react-dates/initialize';
|
||||||
|
|
||||||
export { default as AdvancedFilters } from './advanced-filters';
|
export { default as AdvancedFilters } from './advanced-filters';
|
||||||
export { default as AnimationSlider } from './animation-slider';
|
export { default as AnimationSlider } from './animation-slider';
|
||||||
export { default as Autocomplete } from './autocomplete';
|
|
||||||
export { default as Chart } from './chart';
|
export { default as Chart } from './chart';
|
||||||
export { default as ChartPlaceholder } from './chart/placeholder';
|
export { default as ChartPlaceholder } from './chart/placeholder';
|
||||||
export { default as Card } from './card';
|
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 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 ScrollTo } from './scroll-to';
|
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 SplitButton } from './split-button';
|
||||||
export { default as Spinner } from './spinner';
|
export { default as Spinner } from './spinner';
|
||||||
export { default as Stepper } from './stepper';
|
export { default as Stepper } from './stepper';
|
||||||
|
|
|
@ -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' },
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
<SelectControl
|
||||||
|
label="Single value"
|
||||||
|
onChange={ selected => 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 |
|
|
@ -3,7 +3,7 @@
|
||||||
* External dependencies
|
* External dependencies
|
||||||
*/
|
*/
|
||||||
import { __ } from '@wordpress/i18n';
|
import { __ } from '@wordpress/i18n';
|
||||||
import { BACKSPACE } from '@wordpress/keycodes';
|
import { BACKSPACE, DOWN, UP } from '@wordpress/keycodes';
|
||||||
import { Component, createRef } from '@wordpress/element';
|
import { Component, createRef } from '@wordpress/element';
|
||||||
import classnames from 'classnames';
|
import classnames from 'classnames';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
@ -16,7 +16,7 @@ import Tags from './tags';
|
||||||
/**
|
/**
|
||||||
* A search control to allow user input to filter the options.
|
* A search control to allow user input to filter the options.
|
||||||
*/
|
*/
|
||||||
class SearchControl extends Component {
|
class Control extends Component {
|
||||||
constructor( props ) {
|
constructor( props ) {
|
||||||
super( props );
|
super( props );
|
||||||
this.state = {
|
this.state = {
|
||||||
|
@ -38,9 +38,15 @@ class SearchControl extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onFocus( onSearch ) {
|
onFocus( onSearch ) {
|
||||||
|
const { isSearchable, setExpanded } = this.props;
|
||||||
|
|
||||||
return event => {
|
return event => {
|
||||||
this.setState( { isActive: true } );
|
this.setState( { isActive: true } );
|
||||||
|
if ( isSearchable ) {
|
||||||
onSearch( event.target.value );
|
onSearch( event.target.value );
|
||||||
|
} else {
|
||||||
|
setExpanded( true );
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,11 +55,42 @@ class SearchControl extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
onKeyDown( event ) {
|
onKeyDown( event ) {
|
||||||
const { selected, onChange, query } = this.props;
|
const {
|
||||||
|
decrementSelectedIndex,
|
||||||
|
incrementSelectedIndex,
|
||||||
|
selected,
|
||||||
|
onChange,
|
||||||
|
query,
|
||||||
|
setExpanded,
|
||||||
|
} = this.props;
|
||||||
|
|
||||||
if ( BACKSPACE === event.keyCode && ! query && selected.length ) {
|
if ( BACKSPACE === event.keyCode && ! query && selected.length ) {
|
||||||
onChange( [ ...selected.slice( 0, -1 ) ] );
|
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 <div className="woocommerce-select-control__control-value">{ selected[ 0 ].label }</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderInput() {
|
renderInput() {
|
||||||
|
@ -63,19 +100,20 @@ class SearchControl extends Component {
|
||||||
inlineTags,
|
inlineTags,
|
||||||
instanceId,
|
instanceId,
|
||||||
isExpanded,
|
isExpanded,
|
||||||
|
isSearchable,
|
||||||
listboxId,
|
listboxId,
|
||||||
onSearch,
|
onSearch,
|
||||||
placeholder,
|
placeholder,
|
||||||
query,
|
|
||||||
} = this.props;
|
} = this.props;
|
||||||
const { isActive } = this.state;
|
const { isActive } = this.state;
|
||||||
|
|
||||||
return <input
|
return (
|
||||||
className="woocommerce-autocomplete__control-input"
|
<input
|
||||||
id={ `woocommerce-autocomplete-${ instanceId }__control-input` }
|
className="woocommerce-select-control__control-input"
|
||||||
|
id={ `woocommerce-select-control-${ instanceId }__control-input` }
|
||||||
ref={ this.input }
|
ref={ this.input }
|
||||||
type={ 'search' }
|
type={ isSearchable ? 'search' : 'button' }
|
||||||
value={ query }
|
value={ this.getInputValue() }
|
||||||
placeholder={ isActive ? placeholder : '' }
|
placeholder={ isActive ? placeholder : '' }
|
||||||
onChange={ this.updateSearch( onSearch ) }
|
onChange={ this.updateSearch( onSearch ) }
|
||||||
onFocus={ this.onFocus( onSearch ) }
|
onFocus={ this.onFocus( onSearch ) }
|
||||||
|
@ -88,21 +126,24 @@ class SearchControl extends Component {
|
||||||
aria-owns={ listboxId }
|
aria-owns={ listboxId }
|
||||||
aria-controls={ listboxId }
|
aria-controls={ listboxId }
|
||||||
aria-activedescendant={ activeId }
|
aria-activedescendant={ activeId }
|
||||||
aria-describedby={
|
aria-describedby={ hasTags && inlineTags ? `search-inline-input-${ instanceId }` : null }
|
||||||
hasTags && inlineTags ? `search-inline-input-${ instanceId }` : null
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
/>;
|
|
||||||
|
getInputValue() {
|
||||||
|
const { isSearchable, multiple, query, selected } = this.props;
|
||||||
|
const selectedValue = selected.length ? selected[ 0 ].label : '';
|
||||||
|
|
||||||
|
if ( ! isSearchable && multiple ) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSearchable ? query : selectedValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { hasTags, help, inlineTags, instanceId, isSearchable, label, query } = this.props;
|
||||||
hasTags,
|
|
||||||
help,
|
|
||||||
inlineTags,
|
|
||||||
instanceId,
|
|
||||||
label,
|
|
||||||
query,
|
|
||||||
} = this.props;
|
|
||||||
const { isActive } = this.state;
|
const { isActive } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
@ -113,40 +154,42 @@ class SearchControl extends Component {
|
||||||
// for the benefit of sighted users.
|
// for the benefit of sighted users.
|
||||||
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
||||||
<div
|
<div
|
||||||
className={ classnames( 'components-base-control', 'woocommerce-autocomplete__control', {
|
className={ classnames( 'components-base-control', 'woocommerce-select-control__control', {
|
||||||
empty: ! query.length,
|
empty: ! query.length,
|
||||||
'is-active': isActive,
|
'is-active': isActive,
|
||||||
'has-tags': inlineTags && hasTags,
|
'has-tags': inlineTags && hasTags,
|
||||||
'with-value': query.length,
|
'with-value': this.getInputValue().length,
|
||||||
} ) }
|
} ) }
|
||||||
onClick={ () => {
|
onClick={ () => {
|
||||||
this.input.current.focus();
|
this.input.current.focus();
|
||||||
} }
|
} }
|
||||||
>
|
>
|
||||||
<i className="material-icons-outlined">search</i>
|
{ isSearchable && <i className="material-icons-outlined">search</i> }
|
||||||
{ inlineTags && <Tags { ...this.props } /> }
|
{ inlineTags && <Tags { ...this.props } /> }
|
||||||
|
|
||||||
<div className="components-base-control__field">
|
<div className="components-base-control__field">
|
||||||
{ !! label &&
|
{ !! label && (
|
||||||
<label
|
<label
|
||||||
htmlFor={ `woocommerce-autocomplete-${ instanceId }__control-input` }
|
htmlFor={ `woocommerce-select-control-${ instanceId }__control-input` }
|
||||||
className="components-base-control__label"
|
className="components-base-control__label"
|
||||||
>
|
>
|
||||||
{ label }
|
{ label }
|
||||||
</label>
|
</label>
|
||||||
}
|
) }
|
||||||
{ this.renderInput() }
|
{ this.renderInput() }
|
||||||
{ inlineTags && <span id={ `search-inline-input-${ instanceId }` } className="screen-reader-text">
|
{ inlineTags && (
|
||||||
|
<span id={ `search-inline-input-${ instanceId }` } className="screen-reader-text">
|
||||||
{ __( 'Move backward for selected items', 'woocommerce-admin' ) }
|
{ __( 'Move backward for selected items', 'woocommerce-admin' ) }
|
||||||
</span> }
|
</span>
|
||||||
{ !! help &&
|
) }
|
||||||
|
{ !! help && (
|
||||||
<p
|
<p
|
||||||
id={ `woocommerce-autocomplete-${ instanceId }__help` }
|
id={ `woocommerce-select-control-${ instanceId }__help` }
|
||||||
className="components-base-control__help"
|
className="components-base-control__help"
|
||||||
>
|
>
|
||||||
{ help }
|
{ help }
|
||||||
</p>
|
</p>
|
||||||
}
|
) }
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
|
/* 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.
|
* Bool to determine if tags should be rendered.
|
||||||
*/
|
*/
|
||||||
|
@ -162,16 +205,17 @@ SearchControl.propTypes = {
|
||||||
/**
|
/**
|
||||||
* Help text to be appended beneath the input.
|
* Help text to be appended beneath the input.
|
||||||
*/
|
*/
|
||||||
help: PropTypes.oneOfType( [
|
help: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.node,
|
|
||||||
] ),
|
|
||||||
/**
|
/**
|
||||||
* Render tags inside input, otherwise render below input.
|
* Render tags inside input, otherwise render below input.
|
||||||
*/
|
*/
|
||||||
inlineTags: PropTypes.bool,
|
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,
|
instanceId: PropTypes.number,
|
||||||
/**
|
/**
|
||||||
|
@ -205,13 +249,10 @@ SearchControl.propTypes = {
|
||||||
*/
|
*/
|
||||||
selected: PropTypes.arrayOf(
|
selected: PropTypes.arrayOf(
|
||||||
PropTypes.shape( {
|
PropTypes.shape( {
|
||||||
key: PropTypes.oneOfType( [
|
key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] ).isRequired,
|
||||||
PropTypes.number,
|
|
||||||
PropTypes.string,
|
|
||||||
] ).isRequired,
|
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
} )
|
} )
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default SearchControl;
|
export default Control;
|
|
@ -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,
|
||||||
|
} ) => (
|
||||||
|
<div>
|
||||||
|
<SelectControl
|
||||||
|
label="Simple single value"
|
||||||
|
onChange={ selected => setState( { simpleSelected: selected } ) }
|
||||||
|
options={ options }
|
||||||
|
placeholder="Start typing to filter options..."
|
||||||
|
selected={ simpleSelected }
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<SelectControl
|
||||||
|
label="Multiple values"
|
||||||
|
multiple
|
||||||
|
onChange={ selected => setState( { simpleMultipleSelected: selected } ) }
|
||||||
|
options={ options }
|
||||||
|
placeholder="Start typing to filter options..."
|
||||||
|
selected={ simpleMultipleSelected }
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<SelectControl
|
||||||
|
label="Single value searchable"
|
||||||
|
isSearchable
|
||||||
|
onChange={ selected => setState( { singleSelected: selected } ) }
|
||||||
|
options={ options }
|
||||||
|
placeholder="Start typing to filter options..."
|
||||||
|
selected={ singleSelected }
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<SelectControl
|
||||||
|
label="Inline tags searchable"
|
||||||
|
isSearchable
|
||||||
|
multiple
|
||||||
|
inlineTags
|
||||||
|
onChange={ selected => setState( { inlineSelected: selected } ) }
|
||||||
|
options={ options }
|
||||||
|
placeholder="Start typing to filter options..."
|
||||||
|
selected={ inlineSelected }
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<SelectControl
|
||||||
|
hideBeforeSearch
|
||||||
|
isSearchable
|
||||||
|
label="Hidden options before search"
|
||||||
|
multiple
|
||||||
|
onChange={ selected => setState( { multipleSelected: selected } ) }
|
||||||
|
options={ options }
|
||||||
|
placeholder="Start typing to filter options..."
|
||||||
|
selected={ multipleSelected }
|
||||||
|
showClearButton
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
);
|
|
@ -15,36 +15,41 @@ import { withInstanceId, compose } from '@wordpress/compose';
|
||||||
*/
|
*/
|
||||||
import List from './list';
|
import List from './list';
|
||||||
import Tags from './tags';
|
import Tags from './tags';
|
||||||
import SearchControl from './control';
|
import Control from './control';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A search box which filters options while typing,
|
* A search box which filters options while typing,
|
||||||
* allowing a user to select from an option from a filtered list.
|
* allowing a user to select from an option from a filtered list.
|
||||||
*/
|
*/
|
||||||
export class Autocomplete extends Component {
|
export class SelectControl extends Component {
|
||||||
static getInitialState() {
|
static getInitialState() {
|
||||||
return {
|
return {
|
||||||
filteredOptions: [],
|
isExpanded: false,
|
||||||
selectedIndex: 0,
|
|
||||||
query: '',
|
query: '',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor( props ) {
|
constructor( props ) {
|
||||||
super( props );
|
super( props );
|
||||||
this.state = this.constructor.getInitialState();
|
this.state = {
|
||||||
|
...this.constructor.getInitialState(),
|
||||||
|
filteredOptions: [],
|
||||||
|
selectedIndex: 0,
|
||||||
|
};
|
||||||
|
|
||||||
this.bindNode = this.bindNode.bind( this );
|
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.search = this.search.bind( this );
|
||||||
this.selectOption = this.selectOption.bind( this );
|
this.selectOption = this.selectOption.bind( this );
|
||||||
this.updateSelectedIndex = this.updateSelectedIndex.bind( this );
|
this.setExpanded = this.setExpanded.bind( this );
|
||||||
}
|
}
|
||||||
|
|
||||||
bindNode( node ) {
|
bindNode( node ) {
|
||||||
this.node = node;
|
this.node = node;
|
||||||
}
|
}
|
||||||
|
|
||||||
reset( selected = this.props.selected ) {
|
reset( selected = this.getSelected() ) {
|
||||||
const { multiple } = this.props;
|
const { multiple } = this.props;
|
||||||
const initialState = this.constructor.getInitialState();
|
const initialState = this.constructor.getInitialState();
|
||||||
|
|
||||||
|
@ -60,12 +65,6 @@ export class Autocomplete extends Component {
|
||||||
this.reset();
|
this.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
isExpanded() {
|
|
||||||
const { filteredOptions, query } = this.state;
|
|
||||||
|
|
||||||
return filteredOptions.length > 0 || query;
|
|
||||||
}
|
|
||||||
|
|
||||||
hasTags() {
|
hasTags() {
|
||||||
const { multiple, selected } = this.props;
|
const { multiple, selected } = this.props;
|
||||||
|
|
||||||
|
@ -76,22 +75,54 @@ export class Autocomplete extends Component {
|
||||||
return selected.some( item => Boolean( item.label ) );
|
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 ) {
|
selectOption( option ) {
|
||||||
const { multiple, onChange, selected } = this.props;
|
const { multiple, onChange, selected } = this.props;
|
||||||
const { query } = this.state;
|
const { query } = this.state;
|
||||||
const newSelected = multiple ? [ ...selected, option ] : [ option ];
|
const newSelected = multiple ? [ ...selected, option ] : [ option ];
|
||||||
|
|
||||||
// Check if this is already selected
|
// 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 } );
|
const isSelected = findIndex( selected, { key: option.key } );
|
||||||
if ( -1 === isSelected ) {
|
if ( -1 === isSelected ) {
|
||||||
onChange( newSelected, query );
|
onChange( newSelected, query );
|
||||||
}
|
}
|
||||||
|
} else if ( selected !== option.key ) {
|
||||||
|
onChange( option.key, query );
|
||||||
|
}
|
||||||
|
|
||||||
this.reset( newSelected );
|
this.reset( newSelected );
|
||||||
}
|
}
|
||||||
|
|
||||||
updateSelectedIndex( value ) {
|
decrementSelectedIndex() {
|
||||||
this.setState( { selectedIndex: value } );
|
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 ) {
|
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 ) {
|
getFilteredOptions( query ) {
|
||||||
const { excludeSelectedOptions, getSearchExpression, maxResults, onFilter, options, selected } = this.props;
|
const {
|
||||||
const selectedKeys = selected.map( option => option.key );
|
excludeSelectedOptions,
|
||||||
|
getSearchExpression,
|
||||||
|
maxResults,
|
||||||
|
onFilter,
|
||||||
|
options,
|
||||||
|
} = this.props;
|
||||||
|
const selectedKeys = this.getSelected().map( option => option.key );
|
||||||
const filtered = [];
|
const filtered = [];
|
||||||
|
|
||||||
// Create a regular expression to filter the options.
|
// Create a regular expression to filter the options.
|
||||||
|
@ -155,42 +198,50 @@ export class Autocomplete extends Component {
|
||||||
return onFilter( filtered );
|
return onFilter( filtered );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setExpanded( value ) {
|
||||||
|
this.setState( { isExpanded: value } );
|
||||||
|
}
|
||||||
|
|
||||||
search( query ) {
|
search( query ) {
|
||||||
const { hideBeforeSearch, onSearch, options } = this.props;
|
const { hideBeforeSearch, onSearch, options } = this.props;
|
||||||
|
|
||||||
onSearch( query );
|
onSearch( query );
|
||||||
// Get all options if `hideBeforeSearch` is enabled and query is not null.
|
// Get all options if `hideBeforeSearch` is enabled and query is not null.
|
||||||
const filteredOptions = null !== query && ! query.length && ! hideBeforeSearch
|
const filteredOptions =
|
||||||
|
null !== query && ! query.length && ! hideBeforeSearch
|
||||||
? options
|
? options
|
||||||
: this.getFilteredOptions( query );
|
: this.getFilteredOptions( query );
|
||||||
this.setState( { selectedIndex: 0, filteredOptions, query: query || '' }, () => this.announce( filteredOptions ) );
|
this.setState(
|
||||||
|
{
|
||||||
|
selectedIndex: 0,
|
||||||
|
filteredOptions,
|
||||||
|
isExpanded: Boolean( filteredOptions.length ),
|
||||||
|
query: query || '',
|
||||||
|
},
|
||||||
|
() => this.announce( filteredOptions )
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const {
|
const { className, inlineTags, instanceId, isSearchable, options } = this.props;
|
||||||
className,
|
const { isExpanded, selectedIndex } = this.state;
|
||||||
inlineTags,
|
|
||||||
instanceId,
|
|
||||||
options,
|
|
||||||
} = this.props;
|
|
||||||
const { selectedIndex } = this.state;
|
|
||||||
|
|
||||||
const isExpanded = this.isExpanded();
|
|
||||||
const hasTags = this.hasTags();
|
const hasTags = this.hasTags();
|
||||||
const { key: selectedKey = '' } = options[ selectedIndex ] || {};
|
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
|
const activeId = isExpanded
|
||||||
? `woocommerce-autocomplete__option-${ instanceId }-${ selectedKey }`
|
? `woocommerce-select-control__option-${ instanceId }-${ selectedKey }`
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={ classnames( 'woocommerce-autocomplete', className, {
|
className={ classnames( 'woocommerce-select-control', className, {
|
||||||
'has-inline-tags': hasTags && inlineTags,
|
'has-inline-tags': hasTags && inlineTags,
|
||||||
|
'is-searchable': isSearchable,
|
||||||
} ) }
|
} ) }
|
||||||
ref={ this.bindNode }
|
ref={ this.bindNode }
|
||||||
>
|
>
|
||||||
<SearchControl
|
<Control
|
||||||
{ ...this.props }
|
{ ...this.props }
|
||||||
{ ...this.state }
|
{ ...this.state }
|
||||||
activeId={ activeId }
|
activeId={ activeId }
|
||||||
|
@ -198,26 +249,33 @@ export class Autocomplete extends Component {
|
||||||
isExpanded={ isExpanded }
|
isExpanded={ isExpanded }
|
||||||
listboxId={ listboxId }
|
listboxId={ listboxId }
|
||||||
onSearch={ this.search }
|
onSearch={ this.search }
|
||||||
|
selected={ this.getSelected() }
|
||||||
|
setExpanded={ this.setExpanded }
|
||||||
|
decrementSelectedIndex={ this.decrementSelectedIndex }
|
||||||
|
incrementSelectedIndex={ this.incrementSelectedIndex }
|
||||||
/>
|
/>
|
||||||
{ ! inlineTags && hasTags && <Tags { ...this.props } /> }
|
{ ! inlineTags && hasTags && <Tags { ...this.props } selected={ this.getSelected() } /> }
|
||||||
{ isExpanded &&
|
{ isExpanded && (
|
||||||
<List
|
<List
|
||||||
{ ...this.props }
|
{ ...this.props }
|
||||||
{ ...this.state }
|
{ ...this.state }
|
||||||
activeId={ activeId }
|
activeId={ activeId }
|
||||||
listboxId={ listboxId }
|
listboxId={ listboxId }
|
||||||
node={ this.node }
|
node={ this.node }
|
||||||
onChange={ this.updateSelectedIndex }
|
|
||||||
onSelect={ this.selectOption }
|
onSelect={ this.selectOption }
|
||||||
onSearch={ this.search }
|
onSearch={ this.search }
|
||||||
|
options={ this.getOptions() }
|
||||||
|
decrementSelectedIndex={ this.decrementSelectedIndex }
|
||||||
|
incrementSelectedIndex={ this.incrementSelectedIndex }
|
||||||
|
setExpanded={ this.setExpanded }
|
||||||
/>
|
/>
|
||||||
}
|
) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Autocomplete.propTypes = {
|
SelectControl.propTypes = {
|
||||||
/**
|
/**
|
||||||
* Class name applied to parent div.
|
* Class name applied to parent div.
|
||||||
*/
|
*/
|
||||||
|
@ -238,14 +296,15 @@ Autocomplete.propTypes = {
|
||||||
/**
|
/**
|
||||||
* Help text to be appended beneath the input.
|
* Help text to be appended beneath the input.
|
||||||
*/
|
*/
|
||||||
help: PropTypes.oneOfType( [
|
help: PropTypes.oneOfType( [ PropTypes.string, PropTypes.node ] ),
|
||||||
PropTypes.string,
|
|
||||||
PropTypes.node,
|
|
||||||
] ),
|
|
||||||
/**
|
/**
|
||||||
* Render tags inside input, otherwise render below input.
|
* Render tags inside input, otherwise render below input.
|
||||||
*/
|
*/
|
||||||
inlineTags: PropTypes.bool,
|
inlineTags: PropTypes.bool,
|
||||||
|
/**
|
||||||
|
* Allow the select options to be filtered by search input.
|
||||||
|
*/
|
||||||
|
isSearchable: PropTypes.bool,
|
||||||
/**
|
/**
|
||||||
* A label to use for the main input.
|
* A label to use for the main input.
|
||||||
*/
|
*/
|
||||||
|
@ -265,10 +324,7 @@ Autocomplete.propTypes = {
|
||||||
options: PropTypes.arrayOf(
|
options: PropTypes.arrayOf(
|
||||||
PropTypes.shape( {
|
PropTypes.shape( {
|
||||||
isDisabled: PropTypes.bool,
|
isDisabled: PropTypes.bool,
|
||||||
key: PropTypes.oneOfType( [
|
key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] ).isRequired,
|
||||||
PropTypes.number,
|
|
||||||
PropTypes.string,
|
|
||||||
] ).isRequired,
|
|
||||||
keywords: PropTypes.arrayOf( PropTypes.string ),
|
keywords: PropTypes.arrayOf( PropTypes.string ),
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
value: PropTypes.any,
|
value: PropTypes.any,
|
||||||
|
@ -279,19 +335,19 @@ Autocomplete.propTypes = {
|
||||||
*/
|
*/
|
||||||
placeholder: PropTypes.string,
|
placeholder: PropTypes.string,
|
||||||
/**
|
/**
|
||||||
* An array of objects describing selected values. If the label of the selected
|
* An array of objects describing selected values or optionally a string for a single value.
|
||||||
* value is omitted, the Tag of that value will not be rendered inside the
|
* If the label of the selected value is omitted, the Tag of that value will not
|
||||||
* search box.
|
* be rendered inside the search box.
|
||||||
*/
|
*/
|
||||||
selected: PropTypes.arrayOf(
|
selected: PropTypes.oneOfType( [
|
||||||
PropTypes.shape( {
|
|
||||||
key: PropTypes.oneOfType( [
|
|
||||||
PropTypes.number,
|
|
||||||
PropTypes.string,
|
PropTypes.string,
|
||||||
] ).isRequired,
|
PropTypes.arrayOf(
|
||||||
|
PropTypes.shape( {
|
||||||
|
key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] ).isRequired,
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
} )
|
} )
|
||||||
),
|
),
|
||||||
|
] ),
|
||||||
/**
|
/**
|
||||||
* A limit for the number of results shown in the options menu. Set to 0 for no limit.
|
* 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,
|
staticList: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
|
||||||
Autocomplete.defaultProps = {
|
SelectControl.defaultProps = {
|
||||||
excludeSelectedOptions: true,
|
excludeSelectedOptions: true,
|
||||||
getSearchExpression: identity,
|
getSearchExpression: identity,
|
||||||
inlineTags: false,
|
inlineTags: false,
|
||||||
|
isSearchable: false,
|
||||||
onChange: noop,
|
onChange: noop,
|
||||||
onFilter: identity,
|
onFilter: identity,
|
||||||
onSearch: noop,
|
onSearch: noop,
|
||||||
|
@ -333,4 +390,4 @@ export default compose( [
|
||||||
withSpokenMessages,
|
withSpokenMessages,
|
||||||
withInstanceId,
|
withInstanceId,
|
||||||
withFocusOutside, // this MUST be the innermost HOC as it calls handleFocusOutside
|
withFocusOutside, // this MUST be the innermost HOC as it calls handleFocusOutside
|
||||||
] )( Autocomplete );
|
] )( SelectControl );
|
|
@ -23,12 +23,16 @@ class List extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate( prevProps ) {
|
componentDidUpdate( prevProps ) {
|
||||||
const { filteredOptions } = this.props;
|
const { options, selectedIndex } = this.props;
|
||||||
|
|
||||||
// Remove old option refs to avoid memory leaks.
|
// Remove old option refs to avoid memory leaks.
|
||||||
if ( ! isEqual( filteredOptions, prevProps.filteredOptions ) ) {
|
if ( ! isEqual( options, prevProps.options ) ) {
|
||||||
this.optionRefs = {};
|
this.optionRefs = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ( selectedIndex !== prevProps.selectedIndex ) {
|
||||||
|
this.scrollToOption( selectedIndex );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getOptionRef( index ) {
|
getOptionRef( index ) {
|
||||||
|
@ -52,7 +56,14 @@ class List extends Component {
|
||||||
scrollToOption( index ) {
|
scrollToOption( index ) {
|
||||||
const listbox = this.listbox.current;
|
const listbox = this.listbox.current;
|
||||||
|
|
||||||
if ( listbox.scrollHeight > listbox.clientHeight ) {
|
if ( listbox.scrollHeight <= listbox.clientHeight ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! this.optionRefs[ index ] ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const option = this.optionRefs[ index ].current;
|
const option = this.optionRefs[ index ].current;
|
||||||
const scrollBottom = listbox.clientHeight + listbox.scrollTop;
|
const scrollBottom = listbox.clientHeight + listbox.scrollTop;
|
||||||
const elementBottom = option.offsetTop + option.offsetHeight;
|
const elementBottom = option.offsetTop + option.offsetHeight;
|
||||||
|
@ -62,49 +73,46 @@ class List extends Component {
|
||||||
listbox.scrollTop = option.offsetTop;
|
listbox.scrollTop = option.offsetTop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
handleKeyDown( event ) {
|
handleKeyDown( event ) {
|
||||||
const { filteredOptions, onChange, onSearch, selectedIndex } = this.props;
|
const {
|
||||||
if ( filteredOptions.length === 0 ) {
|
decrementSelectedIndex,
|
||||||
|
incrementSelectedIndex,
|
||||||
|
options,
|
||||||
|
onSearch,
|
||||||
|
selectedIndex,
|
||||||
|
setExpanded,
|
||||||
|
} = this.props;
|
||||||
|
if ( options.length === 0 ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let nextSelectedIndex;
|
|
||||||
switch ( event.keyCode ) {
|
switch ( event.keyCode ) {
|
||||||
case UP:
|
case UP:
|
||||||
nextSelectedIndex = null !== selectedIndex
|
decrementSelectedIndex();
|
||||||
? ( selectedIndex === 0 ? filteredOptions.length : selectedIndex ) - 1
|
|
||||||
: filteredOptions.length - 1;
|
|
||||||
onChange( nextSelectedIndex );
|
|
||||||
this.scrollToOption( nextSelectedIndex );
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case DOWN:
|
case DOWN:
|
||||||
nextSelectedIndex = null !== selectedIndex
|
incrementSelectedIndex();
|
||||||
? ( selectedIndex + 1 ) % filteredOptions.length
|
|
||||||
: 0;
|
|
||||||
onChange( nextSelectedIndex );
|
|
||||||
this.scrollToOption( nextSelectedIndex );
|
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ENTER:
|
case ENTER:
|
||||||
this.select( filteredOptions[ selectedIndex ] );
|
this.select( options[ selectedIndex ] );
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case LEFT:
|
case LEFT:
|
||||||
case RIGHT:
|
case RIGHT:
|
||||||
onChange( null );
|
setExpanded( false );
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case ESCAPE:
|
case ESCAPE:
|
||||||
onChange( null );
|
setExpanded( false );
|
||||||
onSearch( null );
|
onSearch( null );
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
@ -133,22 +141,22 @@ class List extends Component {
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { filteredOptions, instanceId, listboxId, selectedIndex, staticList } = this.props;
|
const { instanceId, listboxId, options, selectedIndex, staticList } = this.props;
|
||||||
const listboxClasses = classnames( 'woocommerce-autocomplete__listbox', {
|
const listboxClasses = classnames( 'woocommerce-select-control__listbox', {
|
||||||
'is-static': staticList,
|
'is-static': staticList,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={ this.listbox } id={ listboxId } role="listbox" className={ listboxClasses }>
|
<div ref={ this.listbox } id={ listboxId } role="listbox" className={ listboxClasses }>
|
||||||
{ filteredOptions.map( ( option, index ) => (
|
{ options.map( ( option, index ) => (
|
||||||
<Button
|
<Button
|
||||||
ref={ this.getOptionRef( index ) }
|
ref={ this.getOptionRef( index ) }
|
||||||
key={ option.key }
|
key={ option.key }
|
||||||
id={ `woocommerce-autocomplete__option-${ instanceId }-${ option.key }` }
|
id={ `woocommerce-select-control__option-${ instanceId }-${ option.key }` }
|
||||||
role="option"
|
role="option"
|
||||||
aria-selected={ index === selectedIndex }
|
aria-selected={ index === selectedIndex }
|
||||||
disabled={ option.isDisabled }
|
disabled={ option.isDisabled }
|
||||||
className={ classnames( 'woocommerce-autocomplete__option', {
|
className={ classnames( 'woocommerce-select-control__option', {
|
||||||
'is-selected': index === selectedIndex,
|
'is-selected': index === selectedIndex,
|
||||||
} ) }
|
} ) }
|
||||||
onClick={ () => this.select( option ) }
|
onClick={ () => this.select( option ) }
|
||||||
|
@ -164,22 +172,7 @@ class List extends Component {
|
||||||
|
|
||||||
List.propTypes = {
|
List.propTypes = {
|
||||||
/**
|
/**
|
||||||
* Array of filtered options to display.
|
* ID of the main SelectControl instance.
|
||||||
*/
|
|
||||||
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.
|
|
||||||
*/
|
*/
|
||||||
instanceId: PropTypes.number,
|
instanceId: PropTypes.number,
|
||||||
/**
|
/**
|
||||||
|
@ -190,14 +183,22 @@ List.propTypes = {
|
||||||
* Parent node to bind keyboard events to.
|
* Parent node to bind keyboard events to.
|
||||||
*/
|
*/
|
||||||
node: PropTypes.instanceOf( Element ).isRequired,
|
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.
|
* Function to execute when an option is selected.
|
||||||
*/
|
*/
|
||||||
onSelect: PropTypes.func,
|
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.
|
* Integer for the currently selected item.
|
||||||
*/
|
*/
|
|
@ -1,4 +1,6 @@
|
||||||
.woocommerce-autocomplete {
|
/** @format */
|
||||||
|
|
||||||
|
.woocommerce-select-control {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.components-base-control {
|
.components-base-control {
|
||||||
|
@ -11,7 +13,7 @@
|
||||||
padding: $gap-small $gap;
|
padding: $gap-small $gap;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
.woocommerce-autocomplete__tags {
|
.woocommerce-select-control__tags {
|
||||||
margin: $gap-small $gap-smallest 0 0;
|
margin: $gap-small $gap-smallest 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -24,16 +26,16 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.components-base-control__label {
|
.components-base-control__label {
|
||||||
left: 52px;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
color: $studio-gray-50;
|
color: $studio-gray-50;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-autocomplete__control-input {
|
.woocommerce-select-control__control-input {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
border: 0;
|
border: 0;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
|
@ -43,10 +45,15 @@
|
||||||
padding-right: 0;
|
padding-right: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
line-height: 24px;
|
line-height: 24px;
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
&::-webkit-search-cancel-button {
|
&::-webkit-search-cancel-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
i {
|
i {
|
||||||
|
@ -61,14 +68,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.with-value .components-base-control__label,
|
&.with-value .components-base-control__label,
|
||||||
&.is-active .components-base-control__label,
|
|
||||||
&.has-tags .components-base-control__label {
|
&.has-tags .components-base-control__label {
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-top: -$gap-small;
|
margin-top: -$gap-small;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-autocomplete__tags {
|
.woocommerce-select-control__tags {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: $gap-small 0;
|
margin: $gap-small 0;
|
||||||
|
|
||||||
|
@ -81,7 +87,7 @@
|
||||||
max-height: 24px;
|
max-height: 24px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-autocomplete__clear {
|
.woocommerce-select-control__clear {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: 0;
|
right: 0;
|
||||||
top: calc(50% - 10px);
|
top: calc(50% - 10px);
|
||||||
|
@ -91,7 +97,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-autocomplete__listbox {
|
.woocommerce-select-control__listbox {
|
||||||
background: $studio-white;
|
background: $studio-white;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -111,7 +117,7 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.woocommerce-autocomplete__option {
|
.woocommerce-select-control__option {
|
||||||
padding: $gap;
|
padding: $gap;
|
||||||
min-height: 56px;
|
min-height: 56px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
|
@ -121,4 +127,15 @@
|
||||||
background: $studio-gray-0;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -43,7 +43,7 @@ class Tags extends Component {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const classes = classnames( 'woocommerce-autocomplete__tags', {
|
const classes = classnames( 'woocommerce-select-control__tags', {
|
||||||
'has-clear': showClearButton,
|
'has-clear': showClearButton,
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
@ -69,14 +69,12 @@ class Tags extends Component {
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} ) }
|
} ) }
|
||||||
{ showClearButton && <Button
|
{ showClearButton && (
|
||||||
className="woocommerce-autocomplete__clear"
|
<Button className="woocommerce-select-control__clear" isLink onClick={ this.removeAll }>
|
||||||
isLink
|
|
||||||
onClick={ this.removeAll }
|
|
||||||
>
|
|
||||||
<Icon icon="dismiss" />
|
<Icon icon="dismiss" />
|
||||||
<span className="screen-reader-text">{ __( 'Clear all', 'woocommerce-admin' ) }</span>
|
<span className="screen-reader-text">{ __( 'Clear all', 'woocommerce-admin' ) }</span>
|
||||||
</Button> }
|
</Button>
|
||||||
|
) }
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -98,10 +96,7 @@ Tags.propTypes = {
|
||||||
*/
|
*/
|
||||||
selected: PropTypes.arrayOf(
|
selected: PropTypes.arrayOf(
|
||||||
PropTypes.shape( {
|
PropTypes.shape( {
|
||||||
key: PropTypes.oneOfType( [
|
key: PropTypes.oneOfType( [ PropTypes.number, PropTypes.string ] ).isRequired,
|
||||||
PropTypes.number,
|
|
||||||
PropTypes.string,
|
|
||||||
] ).isRequired,
|
|
||||||
label: PropTypes.string,
|
label: PropTypes.string,
|
||||||
} )
|
} )
|
||||||
),
|
),
|
|
@ -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 options={ options } /> );
|
||||||
|
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 isSearchable options={ options } /> );
|
||||||
|
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
|
||||||
|
isSearchable
|
||||||
|
options={ options }
|
||||||
|
selected={ [ options[ 1 ] ] }
|
||||||
|
excludeSelectedOptions
|
||||||
|
multiple
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
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 isSearchable options={ options } /> );
|
||||||
|
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 isSearchable options={ options } maxResults={ 1 } />
|
||||||
|
);
|
||||||
|
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 isSearchable options={ options } /> );
|
||||||
|
|
||||||
|
selectControl.instance().search( '' );
|
||||||
|
selectControl.update();
|
||||||
|
|
||||||
|
expect( selectControl.find( Button ).filter( '.' + optionClassname ).length ).toBe( 3 );
|
||||||
|
} );
|
||||||
|
|
||||||
|
it( 'shows options after query', () => {
|
||||||
|
const selectControl = mount(
|
||||||
|
<SelectControl isSearchable options={ options } hideBeforeSearch />
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<SelectControl
|
||||||
|
options={ options }
|
||||||
|
onFilter={ filteredOptions =>
|
||||||
|
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(
|
||||||
|
<SelectControl
|
||||||
|
isSearchable
|
||||||
|
options={ queriedOptions }
|
||||||
|
onSearch={ queryOptions }
|
||||||
|
onFilter={ () => 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 );
|
||||||
|
} );
|
||||||
|
} );
|
|
@ -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',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
<SimpleSelectControl
|
|
||||||
label="What is your favorite pet?"
|
|
||||||
onChange={ value => 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.
|
|
|
@ -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 (
|
|
||||||
<SimpleSelectControl
|
|
||||||
label="What is your favorite pet?"
|
|
||||||
onChange={ value => this.setState( { pet: value } ) }
|
|
||||||
options={ petOptions }
|
|
||||||
value={ pet }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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 (
|
|
||||||
<Dropdown
|
|
||||||
id={ id }
|
|
||||||
className={ classNames(
|
|
||||||
'woocommerce-simple-select-control__dropdown',
|
|
||||||
'components-base-control',
|
|
||||||
className,
|
|
||||||
{
|
|
||||||
'is-empty': isEmpty,
|
|
||||||
'has-value': ! isEmpty,
|
|
||||||
'is-active': isFocused,
|
|
||||||
}
|
|
||||||
) }
|
|
||||||
contentClassName="woocommerce-simple-select-control__dropdown-content"
|
|
||||||
position="center"
|
|
||||||
renderToggle={ ( { isOpen, onToggle } ) => (
|
|
||||||
<Fragment>
|
|
||||||
<Button
|
|
||||||
className="woocommerce-simple-select-control__selector"
|
|
||||||
onClick={ () => this.handleOnClick( onToggle ) }
|
|
||||||
onFocus={ () => this.handleOnFocus() }
|
|
||||||
aria-expanded={ isOpen }
|
|
||||||
aria-label={ ! isEmpty ? sprintf(
|
|
||||||
/* translators: Label: Current Value for a Select Dropddown */
|
|
||||||
__( '%s: %s' ), label, currentOption && currentOption.label
|
|
||||||
) : label }
|
|
||||||
>
|
|
||||||
<span className="woocommerce-simple-select-control__label">{ label }</span>
|
|
||||||
<span className="woocommerce-simple-select-control__value">{ currentOption && currentOption.label }</span>
|
|
||||||
</Button>
|
|
||||||
{ !! help && <p id={ id + '__help' } className="components-base-control__help">{ help }</p> }
|
|
||||||
</Fragment>
|
|
||||||
) }
|
|
||||||
renderContent={ ( { onClose } ) => (
|
|
||||||
<NavigableMenu>
|
|
||||||
{ map( options, ( option ) => {
|
|
||||||
const optionValue = option.value;
|
|
||||||
const optionLabel = option.label;
|
|
||||||
const optionDisabled = option.disabled || false;
|
|
||||||
const isSelected = ( currentValue === optionValue );
|
|
||||||
return (
|
|
||||||
<Button
|
|
||||||
key={ optionValue }
|
|
||||||
onClick={ () => {
|
|
||||||
onChange( optionValue );
|
|
||||||
onClose();
|
|
||||||
} }
|
|
||||||
className={ classNames( {
|
|
||||||
'is-selected': isSelected,
|
|
||||||
} ) }
|
|
||||||
disabled={ optionDisabled }
|
|
||||||
role="menuitemradio"
|
|
||||||
aria-checked={ isSelected }
|
|
||||||
>
|
|
||||||
<span>
|
|
||||||
{ optionLabel }
|
|
||||||
</span>
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
} ) }
|
|
||||||
</NavigableMenu>
|
|
||||||
) }
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 ) );
|
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,6 @@
|
||||||
* Internal Dependencies
|
* Internal Dependencies
|
||||||
*/
|
*/
|
||||||
@import 'animation-slider/style.scss';
|
@import 'animation-slider/style.scss';
|
||||||
@import 'autocomplete/style.scss';
|
|
||||||
@import 'calendar/style.scss';
|
@import 'calendar/style.scss';
|
||||||
@import 'card/style.scss';
|
@import 'card/style.scss';
|
||||||
@import 'chart/style.scss';
|
@import 'chart/style.scss';
|
||||||
|
@ -27,7 +26,7 @@
|
||||||
@import 'search-list-control/style.scss';
|
@import 'search-list-control/style.scss';
|
||||||
@import 'section-header/style.scss';
|
@import 'section-header/style.scss';
|
||||||
@import 'segmented-selection/style.scss';
|
@import 'segmented-selection/style.scss';
|
||||||
@import 'simple-select-control/style.scss';
|
@import 'select-control/style.scss';
|
||||||
@import 'split-button/style.scss';
|
@import 'split-button/style.scss';
|
||||||
@import 'stepper/style.scss';
|
@import 'stepper/style.scss';
|
||||||
@import 'spinner/style.scss';
|
@import 'spinner/style.scss';
|
||||||
|
|
Loading…
Reference in New Issue