Components: Move SearchListControl to woocommerce/components (https://github.com/woocommerce/woocommerce-blocks/pull/442)
* Switch to using the SearchListControl from woocommerce/components * Remove unused mixin
This commit is contained in:
parent
71d8235449
commit
2a00a897d5
|
@ -57,15 +57,3 @@
|
|||
margin: unset;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// Create a string-repeat function
|
||||
@function repeat($character, $n) {
|
||||
@if $n == 0 {
|
||||
@return "";
|
||||
}
|
||||
$c: "";
|
||||
@for $i from 1 through $n {
|
||||
$c: $c + $character;
|
||||
}
|
||||
@return $c;
|
||||
}
|
||||
|
|
|
@ -7,14 +7,13 @@ import apiFetch from '@wordpress/api-fetch';
|
|||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { debounce, find } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SearchListControl, SearchListItem } from '@woocommerce/components';
|
||||
import { SelectControl, Spinner } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import SearchListControl from '../search-list-control';
|
||||
import SearchListItem from '../search-list-control/item';
|
||||
|
||||
class ProductAttributeControl extends Component {
|
||||
constructor() {
|
||||
|
|
|
@ -7,14 +7,13 @@ import apiFetch from '@wordpress/api-fetch';
|
|||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { find } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
import { SearchListControl, SearchListItem } from '@woocommerce/components';
|
||||
import { SelectControl } from '@wordpress/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import SearchListControl from '../search-list-control';
|
||||
import SearchListItem from '../search-list-control/item';
|
||||
|
||||
class ProductCategoryControl extends Component {
|
||||
constructor() {
|
||||
|
|
|
@ -7,11 +7,7 @@ import apiFetch from '@wordpress/api-fetch';
|
|||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { find } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import SearchListControl from '../search-list-control';
|
||||
import { SearchListControl } from '@woocommerce/components';
|
||||
|
||||
class ProductControl extends Component {
|
||||
constructor() {
|
||||
|
|
|
@ -7,11 +7,7 @@ import apiFetch from '@wordpress/api-fetch';
|
|||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { find } from 'lodash';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import SearchListControl from '../search-list-control';
|
||||
import { SearchListControl } from '@woocommerce/components';
|
||||
|
||||
class ProductsControl extends Component {
|
||||
constructor() {
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { forEach, groupBy, keyBy } from 'lodash';
|
||||
|
||||
/**
|
||||
* Returns terms in a tree form.
|
||||
*
|
||||
* @param {Array} filteredList Array of terms, possibly a subset of all terms, in flat format.
|
||||
* @param {Array} list Array of the full list of terms, defaults to the filteredList.
|
||||
*
|
||||
* @return {Array} Array of terms in tree format.
|
||||
*/
|
||||
export function buildTermsTree( filteredList, list = filteredList ) {
|
||||
const termsByParent = groupBy( filteredList, 'parent' );
|
||||
const listById = keyBy( list, 'id' );
|
||||
|
||||
const getParentsName = ( term = {} ) => {
|
||||
if ( ! term.parent ) {
|
||||
return term.name ? [ term.name ] : [];
|
||||
}
|
||||
|
||||
const parentName = getParentsName( listById[ term.parent ] );
|
||||
return [ ...parentName, term.name ];
|
||||
};
|
||||
|
||||
const fillWithChildren = ( terms ) => {
|
||||
return terms.map( ( term ) => {
|
||||
const children = termsByParent[ term.id ];
|
||||
delete termsByParent[ term.id ];
|
||||
return {
|
||||
...term,
|
||||
breadcrumbs: getParentsName( listById[ term.parent ] ),
|
||||
children: children && children.length ? fillWithChildren( children ) : [],
|
||||
};
|
||||
} );
|
||||
};
|
||||
|
||||
const tree = fillWithChildren( termsByParent[ '0' ] || [] );
|
||||
delete termsByParent[ '0' ];
|
||||
|
||||
// anything left in termsByParent has no visible parent
|
||||
forEach( termsByParent, ( terms ) => {
|
||||
tree.push( ...fillWithChildren( terms || [] ) );
|
||||
} );
|
||||
|
||||
return tree;
|
||||
}
|
|
@ -1,312 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { __, _n, sprintf } from '@wordpress/i18n';
|
||||
import {
|
||||
Button,
|
||||
MenuGroup,
|
||||
Spinner,
|
||||
TextControl,
|
||||
withSpokenMessages,
|
||||
} from '@wordpress/components';
|
||||
import { Component, Fragment } from '@wordpress/element';
|
||||
import { compose, withInstanceId, withState } from '@wordpress/compose';
|
||||
import { escapeRegExp, findIndex } from 'lodash';
|
||||
import Gridicon from 'gridicons';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Tag } from '@woocommerce/components';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import './style.scss';
|
||||
import { buildTermsTree } from './hierarchy';
|
||||
import SearchListItem from './item';
|
||||
|
||||
const defaultMessages = {
|
||||
clear: __( 'Clear all selected items', 'woo-gutenberg-products-block' ),
|
||||
list: __( 'Results', 'woo-gutenberg-products-block' ),
|
||||
noItems: __( 'No items found.', 'woo-gutenberg-products-block' ),
|
||||
noResults: __( 'No results for %s', 'woo-gutenberg-products-block' ),
|
||||
search: __( 'Search for items', 'woo-gutenberg-products-block' ),
|
||||
selected: ( n ) =>
|
||||
sprintf( _n( '%d item selected', '%d items selected', n, 'woo-gutenberg-products-block' ), n ),
|
||||
updated: __( 'Search results updated.', 'woo-gutenberg-products-block' ),
|
||||
};
|
||||
|
||||
/**
|
||||
* Component to display a searchable, selectable list of items.
|
||||
*/
|
||||
export class SearchListControl extends Component {
|
||||
constructor() {
|
||||
super( ...arguments );
|
||||
|
||||
this.onSelect = this.onSelect.bind( this );
|
||||
this.onRemove = this.onRemove.bind( this );
|
||||
this.onClear = this.onClear.bind( this );
|
||||
this.isSelected = this.isSelected.bind( this );
|
||||
this.defaultRenderItem = this.defaultRenderItem.bind( this );
|
||||
this.renderList = this.renderList.bind( this );
|
||||
}
|
||||
|
||||
onRemove( id ) {
|
||||
const { isSingle, onChange, selected } = this.props;
|
||||
return () => {
|
||||
if ( isSingle ) {
|
||||
onChange( [] );
|
||||
}
|
||||
const i = findIndex( selected, { id } );
|
||||
onChange( [ ...selected.slice( 0, i ), ...selected.slice( i + 1 ) ] );
|
||||
};
|
||||
}
|
||||
|
||||
onSelect( item ) {
|
||||
const { isSingle, onChange, selected } = this.props;
|
||||
return () => {
|
||||
if ( this.isSelected( item ) ) {
|
||||
this.onRemove( item.id )();
|
||||
return;
|
||||
}
|
||||
if ( isSingle ) {
|
||||
onChange( [ item ] );
|
||||
} else {
|
||||
onChange( [ ...selected, item ] );
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
onClear() {
|
||||
this.props.onChange( [] );
|
||||
}
|
||||
|
||||
isSelected( item ) {
|
||||
return -1 !== findIndex( this.props.selected, { id: item.id } );
|
||||
}
|
||||
|
||||
getFilteredList( list, search ) {
|
||||
const { isHierarchical } = this.props;
|
||||
if ( ! search ) {
|
||||
return isHierarchical ? buildTermsTree( list ) : list;
|
||||
}
|
||||
const messages = { ...defaultMessages, ...this.props.messages };
|
||||
const re = new RegExp( escapeRegExp( search ), 'i' );
|
||||
this.props.debouncedSpeak( messages.updated );
|
||||
const filteredList = list
|
||||
.map( ( item ) => ( re.test( item.name ) ? item : false ) )
|
||||
.filter( Boolean );
|
||||
return isHierarchical ? buildTermsTree( filteredList, list ) : filteredList;
|
||||
}
|
||||
|
||||
defaultRenderItem( args ) {
|
||||
return <SearchListItem { ...args } />;
|
||||
}
|
||||
|
||||
renderList( list, depth = 0 ) {
|
||||
const { isSingle, search } = this.props;
|
||||
const renderItem = this.props.renderItem || this.defaultRenderItem;
|
||||
if ( ! list ) {
|
||||
return null;
|
||||
}
|
||||
return list.map( ( item ) => (
|
||||
<Fragment key={ item.id }>
|
||||
{ renderItem( {
|
||||
item,
|
||||
isSelected: this.isSelected( item ),
|
||||
onSelect: this.onSelect,
|
||||
isSingle,
|
||||
search,
|
||||
depth,
|
||||
} ) }
|
||||
{ this.renderList( item.children, depth + 1 ) }
|
||||
</Fragment>
|
||||
) );
|
||||
}
|
||||
|
||||
renderListSection() {
|
||||
const { isLoading, search } = this.props;
|
||||
const list = this.getFilteredList( this.props.list, search );
|
||||
const messages = { ...defaultMessages, ...this.props.messages };
|
||||
|
||||
if ( isLoading ) {
|
||||
return (
|
||||
<div className="woocommerce-search-list__list is-loading">
|
||||
<Spinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! list.length ) {
|
||||
return (
|
||||
<div className="woocommerce-search-list__list is-not-found">
|
||||
<span className="woocommerce-search-list__not-found-icon">
|
||||
<Gridicon
|
||||
icon="notice-outline"
|
||||
role="img"
|
||||
aria-hidden="true"
|
||||
focusable="false"
|
||||
/>
|
||||
</span>
|
||||
<span className="woocommerce-search-list__not-found-text">
|
||||
{ search ? sprintf( messages.noResults, search ) : messages.noItems }
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MenuGroup
|
||||
label={ messages.list }
|
||||
className="woocommerce-search-list__list"
|
||||
>
|
||||
{ this.renderList( list ) }
|
||||
</MenuGroup>
|
||||
);
|
||||
}
|
||||
|
||||
renderSelectedSection() {
|
||||
const { isLoading, isSingle, selected } = this.props;
|
||||
const messages = { ...defaultMessages, ...this.props.messages };
|
||||
|
||||
if ( isLoading || isSingle || ! selected ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedCount = selected.length;
|
||||
return (
|
||||
<div className="woocommerce-search-list__selected">
|
||||
<div className="woocommerce-search-list__selected-header">
|
||||
<strong>{ messages.selected( selectedCount ) }</strong>
|
||||
{ selectedCount > 0 ? (
|
||||
<Button
|
||||
isLink
|
||||
isDestructive
|
||||
onClick={ this.onClear }
|
||||
aria-label={ messages.clear }
|
||||
>
|
||||
{ __( 'Clear all', 'woo-gutenberg-products-block' ) }
|
||||
</Button>
|
||||
) : null }
|
||||
</div>
|
||||
{ selected.map( ( item, i ) => (
|
||||
<Tag key={ i } label={ item.name } id={ item.id } remove={ this.onRemove } />
|
||||
) ) }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { className = '', search, setState } = this.props;
|
||||
const messages = { ...defaultMessages, ...this.props.messages };
|
||||
|
||||
return (
|
||||
<div className={ `woocommerce-search-list ${ className }` }>
|
||||
{ this.renderSelectedSection() }
|
||||
|
||||
<div className="woocommerce-search-list__search">
|
||||
<TextControl
|
||||
label={ messages.search }
|
||||
type="search"
|
||||
value={ search }
|
||||
onChange={ ( value ) => setState( { search: value } ) }
|
||||
/>
|
||||
</div>
|
||||
|
||||
{ this.renderListSection() }
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
SearchListControl.propTypes = {
|
||||
/**
|
||||
* Additional CSS classes.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* Whether the list of items is hierarchical or not. If true, each list item is expected to
|
||||
* have a parent property.
|
||||
*/
|
||||
isHierarchical: PropTypes.bool,
|
||||
/**
|
||||
* Whether the list of items is still loading.
|
||||
*/
|
||||
isLoading: PropTypes.bool,
|
||||
/**
|
||||
* Restrict selections to one item.
|
||||
*/
|
||||
isSingle: PropTypes.bool,
|
||||
/**
|
||||
* A complete list of item objects, each with id, name properties. This is displayed as a
|
||||
* clickable/keyboard-able list, and possibly filtered by the search term (searches name).
|
||||
*/
|
||||
list: PropTypes.arrayOf(
|
||||
PropTypes.shape( {
|
||||
id: PropTypes.number,
|
||||
name: PropTypes.string,
|
||||
} )
|
||||
),
|
||||
/**
|
||||
* Messages displayed or read to the user. Configure these to reflect your object type.
|
||||
* See `defaultMessages` above for examples.
|
||||
*/
|
||||
messages: PropTypes.shape( {
|
||||
/**
|
||||
* A more detailed label for the "Clear all" button, read to screen reader users.
|
||||
*/
|
||||
clear: PropTypes.string,
|
||||
/**
|
||||
* Label for the list of selectable items, only read to screen reader users.
|
||||
*/
|
||||
list: PropTypes.string,
|
||||
/**
|
||||
* Message to display when the list is empty (implies nothing loaded from the server
|
||||
* or parent component).
|
||||
*/
|
||||
noItems: PropTypes.string,
|
||||
/**
|
||||
* Message to display when no matching results are found. %s is the search term.
|
||||
*/
|
||||
noResults: PropTypes.string,
|
||||
/**
|
||||
* Label for the search input
|
||||
*/
|
||||
search: PropTypes.string,
|
||||
/**
|
||||
* Label for the selected items. This is actually a function, so that we can pass
|
||||
* through the count of currently selected items.
|
||||
*/
|
||||
selected: PropTypes.func,
|
||||
/**
|
||||
* Label indicating that search results have changed, read to screen reader users.
|
||||
*/
|
||||
updated: PropTypes.string,
|
||||
} ),
|
||||
/**
|
||||
* Callback fired when selected items change, whether added, cleared, or removed.
|
||||
* Passed an array of item objects (as passed in via props.list).
|
||||
*/
|
||||
onChange: PropTypes.func.isRequired,
|
||||
/**
|
||||
* Callback to render each item in the selection list, allows any custom object-type rendering.
|
||||
*/
|
||||
renderItem: PropTypes.func,
|
||||
/**
|
||||
* The list of currently selected items.
|
||||
*/
|
||||
selected: PropTypes.array.isRequired,
|
||||
// from withState
|
||||
search: PropTypes.string,
|
||||
setState: PropTypes.func,
|
||||
// from withSpokenMessages
|
||||
debouncedSpeak: PropTypes.func,
|
||||
// from withInstanceId
|
||||
instanceId: PropTypes.number,
|
||||
};
|
||||
|
||||
export default compose( [
|
||||
withState( {
|
||||
search: '',
|
||||
} ),
|
||||
withSpokenMessages,
|
||||
withInstanceId,
|
||||
] )( SearchListControl );
|
|
@ -1,132 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import { escapeRegExp, first, last } from 'lodash';
|
||||
import { MenuItem } from '@wordpress/components';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import {
|
||||
IconCheckChecked,
|
||||
IconCheckUnchecked,
|
||||
IconRadioSelected,
|
||||
IconRadioUnselected,
|
||||
} from '../icons';
|
||||
|
||||
function getHighlightedName( name, search ) {
|
||||
if ( ! search ) {
|
||||
return name;
|
||||
}
|
||||
const re = new RegExp( escapeRegExp( search ), 'ig' );
|
||||
return name.replace( re, '<strong>$&</strong>' );
|
||||
}
|
||||
|
||||
function getBreadcrumbsForDisplay( breadcrumbs ) {
|
||||
if ( breadcrumbs.length === 1 ) {
|
||||
return first( breadcrumbs );
|
||||
}
|
||||
if ( breadcrumbs.length === 2 ) {
|
||||
return first( breadcrumbs ) + ' › ' + last( breadcrumbs );
|
||||
}
|
||||
|
||||
return first( breadcrumbs ) + ' … ' + last( breadcrumbs );
|
||||
}
|
||||
|
||||
const getInteractionIcon = ( isSingle = false, isSelected = false ) => {
|
||||
if ( isSingle ) {
|
||||
return isSelected ? <IconRadioSelected /> : <IconRadioUnselected />;
|
||||
}
|
||||
return isSelected ? <IconCheckChecked /> : <IconCheckUnchecked />;
|
||||
};
|
||||
|
||||
const SearchListItem = ( {
|
||||
className,
|
||||
depth = 0,
|
||||
item,
|
||||
isSelected,
|
||||
isSingle,
|
||||
onSelect,
|
||||
search = '',
|
||||
showCount = false,
|
||||
...props
|
||||
} ) => {
|
||||
const classes = [ className, 'woocommerce-search-list__item' ];
|
||||
classes.push( `depth-${ depth }` );
|
||||
if ( isSingle ) {
|
||||
classes.push( 'is-radio-button' );
|
||||
}
|
||||
const hasBreadcrumbs = item.breadcrumbs && item.breadcrumbs.length;
|
||||
|
||||
return (
|
||||
<MenuItem
|
||||
role={ isSingle ? 'menuitemradio' : 'menuitemcheckbox' }
|
||||
className={ classes.join( ' ' ) }
|
||||
onClick={ onSelect( item ) }
|
||||
isSelected={ isSelected }
|
||||
{ ...props }
|
||||
>
|
||||
<span className="woocommerce-search-list__item-state">
|
||||
{ getInteractionIcon( isSingle, isSelected ) }
|
||||
</span>
|
||||
|
||||
<span className="woocommerce-search-list__item-label">
|
||||
{ hasBreadcrumbs ? (
|
||||
<span className="woocommerce-search-list__item-prefix">
|
||||
{ getBreadcrumbsForDisplay( item.breadcrumbs ) }
|
||||
</span>
|
||||
) : null }
|
||||
<span
|
||||
className="woocommerce-search-list__item-name"
|
||||
dangerouslySetInnerHTML={ {
|
||||
__html: getHighlightedName( item.name, search ),
|
||||
} }
|
||||
/>
|
||||
</span>
|
||||
|
||||
{ !! showCount && (
|
||||
<span className="woocommerce-search-list__item-count">
|
||||
{ item.count }
|
||||
</span>
|
||||
) }
|
||||
</MenuItem>
|
||||
);
|
||||
};
|
||||
|
||||
SearchListItem.propTypes = {
|
||||
/**
|
||||
* Additional CSS classes.
|
||||
*/
|
||||
className: PropTypes.string,
|
||||
/**
|
||||
* Depth, non-zero if the list is hierarchical.
|
||||
*/
|
||||
depth: PropTypes.number,
|
||||
/**
|
||||
* Current item to display.
|
||||
*/
|
||||
item: PropTypes.object,
|
||||
/**
|
||||
* Whether this item is selected.
|
||||
*/
|
||||
isSelected: PropTypes.bool,
|
||||
/**
|
||||
* Whether this should only display a single item (controls radio vs checkbox icon).
|
||||
*/
|
||||
isSingle: PropTypes.bool,
|
||||
/**
|
||||
* Callback for selecting the item.
|
||||
*/
|
||||
onSelect: PropTypes.func,
|
||||
/**
|
||||
* Search string, used to highlight the substring in the item name.
|
||||
*/
|
||||
search: PropTypes.string,
|
||||
/**
|
||||
* Toggles the "count" bubble on/off.
|
||||
*/
|
||||
showCount: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default SearchListItem;
|
|
@ -1,208 +0,0 @@
|
|||
.woocommerce-search-list {
|
||||
width: 100%;
|
||||
padding: 0 0 $gap;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.woocommerce-search-list__selected {
|
||||
margin: $gap 0;
|
||||
padding: $gap 0 0;
|
||||
// 76px is the height of 1 row of tags.
|
||||
min-height: 76px;
|
||||
border-top: 1px solid $core-grey-light-500;
|
||||
|
||||
.woocommerce-search-list__selected-header {
|
||||
margin-bottom: $gap-smaller;
|
||||
|
||||
button {
|
||||
margin-left: $gap-small;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-tag__text {
|
||||
max-width: 13em;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-search-list__search {
|
||||
margin: $gap 0;
|
||||
padding: $gap 0 0;
|
||||
border-top: 1px solid $core-grey-light-500;
|
||||
|
||||
.components-base-control__field {
|
||||
margin-bottom: $gap;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-search-list__list {
|
||||
padding: 0;
|
||||
max-height: 17em;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
border-top: 1px solid $core-grey-light-500;
|
||||
border-bottom: 1px solid $core-grey-light-500;
|
||||
|
||||
&.is-loading {
|
||||
padding: $gap-small 0;
|
||||
text-align: center;
|
||||
border: none;
|
||||
}
|
||||
|
||||
&.is-not-found {
|
||||
padding: $gap-small 0;
|
||||
text-align: center;
|
||||
border: none;
|
||||
|
||||
.woocommerce-search-list__not-found-icon,
|
||||
.woocommerce-search-list__not-found-text {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.woocommerce-search-list__not-found-icon {
|
||||
margin-right: $gap;
|
||||
|
||||
.gridicon {
|
||||
vertical-align: top;
|
||||
margin-top: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.components-spinner {
|
||||
float: none;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.components-menu-group__label {
|
||||
@include visually-hidden;
|
||||
}
|
||||
|
||||
& > [role="menu"] {
|
||||
border: 1px solid $core-grey-light-500;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.woocommerce-search-list__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 0;
|
||||
padding: $gap-small $gap;
|
||||
background: $white;
|
||||
// !important to keep the border around on hover
|
||||
border-bottom: 1px solid $core-grey-light-500 !important;
|
||||
color: $core-grey-dark-500;
|
||||
|
||||
@include hover-state {
|
||||
background: $core-grey-light-100;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.woocommerce-search-list__item-state {
|
||||
flex: 0 0 16px;
|
||||
margin-right: $gap-smaller;
|
||||
// Set an explicit height to ensure vertical alignment
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.woocommerce-search-list__item-label {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.depth-0 + .depth-1 {
|
||||
// Hide the border on the preceding list item
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
&:not(.depth-0) {
|
||||
border-bottom: 0 !important;
|
||||
}
|
||||
|
||||
&:not(.depth-0) + .depth-0 {
|
||||
border-top: 1px solid $core-grey-light-500;
|
||||
}
|
||||
|
||||
// Anything deeper than 5 levels will use this fallback depth
|
||||
&[class*="depth-"] .woocommerce-search-list__item-label:before {
|
||||
margin-right: $gap-smallest;
|
||||
content: repeat( '— ', 5 );
|
||||
}
|
||||
|
||||
&.depth-0 .woocommerce-search-list__item-label:before {
|
||||
margin-right: 0;
|
||||
content: '';
|
||||
}
|
||||
|
||||
@for $i from 1 to 5 {
|
||||
&.depth-#{$i} .woocommerce-search-list__item-label:before {
|
||||
content: repeat( '— ', $i );
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-search-list__item-name {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.woocommerce-search-list__item-prefix {
|
||||
display: none;
|
||||
color: $core-grey-dark-300;
|
||||
}
|
||||
|
||||
&.is-searching,
|
||||
&.is-skip-level {
|
||||
.woocommerce-search-list__item-label {
|
||||
// Un-flex the label, so the prefix (breadcrumbs) and name are aligned.
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.woocommerce-search-list__item-prefix {
|
||||
display: inline;
|
||||
|
||||
&:after {
|
||||
margin-right: $gap-smallest;
|
||||
content: " ›";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.is-searching {
|
||||
.woocommerce-search-list__item-name {
|
||||
color: $core-grey-dark-900;
|
||||
}
|
||||
}
|
||||
|
||||
.woocommerce-search-list__item-count {
|
||||
flex: 0;
|
||||
padding: $gap-smallest/2 $gap-smaller;
|
||||
border: 1px solid $core-grey-light-500;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8em;
|
||||
line-height: 1.4;
|
||||
color: $core-grey-dark-300;
|
||||
background: $white;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.components-panel {
|
||||
.woocommerce-search-list {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.woocommerce-search-list__selected {
|
||||
margin: 0 0 $gap;
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
// 54px is the height of 1 row of tags in the sidebar.
|
||||
min-height: 54px;
|
||||
}
|
||||
|
||||
.woocommerce-search-list__search {
|
||||
margin: 0 0 $gap;
|
||||
padding: 0;
|
||||
border-top: none;
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
|
@ -1,152 +0,0 @@
|
|||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { buildTermsTree } from '../hierarchy';
|
||||
|
||||
const list = [
|
||||
{ id: 1, name: 'Apricots', parent: 0 },
|
||||
{ id: 2, name: 'Clementine', parent: 0 },
|
||||
{ id: 3, name: 'Elderberry', parent: 2 },
|
||||
{ id: 4, name: 'Guava', parent: 2 },
|
||||
{ id: 5, name: 'Lychee', parent: 3 },
|
||||
{ id: 6, name: 'Mulberry', parent: 0 },
|
||||
{ id: 7, name: 'Tamarind', parent: 5 },
|
||||
];
|
||||
|
||||
describe( 'buildTermsTree', () => {
|
||||
test( 'should return an empty array on empty input', () => {
|
||||
const tree = buildTermsTree( [] );
|
||||
expect( tree ).toEqual( [] );
|
||||
} );
|
||||
|
||||
test( 'should return a flat array when there are no parent relationships', () => {
|
||||
const tree = buildTermsTree( [
|
||||
{ id: 1, name: 'Apricots', parent: 0 },
|
||||
{ id: 2, name: 'Clementine', parent: 0 },
|
||||
] );
|
||||
expect( tree ).toEqual( [
|
||||
{ id: 1, name: 'Apricots', parent: 0, breadcrumbs: [], children: [] },
|
||||
{ id: 2, name: 'Clementine', parent: 0, breadcrumbs: [], children: [] },
|
||||
] );
|
||||
} );
|
||||
|
||||
test( 'should return a tree of items', () => {
|
||||
const tree = buildTermsTree( list );
|
||||
expect( tree ).toEqual( [
|
||||
{ id: 1, name: 'Apricots', parent: 0, breadcrumbs: [], children: [] },
|
||||
{
|
||||
id: 2,
|
||||
name: 'Clementine',
|
||||
parent: 0,
|
||||
breadcrumbs: [],
|
||||
children: [
|
||||
{
|
||||
id: 3,
|
||||
name: 'Elderberry',
|
||||
parent: 2,
|
||||
breadcrumbs: [ 'Clementine' ],
|
||||
children: [
|
||||
{
|
||||
id: 5,
|
||||
name: 'Lychee',
|
||||
parent: 3,
|
||||
breadcrumbs: [ 'Clementine', 'Elderberry' ],
|
||||
children: [
|
||||
{
|
||||
id: 7,
|
||||
name: 'Tamarind',
|
||||
parent: 5,
|
||||
breadcrumbs: [ 'Clementine', 'Elderberry', 'Lychee' ],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Guava',
|
||||
parent: 2,
|
||||
breadcrumbs: [ 'Clementine' ],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 6, name: 'Mulberry', parent: 0, breadcrumbs: [], children: [] },
|
||||
] );
|
||||
} );
|
||||
|
||||
test( 'should return a tree of items, with orphan categories appended to the end', () => {
|
||||
const filteredList = [
|
||||
{ id: 1, name: 'Apricots', parent: 0 },
|
||||
{ id: 2, name: 'Clementine', parent: 0 },
|
||||
{ id: 4, name: 'Guava', parent: 2 },
|
||||
{ id: 5, name: 'Lychee', parent: 3 },
|
||||
{ id: 6, name: 'Mulberry', parent: 0 },
|
||||
];
|
||||
const tree = buildTermsTree( filteredList, list );
|
||||
expect( tree ).toEqual( [
|
||||
{ id: 1, name: 'Apricots', parent: 0, breadcrumbs: [], children: [] },
|
||||
{
|
||||
id: 2,
|
||||
name: 'Clementine',
|
||||
parent: 0,
|
||||
breadcrumbs: [],
|
||||
children: [
|
||||
{
|
||||
id: 4,
|
||||
name: 'Guava',
|
||||
parent: 2,
|
||||
breadcrumbs: [ 'Clementine' ],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: 6, name: 'Mulberry', parent: 0, breadcrumbs: [], children: [] },
|
||||
{
|
||||
id: 5,
|
||||
name: 'Lychee',
|
||||
parent: 3,
|
||||
breadcrumbs: [ 'Clementine', 'Elderberry' ],
|
||||
children: [],
|
||||
},
|
||||
] );
|
||||
} );
|
||||
|
||||
test( 'should return a tree of items, with orphan categories appended to the end, with children of thier own', () => {
|
||||
const filteredList = [
|
||||
{ id: 1, name: 'Apricots', parent: 0 },
|
||||
{ id: 3, name: 'Elderberry', parent: 2 },
|
||||
{ id: 4, name: 'Guava', parent: 2 },
|
||||
{ id: 5, name: 'Lychee', parent: 3 },
|
||||
{ id: 6, name: 'Mulberry', parent: 0 },
|
||||
];
|
||||
const tree = buildTermsTree( filteredList, list );
|
||||
expect( tree ).toEqual( [
|
||||
{ id: 1, name: 'Apricots', parent: 0, breadcrumbs: [], children: [] },
|
||||
{ id: 6, name: 'Mulberry', parent: 0, breadcrumbs: [], children: [] },
|
||||
{
|
||||
id: 3,
|
||||
name: 'Elderberry',
|
||||
parent: 2,
|
||||
breadcrumbs: [ 'Clementine' ],
|
||||
children: [
|
||||
{
|
||||
id: 5,
|
||||
name: 'Lychee',
|
||||
parent: 3,
|
||||
breadcrumbs: [ 'Clementine', 'Elderberry' ],
|
||||
children: [],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
name: 'Guava',
|
||||
parent: 2,
|
||||
breadcrumbs: [ 'Clementine' ],
|
||||
children: [],
|
||||
},
|
||||
] );
|
||||
} );
|
||||
} );
|
|
@ -1,174 +0,0 @@
|
|||
/**
|
||||
* External dependencies
|
||||
*/
|
||||
import renderer from 'react-test-renderer';
|
||||
import { noop } from 'lodash';
|
||||
|
||||
/**
|
||||
* Internal dependencies
|
||||
*/
|
||||
import { SearchListControl } from '../';
|
||||
|
||||
const list = [
|
||||
{ id: 1, name: 'Apricots' },
|
||||
{ id: 2, name: 'Clementine' },
|
||||
{ id: 3, name: 'Elderberry' },
|
||||
{ id: 4, name: 'Guava' },
|
||||
{ id: 5, name: 'Lychee' },
|
||||
{ id: 6, name: 'Mulberry' },
|
||||
];
|
||||
|
||||
const hierarchicalList = [
|
||||
{ id: 1, name: 'Apricots', parent: 0 },
|
||||
{ id: 2, name: 'Clementine', parent: 1 },
|
||||
{ id: 3, name: 'Elderberry', parent: 1 },
|
||||
{ id: 4, name: 'Guava', parent: 3 },
|
||||
{ id: 5, name: 'Lychee', parent: 0 },
|
||||
{ id: 6, name: 'Mulberry', parent: 0 },
|
||||
];
|
||||
|
||||
describe( 'SearchListControl', () => {
|
||||
test( 'should render a search box and list of options', () => {
|
||||
const component = renderer.create(
|
||||
<SearchListControl
|
||||
instanceId={ 1 }
|
||||
list={ list }
|
||||
selected={ [] }
|
||||
onChange={ noop }
|
||||
/>
|
||||
);
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render a search box and list of options with a custom className', () => {
|
||||
const component = renderer.create(
|
||||
<SearchListControl
|
||||
instanceId={ 1 }
|
||||
className="test-search"
|
||||
list={ list }
|
||||
selected={ [] }
|
||||
onChange={ noop }
|
||||
/>
|
||||
);
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render a search box, a list of options, and 1 selected item', () => {
|
||||
const component = renderer.create(
|
||||
<SearchListControl
|
||||
instanceId={ 1 }
|
||||
list={ list }
|
||||
selected={ [ list[ 1 ] ] }
|
||||
onChange={ noop }
|
||||
/>
|
||||
);
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render a search box, a list of options, and 2 selected item', () => {
|
||||
const component = renderer.create(
|
||||
<SearchListControl
|
||||
instanceId={ 1 }
|
||||
list={ list }
|
||||
selected={ [ list[ 1 ], list[ 3 ] ] }
|
||||
onChange={ noop }
|
||||
/>
|
||||
);
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render a search box and no options', () => {
|
||||
const component = renderer.create(
|
||||
<SearchListControl
|
||||
instanceId={ 1 }
|
||||
list={ [] }
|
||||
selected={ [] }
|
||||
onChange={ noop }
|
||||
/>
|
||||
);
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render a search box with a search term, and only matching options', () => {
|
||||
const component = renderer.create(
|
||||
<SearchListControl
|
||||
instanceId={ 1 }
|
||||
list={ list }
|
||||
search="berry"
|
||||
selected={ [] }
|
||||
onChange={ noop }
|
||||
debouncedSpeak={ noop }
|
||||
/>
|
||||
);
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render a search box with a search term, and only matching options, regardless of case sensitivity', () => {
|
||||
const component = renderer.create(
|
||||
<SearchListControl
|
||||
instanceId={ 1 }
|
||||
list={ list }
|
||||
search="bERry"
|
||||
selected={ [] }
|
||||
onChange={ noop }
|
||||
debouncedSpeak={ noop }
|
||||
/>
|
||||
);
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render a search box with a search term, and no matching options', () => {
|
||||
const component = renderer.create(
|
||||
<SearchListControl
|
||||
instanceId={ 1 }
|
||||
list={ list }
|
||||
search="no matches"
|
||||
selected={ [] }
|
||||
onChange={ noop }
|
||||
debouncedSpeak={ noop }
|
||||
/>
|
||||
);
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render a search box and list of options, with a custom search input message', () => {
|
||||
const messages = { search: 'Testing search label' };
|
||||
const component = renderer.create(
|
||||
<SearchListControl
|
||||
instanceId={ 1 }
|
||||
list={ list }
|
||||
selected={ [] }
|
||||
onChange={ noop }
|
||||
messages={ messages }
|
||||
/>
|
||||
);
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render a search box and list of options, with a custom render callback for each item', () => {
|
||||
const renderItem = ({ item }) => <div key={item.id}>{item.name}!</div>; // eslint-disable-line
|
||||
const component = renderer.create(
|
||||
<SearchListControl
|
||||
instanceId={ 1 }
|
||||
list={ list }
|
||||
selected={ [] }
|
||||
onChange={ noop }
|
||||
renderItem={ renderItem }
|
||||
/>
|
||||
);
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
|
||||
test( 'should render a search box and list of hierarchical options', () => {
|
||||
const component = renderer.create(
|
||||
<SearchListControl
|
||||
instanceId={ 1 }
|
||||
list={ hierarchicalList }
|
||||
selected={ [] }
|
||||
onChange={ noop }
|
||||
isHierarchical
|
||||
/>
|
||||
);
|
||||
expect( component.toJSON() ).toMatchSnapshot();
|
||||
} );
|
||||
} );
|
Loading…
Reference in New Issue