Merge pull request woocommerce/woocommerce-admin#496 from woocommerce/add/search-inline-tags

Search: Add inline tags
This commit is contained in:
Paul Sealock 2018-10-12 16:25:19 +13:00 committed by GitHub
commit 22f28bfa11
11 changed files with 295 additions and 68 deletions

View File

@ -25,5 +25,8 @@
"wpcalypso/import-no-redux-combine-reducers": 0,
"wpcalypso/jsx-classname-namespace": 0,
"wpcalypso/redux-no-bound-selectors": 1,
"jsx-a11y/label-has-for": [ "error", {
"required": "id"
} ]
},
}

View File

@ -70,6 +70,7 @@ class SearchFilter extends Component {
placeholder={ labels.placeholder }
selected={ selected }
ariaLabelledby={ `${ key }-${ instanceId }` }
inlineTags
/>
</div>
</Fragment>

View File

@ -3,12 +3,28 @@ import { Search } from '@woocommerce/components';
const MySearch = withState( {
selected: [],
} )( ( { selected, setState } ) => (
<Search
type="products"
placeholder="Search for a product"
selected={ selected }
onChange={ items => setState( { selected: items } ) }
/>
inlineSelected: [],
} )( ( { selected, inlineSelected, setState } ) => (
<div>
<H>Tags Below Input</H>
<Section component={ false }>
<Search
type="products"
placeholder="Search for a product"
selected={ selected }
onChange={ items => setState( { selected: items } ) }
/>
</Section>
<H>Tags Inline with Input</H>
<Section component={ false }>
<Search
type="products"
placeholder="Search for a product"
selected={ inlineSelected }
onChange={ items => setState( { inlineSelected: items } ) }
inlineTags
/>
</Section>
</div>
) );
```

View File

@ -3,10 +3,12 @@
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { Component, createRef } from '@wordpress/element';
import { withInstanceId } from '@wordpress/compose';
import { findIndex, noop } from 'lodash';
import Gridicon from 'gridicons';
import PropTypes from 'prop-types';
import classnames from 'classnames';
/**
* Internal dependencies
@ -25,11 +27,16 @@ class Search extends Component {
super( props );
this.state = {
value: '',
isActive: false,
};
this.input = createRef();
this.selectResult = this.selectResult.bind( this );
this.removeResult = this.removeResult.bind( this );
this.updateSearch = this.updateSearch.bind( this );
this.onFocus = this.onFocus.bind( this );
this.onBlur = this.onBlur.bind( this );
}
selectResult( value ) {
@ -71,10 +78,44 @@ class Search extends Component {
}
}
renderTags() {
const { selected } = this.props;
return selected.length ? (
<div className="woocommerce-search__token-list">
{ selected.map( ( item, i ) => {
const screenReaderLabel = sprintf(
__( '%1$s (%2$s of %3$s)', 'wc-admin' ),
item.label,
i + 1,
selected.length
);
return (
<Tag
key={ item.id }
id={ item.id }
label={ item.label }
remove={ this.removeResult }
removeLabel={ __( 'Remove product', 'wc-admin' ) }
screenReaderLabel={ screenReaderLabel }
/>
);
} ) }
</div>
) : null;
}
onFocus() {
this.setState( { isActive: true } );
}
onBlur() {
this.setState( { isActive: false } );
}
render() {
const autocompleter = this.getAutocompleter();
const { placeholder, selected } = this.props;
const { value = '' } = this.state;
const { placeholder, inlineTags, selected, instanceId } = this.props;
const { value = '', isActive } = this.state;
const aria = {
'aria-labelledby': this.props[ 'aria-labelledby' ],
'aria-label': this.props[ 'aria-label' ],
@ -83,43 +124,65 @@ class Search extends Component {
<div className="woocommerce-search">
<Gridicon className="woocommerce-search__icon" icon="search" size={ 18 } />
<Autocomplete completer={ autocompleter } onSelect={ this.selectResult }>
{ ( { listBoxId, activeId, onChange } ) => (
<input
type="search"
value={ value }
placeholder={ placeholder }
className="woocommerce-search__input"
onChange={ this.updateSearch( onChange ) }
aria-owns={ listBoxId }
aria-activedescendant={ activeId }
{ ...aria }
/>
) }
</Autocomplete>
{ selected.length ? (
<div className="woocommerce-search__token-list">
{ selected.map( ( item, i ) => {
const screenReaderLabel = sprintf(
__( '%1$s (%2$s of %3$s)', 'wc-admin' ),
item.label,
i,
selected.length
);
return (
<Tag
key={ item.id }
id={ item.id }
label={ item.label }
remove={ this.removeResult }
removeLabel={ __( 'Remove product', 'wc-admin' ) }
screenReaderLabel={ screenReaderLabel }
{ ( { listBoxId, activeId, onChange } ) =>
// Disable reason: The div below visually simulates an input field. Its
// child input is the actual input and responds accordingly to all keyboard
// events, but click events need to be passed onto the child input. There
// is no appropriate aria role for describing this situation, which is only
// for the benefit of sighted users.
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
inlineTags ? (
<div
className={ classnames( 'woocommerce-search__inline-container', {
'is-active': isActive,
} ) }
onClick={ () => {
this.input.current.focus();
} }
>
{ this.renderTags() }
<input
ref={ this.input }
type="text"
size={
( ( value.length === 0 && placeholder && placeholder.length ) ||
value.length ) + 1
}
value={ value }
placeholder={ ( selected.length === 0 && placeholder ) || '' }
className="woocommerce-search__inline-input"
onChange={ this.updateSearch( onChange ) }
aria-owns={ listBoxId }
aria-activedescendant={ activeId }
onFocus={ this.onFocus }
onBlur={ this.onBlur }
aria-describedby={
selected.length ? `search-inline-input-${ instanceId }` : null
}
{ ...aria }
/>
);
} ) }
</div>
) : null }
<span id={ `search-inline-input-${ instanceId }` } className="screen-reader-text">
{ __( 'Move backward for selected items' ) }
</span>
</div>
) : (
<input
type="search"
value={ value }
placeholder={ placeholder }
className="woocommerce-search__input"
onChange={ this.updateSearch( onChange ) }
aria-owns={ listBoxId }
aria-activedescendant={ activeId }
{ ...aria }
/>
)
}
</Autocomplete>
{ ! inlineTags && this.renderTags() }
</div>
);
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
}
}
@ -146,11 +209,16 @@ Search.propTypes = {
label: PropTypes.string.isRequired,
} )
),
/**
* Render tags inside input, otherwise render below input
*/
inlineTags: PropTypes.bool,
};
Search.defaultProps = {
onChange: noop,
selected: [],
inlineTags: false,
};
export default Search;
export default withInstanceId( Search );

View File

@ -10,6 +10,30 @@
fill: $core-grey-light-900;
}
.woocommerce-search__inline-container {
width: 100%;
padding: 2px 2px 2px 36px;
border: 1px solid $core-grey-light-700;
background-color: $white;
&.is-active {
border-color: $input-active-border;
box-shadow: inset 0 0 0 $input-active-inner, 0 0 1px 2px $input-active-outer;
}
.woocommerce-search__token-list {
display: inline-block;
}
}
.woocommerce-search__inline-input,
.woocommerce-search__inline-input:focus {
border: none;
outline: none;
box-shadow: none;
padding: 6px 0;
}
.woocommerce-search__input {
width: 100%;
padding: $gap-smaller $gap-small $gap-smaller 36px;

View File

@ -48,6 +48,9 @@ $woocommerce: $woocommerce-500;
$button-hover: #fafafa;
$button-focus-inner: #00435d;
$button-focus-outer: #bfe7f3;
$input-active-border: #00a0d2;
$input-active-inner: $button-focus-inner;
$input-active-outer: $button-focus-outer;
// wp-admin
$wp-admin-background: #f1f1f1;

View File

@ -20,6 +20,13 @@ An array of data.
Format to parse dates into d3 time format
### `pointLabelFormat`
- Type: String
- Default: null
Date format of the point labels (might be used in tooltips and ARIA properties).
### `tooltipFormat`
- Type: String
@ -64,6 +71,14 @@ A number formatting string, passed to d3Format.
`standard` (default) legend layout in the header or `comparison` moves legend layout
to the left or 'compact' has the legend below
### `mode`
- Type: One of: 'item-comparison', 'time-comparison'
- Default: `'item-comparison'`
`item-comparison` (default) or `time-comparison`, this is used to generate correct
ARIA properties.
### `title`
- Type: String
@ -157,6 +172,13 @@ Interval specification (hourly, daily, weekly etc.)
`standard` (default) legend layout in the header or `comparison` moves legend layout
to the left or 'compact' has the legend below
### `pointLabelFormat`
- Type: String
- Default: null
Date format of the point labels (might be used in tooltips and ARIA properties).
### `margin`
- Type: Object
@ -173,6 +195,14 @@ to the left or 'compact' has the legend below
Margins for axis and chart padding.
### `mode`
- Type: One of: 'item-comparison', 'time-comparison'
- Default: `'item-comparison'`
`items-comparison` (default) or `time-comparison`, this is used to generate correct
ARIA properties.
### `orderedKeys`
- Type: Array

View File

@ -85,6 +85,63 @@ The `path` parameter supplied by React-Router.
The query string represented in object form.
`CompareFilter` (component)
===========================
Displays a card + search used to filter results as a comparison between objects.
Props
-----
### `getLabels`
- **Required**
- Type: Function
- Default: null
Function used to fetch object labels via an API request, returns a Promise.
### `labels`
- Type: Object
- placeholder: String - Label for the search placeholder.
- title: String - Label for the card title.
- update: String - Label for button which updates the URL/report.
- Default: `{}`
Object of localized labels.
### `param`
- **Required**
- Type: String
- Default: null
The parameter to use in the querystring.
### `path`
- **Required**
- Type: String
- Default: null
The `path` parameter supplied by React-Router
### `query`
- Type: Object
- Default: `{}`
The query string represented in object form
### `type`
- **Required**
- Type: String
- Default: null
Which type of autocompleter should be used in the Search
`DatePicker` (component)
========================

View File

@ -17,7 +17,7 @@ Function called when selected results change, passed result list.
### `type`
- **Required**
- Type: One of: 'products', 'product_cats', 'orders', 'customers'
- Type: One of: 'products', 'product_cats', 'orders', 'customers', 'coupons'
- Default: null
The object type to be used in searching.
@ -38,3 +38,10 @@ A placeholder for the search input.
An array of objects describing selected values.
### `inlineTags`
- Type: Boolean
- Default: `false`
Render tags inside input, otherwise render below input

View File

@ -30,6 +30,17 @@ The string to use as a query parameter when comparing row items.
An array of column headers (see `Table` props).
### `labels`
- Type: Object
- compareButton: String
- downloadButton: String
- helpText: String
- placeholder: String
- Default: null
Custom labels for table header actions.
### `ids`
- Type: Array
@ -118,6 +129,13 @@ The total number of rows (across all pages).
Props
-----
### `query`
- Type: Object
- Default: null
An object of the query parameters passed to the page, ex `{ page: 2, per_page: 5 }`.
### `caption`
- **Required**

View File

@ -2563,7 +2563,7 @@
},
"babel-plugin-syntax-class-properties": {
"version": "6.13.0",
"resolved": "http://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz",
"resolved": "https://registry.npmjs.org/babel-plugin-syntax-class-properties/-/babel-plugin-syntax-class-properties-6.13.0.tgz",
"integrity": "sha1-1+sjt5oxf4VDlixQW4J8fWysJ94=",
"dev": true
},
@ -2975,7 +2975,7 @@
},
"browserify-aes": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz",
"integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==",
"dev": true,
"requires": {
@ -3012,7 +3012,7 @@
},
"browserify-rsa": {
"version": "4.0.1",
"resolved": "http://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.0.1.tgz",
"integrity": "sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ=",
"dev": true,
"requires": {
@ -3098,7 +3098,7 @@
},
"buffer": {
"version": "4.9.1",
"resolved": "http://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.1.tgz",
"integrity": "sha1-bRu2AbB6TvztlwlBMgkwJ8lbwpg=",
"dev": true,
"requires": {
@ -3834,7 +3834,7 @@
},
"create-hash": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz",
"integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==",
"dev": true,
"requires": {
@ -3847,7 +3847,7 @@
},
"create-hmac": {
"version": "1.1.7",
"resolved": "http://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz",
"integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==",
"dev": true,
"requires": {
@ -4514,7 +4514,7 @@
},
"diffie-hellman": {
"version": "5.0.3",
"resolved": "http://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz",
"integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==",
"dev": true,
"requires": {
@ -4614,7 +4614,7 @@
},
"os-locale": {
"version": "1.4.0",
"resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
"integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
"dev": true,
"requires": {
@ -5651,7 +5651,7 @@
},
"external-editor": {
"version": "2.2.0",
"resolved": "http://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
"resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz",
"integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==",
"dev": true,
"requires": {
@ -7330,7 +7330,7 @@
},
"http-errors": {
"version": "1.6.3",
"resolved": "http://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz",
"integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=",
"dev": true,
"requires": {
@ -7895,7 +7895,7 @@
},
"is-obj": {
"version": "1.0.1",
"resolved": "http://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz",
"integrity": "sha1-PkcprB9f3gJc19g6iW2rn09n2w8=",
"dev": true
},
@ -9908,7 +9908,7 @@
},
"jsonfile": {
"version": "2.4.0",
"resolved": "http://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
"resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz",
"integrity": "sha1-NzaitCi4e72gzIO1P6PWM6NcKug=",
"dev": true,
"requires": {
@ -11055,7 +11055,7 @@
},
"chalk": {
"version": "1.1.3",
"resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz",
"integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=",
"dev": true,
"requires": {
@ -11111,7 +11111,7 @@
},
"minimist": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
@ -11535,7 +11535,7 @@
},
"minimist": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
},
@ -11749,7 +11749,7 @@
},
"parse-asn1": {
"version": "5.1.1",
"resolved": "http://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
"resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.1.tgz",
"integrity": "sha512-KPx7flKXg775zZpnp9SxJlz00gTd4BmJ2yJufSc44gMCRrRQ7NSzAcSJQfifuOLgW6bEi+ftrALtsgALeB2Adw==",
"dev": true,
"requires": {
@ -12679,7 +12679,7 @@
},
"public-encrypt": {
"version": "4.0.2",
"resolved": "http://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz",
"resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.2.tgz",
"integrity": "sha512-4kJ5Esocg8X3h8YgJsKAuoesBgB7mqH3eowiDzMUPKiRDDE7E/BqqZD1hnTByIaAFiwAw246YEltSq7tdrOH0Q==",
"dev": true,
"requires": {
@ -12836,7 +12836,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}
@ -13840,7 +13840,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}
@ -13875,7 +13875,7 @@
},
"os-locale": {
"version": "1.4.0",
"resolved": "http://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
"resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz",
"integrity": "sha1-IPnxeuKe00XoveWDsT0gCYA8FNk=",
"dev": true,
"requires": {
@ -14156,7 +14156,7 @@
},
"sha.js": {
"version": "2.4.11",
"resolved": "http://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz",
"integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==",
"dev": true,
"requires": {
@ -15611,7 +15611,7 @@
"dependencies": {
"minimist": {
"version": "1.2.0",
"resolved": "http://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz",
"integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=",
"dev": true
}