Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hierarchical terms sorted by name and filterable #10138

Merged
merged 5 commits into from
Sep 27, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { get, unescape as unescapeString, without, find, some, invoke } from 'lo
/**
* WordPress dependencies
*/
import { __, _x, sprintf } from '@wordpress/i18n';
import { __, _x, _n, sprintf } from '@wordpress/i18n';
import { Component } from '@wordpress/element';
import { TreeSelect, withSpokenMessages, withFilters, Button } from '@wordpress/components';
import { withSelect, withDispatch } from '@wordpress/data';
Expand All @@ -24,8 +24,8 @@ import { buildTermsTree } from '../../utils/terms';
*/
const DEFAULT_QUERY = {
per_page: -1,
orderby: 'count',
order: 'desc',
orderby: 'name',
order: 'asc',
_fields: 'id,name,parent',
};

Expand All @@ -38,6 +38,8 @@ class HierarchicalTermSelector extends Component {
this.onChangeFormParent = this.onChangeFormParent.bind( this );
this.onAddTerm = this.onAddTerm.bind( this );
this.onToggleForm = this.onToggleForm.bind( this );
this.setFilterValue = this.setFilterValue.bind( this );
this.sortBySelected = this.sortBySelected.bind( this );
this.state = {
loading: true,
availableTermsTree: [],
Expand All @@ -46,6 +48,8 @@ class HierarchicalTermSelector extends Component {
formName: '',
formParent: '',
showForm: false,
filterValue: '',
filteredTermsTree: [],
};
}

Expand Down Expand Up @@ -152,7 +156,7 @@ class HierarchicalTermSelector extends Component {
formName: '',
formParent: '',
availableTerms: newAvailableTerms,
availableTermsTree: buildTermsTree( newAvailableTerms ),
availableTermsTree: this.sortBySelected( buildTermsTree( newAvailableTerms ) ),
} );
onUpdateTerms( [ ...terms, term.id ], taxonomy.rest_base );
}, ( xhr ) => {
Expand Down Expand Up @@ -191,7 +195,7 @@ class HierarchicalTermSelector extends Component {
} );
this.fetchRequest.then(
( terms ) => { // resolve
const availableTermsTree = buildTermsTree( terms );
const availableTermsTree = this.sortBySelected( buildTermsTree( terms ) );

this.fetchRequest = null;
this.setState( {
Expand All @@ -212,6 +216,101 @@ class HierarchicalTermSelector extends Component {
);
}

sortBySelected( termsTree ) {
const { terms } = this.props;
const treeHasSelection = ( termTree ) => {
if ( terms.indexOf( termTree.id ) !== -1 ) {
return true;
}
if ( undefined === termTree.children ) {
return false;
}
const anyChildIsSelected = termTree.children.map( treeHasSelection ).filter( ( child ) => child ).length > 0;
if ( anyChildIsSelected ) {
return true;
}
return false;
};
const termOrChildIsSelected = ( termA, termB ) => {
const termASelected = treeHasSelection( termA );
const termBSelected = treeHasSelection( termB );

if ( termASelected === termBSelected ) {
return 0;
}

if ( termASelected && ! termBSelected ) {
return -1;
}

if ( ! termASelected && termBSelected ) {
return 1;
}

return 0;
};
termsTree.sort( termOrChildIsSelected );
return termsTree;
}

setFilterValue( event ) {
const { availableTermsTree } = this.state;
const filterValue = event.target.value;
const filteredTermsTree = availableTermsTree.map( this.getFilterMatcher( filterValue ) ).filter( ( term ) => term );
const getResultCount = ( terms ) => {
let count = 0;
for ( let i = 0; i < terms.length; i++ ) {
count++;
if ( undefined !== terms[ i ].children ) {
count += getResultCount( terms[ i ].children );
}
}
return count;
};
this.setState(
{
filterValue,
filteredTermsTree,
}
);

const resultCount = getResultCount( filteredTermsTree );
const resultsFoundMessage = sprintf(
_n( '%d result found.', '%d results found.', resultCount, 'term' ),
resultCount
);
this.props.debouncedSpeak( resultsFoundMessage, 'assertive' );
}

getFilterMatcher( filterValue ) {
const matchTermsForFilter = ( originalTerm ) => {
if ( '' === filterValue ) {
return originalTerm;
}

// Shallow clone, because we'll be filtering the term's children and
// don't want to modify the original term.
const term = { ...originalTerm };

// Map and filter the children, recursive so we deal with grandchildren
// and any deeper levels.
if ( term.children.length > 0 ) {
term.children = term.children.map( matchTermsForFilter ).filter( ( child ) => child );
}

// If the term's name contains the filterValue, or it has children
// (i.e. some child matched at some point in the tree) then return it.
if ( -1 !== term.name.toLowerCase().indexOf( filterValue ) || term.children.length > 0 ) {
return term;
}

// Otherwise, return false. After mapping, the list of terms will need
// to have false values filtered out.
return false;
};
return matchTermsForFilter;
}

renderTerms( renderedTerms ) {
const { terms = [] } = this.props;
return renderedTerms.map( ( term ) => {
Expand Down Expand Up @@ -244,7 +343,7 @@ class HierarchicalTermSelector extends Component {
return null;
}

const { availableTermsTree, availableTerms, formName, formParent, loading, showForm } = this.state;
const { availableTermsTree, availableTerms, filteredTermsTree, formName, formParent, loading, showForm, filterValue } = this.state;
const labelWithFallback = ( labelProperty, fallbackIsCategory, fallbackIsNotCategory ) => get(
taxonomy,
[ 'data', 'labels', labelProperty ],
Expand All @@ -268,10 +367,47 @@ class HierarchicalTermSelector extends Component {
const noParentOption = `— ${ parentSelectLabel } —`;
const newTermSubmitLabel = newTermButtonLabel;
const inputId = `editor-post-taxonomies__hierarchical-terms-input-${ instanceId }`;
const filterInputId = `editor-post-taxonomies__hierarchical-terms-filter-${ instanceId }`;
const filterLabel = sprintf(
_x( 'Search %s', 'term' ),
get(
this.props.taxonomy,
[ 'name' ],
slug === 'category' ? __( 'Categories' ) : __( 'Terms' )
)
);
const groupLabel = sprintf(
_x( 'Available %s', 'term' ),
get(
this.props.taxonomy,
[ 'name' ],
slug === 'category' ? __( 'Categories' ) : __( 'Terms' )
)
);

/* eslint-disable jsx-a11y/no-onchange */
return [
...this.renderTerms( availableTermsTree ),
<label
key="filter-label"
htmlFor={ filterInputId }>
{ filterLabel }
</label>,
<input
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the search input needs a visible <label> element, properly associated with for / id attributes

type="search"
id={ filterInputId }
value={ filterValue }
onChange={ this.setFilterValue }
className="editor-post-taxonomies__hierarchical-terms-filter"
key="term-filter-input"
/>,
<div
className="editor-post-taxonomies__hierarchical-terms-list"
key="term-list"
tabIndex="0"
role="group"
aria-label={ groupLabel }
>
{ this.renderTerms( '' !== filterValue ? filteredTermsTree : availableTermsTree ) }
</div>,
! loading && hasCreateAction && (
<Button
key="term-add-button"
Expand Down
9 changes: 9 additions & 0 deletions packages/editor/src/components/post-taxonomies/style.scss
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
.editor-post-taxonomies__hierarchical-terms-list {
max-height: 14em;
overflow: auto;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scrollable divs need to be made keyboard accessible: only Firefox makes them focusable by default so for other browsers it's necessary to make them focusable and add some ARIA. See for example the Inserter. I'd say something like tabIndex="0" role="group" aria-label={ __( 'Available terms' ) }" could work

}

.editor-post-taxonomies__hierarchical-terms-choice {
margin-bottom: 8px;
}
Expand Down Expand Up @@ -25,3 +30,7 @@
margin-top: 8px;
width: 100%;
}
.editor-post-taxonomies__hierarchical-terms-filter {
margin-bottom: 8px;
width: 100%;
}