Skip to content
This repository was archived by the owner on Feb 23, 2024. It is now read-only.

Commit a36db08

Browse files
authored
ProductAttributeControl: Polish style, screen reader interaction (#412)
* Save attribute terms per attribute, so we don’t need to do duplicate API requests for previously fetched terms. Add spinner to loading items. * Add count to API response * Use terms count in displaying attribute type * Update attribute selection code to collapse already-selected attributes * Add a chevron icon indicating open-able attributes * Center the loading indicator * Remove count from attribute type & re-add it to the terms themselves
1 parent f611788 commit a36db08

File tree

4 files changed

+91
-32
lines changed

4 files changed

+91
-32
lines changed

assets/js/components/product-attribute-control/index.js

Lines changed: 45 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ import { __, _n, sprintf } from '@wordpress/i18n';
55
import { addQueryArgs } from '@wordpress/url';
66
import apiFetch from '@wordpress/api-fetch';
77
import { Component, Fragment } from '@wordpress/element';
8-
import { debounce, filter, find, uniqBy } from 'lodash';
8+
import { debounce, find } from 'lodash';
99
import PropTypes from 'prop-types';
10-
import { SelectControl } from '@wordpress/components';
10+
import { SelectControl, Spinner } from '@wordpress/components';
1111

1212
/**
1313
* Internal dependencies
@@ -23,6 +23,7 @@ class ProductAttributeControl extends Component {
2323
list: [],
2424
loading: true,
2525
attribute: 0,
26+
termsList: {},
2627
termsLoading: true,
2728
};
2829

@@ -58,10 +59,13 @@ class ProductAttributeControl extends Component {
5859
}
5960

6061
getTerms() {
61-
const { attribute } = this.state;
62+
const { attribute, termsList } = this.state;
6263
if ( ! attribute ) {
6364
return;
6465
}
66+
if ( ! termsList[ attribute ] ) {
67+
this.setState( { termsLoading: true } );
68+
}
6569

6670
apiFetch( {
6771
path: addQueryArgs( `/wc-pb/v3/products/attributes/${ attribute }/terms`, {
@@ -70,8 +74,8 @@ class ProductAttributeControl extends Component {
7074
} )
7175
.then( ( terms ) => {
7276
terms = terms.map( ( term ) => ( { ...term, parent: attribute } ) );
73-
this.setState( ( { list } ) => ( {
74-
list: uniqBy( [ ...list, ...terms ], 'id' ),
77+
this.setState( ( prevState ) => ( {
78+
termsList: { ...prevState.termsList, [ attribute ]: terms },
7579
termsLoading: false,
7680
} ) );
7781
} )
@@ -82,24 +86,16 @@ class ProductAttributeControl extends Component {
8286

8387
onSelectAttribute( item ) {
8488
return () => {
85-
if ( item.id === this.state.attribute ) {
86-
return;
87-
}
8889
this.props.onChange( [] );
89-
this.setState( ( { list } ) => {
90-
// Remove all other attribute terms from the list.
91-
const updatedList = filter( list, { parent: 0 } );
92-
return {
93-
list: updatedList,
94-
attribute: item.id,
95-
};
90+
this.setState( {
91+
attribute: item.id === this.state.attribute ? 0 : item.id,
9692
} );
9793
};
9894
}
9995

10096
renderItem( args ) {
10197
const { item, search, depth = 0 } = args;
102-
const { attribute } = this.state;
98+
const { attribute, termsLoading } = this.state;
10399
const classes = [
104100
'woocommerce-product-attributes__item',
105101
'woocommerce-search-list__item',
@@ -112,16 +108,39 @@ class ProductAttributeControl extends Component {
112108
}
113109

114110
if ( ! item.breadcrumbs.length ) {
115-
classes.push( 'is-not-active' );
116-
return (
111+
return [
117112
<SearchListItem
113+
key={ `attr-${ item.id }` }
118114
{ ...args }
119115
className={ classes.join( ' ' ) }
120-
isSingle
121116
isSelected={ attribute === item.id }
122117
onSelect={ this.onSelectAttribute }
123-
/>
124-
);
118+
isSingle
119+
disabled={ '0' === item.count }
120+
aria-expanded={ attribute === item.id }
121+
aria-label={ sprintf(
122+
_n(
123+
'%s, has %d term',
124+
'%s, has %d terms',
125+
item.count,
126+
'woo-gutenberg-products-block'
127+
),
128+
item.name,
129+
item.count
130+
) }
131+
/>,
132+
attribute === item.id && termsLoading && (
133+
<div
134+
key="loading"
135+
className={
136+
'woocommerce-search-list__item woocommerce-product-attributes__item' +
137+
'depth-1 is-loading is-not-active'
138+
}
139+
>
140+
<Spinner />
141+
</div>
142+
),
143+
];
125144
}
126145

127146
return (
@@ -135,8 +154,10 @@ class ProductAttributeControl extends Component {
135154
}
136155

137156
render() {
138-
const { list, loading } = this.state;
157+
const { attribute, list, loading, termsList } = this.state;
139158
const { onChange, onOperatorChange, operator, selected } = this.props;
159+
const currentTerms = termsList[ attribute ] || [];
160+
const currentList = [ ...list, ...currentTerms ];
140161

141162
const messages = {
142163
clear: __( 'Clear all product attributes', 'woo-gutenberg-products-block' ),
@@ -169,10 +190,10 @@ class ProductAttributeControl extends Component {
169190
<Fragment>
170191
<SearchListControl
171192
className="woocommerce-product-attributes"
172-
list={ list }
193+
list={ currentList }
173194
isLoading={ loading }
174195
selected={ selected
175-
.map( ( { id } ) => find( list, { id } ) )
196+
.map( ( { id } ) => find( currentList, { id } ) )
176197
.filter( Boolean ) }
177198
onChange={ onChange }
178199
renderItem={ this.renderItem }

assets/js/components/product-attribute-control/style.scss

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,36 @@
2525

2626
&.is-not-active {
2727
@include hover-state {
28-
background: transparent;
28+
background: $white;
2929
}
3030
}
31+
32+
&.is-loading {
33+
justify-content: center;
34+
35+
.components-spinner {
36+
margin-bottom: $gap-small;
37+
}
38+
}
39+
40+
&.depth-0::after {
41+
margin-left: $gap-smaller;
42+
content: '';
43+
height: $gap-large;
44+
width: $gap-large;
45+
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7.41 15.41L12 10.83l4.59 4.58L18 14l-6-6-6 6z" fill="#{$core-grey-dark-300}" /></svg>');
46+
background-repeat: no-repeat;
47+
background-position: center right;
48+
background-size: contain;
49+
}
50+
51+
&.depth-0[aria-expanded="true"]::after {
52+
background-image: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z" fill="#{$core-grey-dark-300}" /></svg>');
53+
}
54+
55+
&[disabled].depth-0::after {
56+
margin-left: 0;
57+
width: auto;
58+
background: none;
59+
}
3160
}

assets/js/components/search-list-control/style.scss

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@
7070

7171
.components-spinner {
7272
float: none;
73+
margin: 0 auto;
7374
}
7475

7576
.components-menu-group__label {

includes/class-wgpb-product-attributes-controller.php

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -138,10 +138,12 @@ protected function check_permissions( $request, $context = 'read' ) {
138138
* @return WP_REST_Response
139139
*/
140140
public function prepare_item_for_response( $item, $request ) {
141-
$data = array(
142-
'id' => (int) $item->attribute_id,
143-
'name' => $item->attribute_label,
144-
'slug' => wc_attribute_taxonomy_name( $item->attribute_name ),
141+
$taxonomy = wc_attribute_taxonomy_name( $item->attribute_name );
142+
$data = array(
143+
'id' => (int) $item->attribute_id,
144+
'name' => $item->attribute_label,
145+
'slug' => $taxonomy,
146+
'count' => wp_count_terms( $taxonomy ),
145147
);
146148

147149
$context = ! empty( $request['context'] ) ? $request['context'] : 'view';
@@ -169,9 +171,15 @@ public function get_item_schema() {
169171
'properties' => array(),
170172
);
171173

172-
$schema['properties']['id'] = $raw_schema['properties']['id'];
173-
$schema['properties']['name'] = $raw_schema['properties']['name'];
174-
$schema['properties']['slug'] = $raw_schema['properties']['slug'];
174+
$schema['properties']['id'] = $raw_schema['properties']['id'];
175+
$schema['properties']['name'] = $raw_schema['properties']['name'];
176+
$schema['properties']['slug'] = $raw_schema['properties']['slug'];
177+
$schema['properties']['count'] = array(
178+
'description' => __( 'Number of terms in the attribute taxonomy.', 'woo-gutenberg-products-block' ),
179+
'type' => 'integer',
180+
'context' => array( 'view', 'edit' ),
181+
'readonly' => true,
182+
);
175183

176184
return $this->add_additional_fields_schema( $schema );
177185
}

0 commit comments

Comments
 (0)