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

Filter all products block by attribute #1127

Merged
merged 69 commits into from
Nov 11, 2019
Merged
Show file tree
Hide file tree
Changes from 68 commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
47a36b2
Block setup
mikejolley Nov 4, 2019
dd1fe39
Working filtering and intersection of arrays
mikejolley Nov 4, 2019
7f36fdf
Implement block settings and no attribute placeholder
mikejolley Nov 4, 2019
63c3489
Correctly toggle counts
mikejolley Nov 4, 2019
2a4bf48
Implement filtering
mikejolley Nov 4, 2019
3f2e1df
Fix price slider constraints
mikejolley Nov 4, 2019
378d861
Fix price slider constraints
mikejolley Nov 4, 2019
8941166
Edit mode
mikejolley Nov 5, 2019
d1695c5
Rename ProductAttributeControl to ProductAttributeTermControl
mikejolley Nov 5, 2019
b92b99d
Attribute ID saving
mikejolley Nov 5, 2019
7d90236
fix incorrect test fixtures
nerrad Nov 4, 2019
98fae73
fix incorrect regex for parsing model (or resource names) from the ro…
nerrad Nov 4, 2019
4aeaed1
Fix query classes for some endpoints
mikejolley Nov 5, 2019
dcb704c
Style improvements
mikejolley Nov 5, 2019
7c26a00
Update inline comments
mikejolley Nov 6, 2019
2792bac
use previous tests
mikejolley Nov 6, 2019
9e3ef5d
Show attribute control in sidebar
mikejolley Nov 6, 2019
56f8d95
Remove displayStyle option
mikejolley Nov 6, 2019
62711cf
Sort attributes by name
mikejolley Nov 6, 2019
505ce96
Merge branch 'master' into update/price-slider-constraints
mikejolley Nov 6, 2019
b02150c
Merge branch 'master' into experiment/filter-by-attribute
mikejolley Nov 6, 2019
b8a7780
Merge branch 'update/price-slider-constraints' into experiment/filter…
mikejolley Nov 6, 2019
e95e19c
Show more/less toggle
mikejolley Nov 6, 2019
d11aa29
Merge branch 'master' into experiment/filter-by-attribute
mikejolley Nov 7, 2019
18562a0
Use renderFrontend
mikejolley Nov 7, 2019
c5dfef4
Only sort when adding values
mikejolley Nov 7, 2019
9b955e8
Rename memo placeholder
mikejolley Nov 7, 2019
21c244d
More specific CSS for pointer
mikejolley Nov 7, 2019
df96edb
Update assets/js/base/hooks/use-query-state.js
mikejolley Nov 7, 2019
8aa6811
Remove always true taxonomy check
mikejolley Nov 7, 2019
0109336
Update assets/js/blocks/attribute-filter/block.js
mikejolley Nov 7, 2019
24bba5c
Merge branch 'experiment/filter-by-attribute' of https://github.com/w…
mikejolley Nov 7, 2019
0af54f9
Remove lodash join
mikejolley Nov 7, 2019
e830631
native js for string casting
mikejolley Nov 7, 2019
a0dc217
Move internal deps
mikejolley Nov 7, 2019
97561a6
hyphenate attributes
mikejolley Nov 7, 2019
171fb41
Correct data set names
mikejolley Nov 7, 2019
18cb5d8
Remove unwanted dependency
mikejolley Nov 7, 2019
81b8abb
Moving imports
mikejolley Nov 7, 2019
0ba0591
Missing deps
mikejolley Nov 7, 2019
ae21f20
replace yoda conditonal
mikejolley Nov 7, 2019
d9a691c
Missing deps
mikejolley Nov 7, 2019
1a8e4af
Missing deps
mikejolley Nov 7, 2019
b1a24d6
Check value exists
mikejolley Nov 7, 2019
b4bcd39
Remove undefined filter
mikejolley Nov 7, 2019
9c14ae7
renderedOptions usememo
mikejolley Nov 7, 2019
2270310
Set defaults in checkbox list
mikejolley Nov 7, 2019
bc8dbfd
Show more/less refactor
mikejolley Nov 7, 2019
ca426f4
Use getAdminLink
mikejolley Nov 7, 2019
4a02e64
Fix object length check
mikejolley Nov 7, 2019
cdb3f09
Correct AND query handling for counts
mikejolley Nov 7, 2019
0867c94
Merge branch 'master' into experiment/filter-by-attribute
mikejolley Nov 7, 2019
1d53d56
useQueryStateByContext
mikejolley Nov 7, 2019
e250eba
Add store rest endpoints
mikejolley Nov 7, 2019
fba12a2
Update assets/js/base/components/checkbox-list/index.js
mikejolley Nov 7, 2019
7631fb8
Update assets/js/base/components/checkbox-list/index.js
mikejolley Nov 7, 2019
4e1f250
Update assets/js/base/components/checkbox-list/index.js
mikejolley Nov 7, 2019
dc492a2
Update assets/js/blocks/attribute-filter/block.js
mikejolley Nov 7, 2019
7785207
Feedback
mikejolley Nov 7, 2019
53ba361
Merge branch 'master' into experiment/filter-by-attribute
mikejolley Nov 7, 2019
8c22ba0
Merge conflict
mikejolley Nov 7, 2019
129aebe
feedback
mikejolley Nov 8, 2019
4a77711
API readme
mikejolley Nov 8, 2019
0432ef7
Fix API relation queries for multiple attributes
mikejolley Nov 8, 2019
607aeee
Prevent all options flashing visible during loads
mikejolley Nov 8, 2019
ab2351f
null check
mikejolley Nov 8, 2019
ded36b6
Improve loading state
mikejolley Nov 8, 2019
e87da8c
Remove null options change - it's no longer needed
mikejolley Nov 8, 2019
bbcebcb
update from master
mikejolley Nov 11, 2019
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
191 changes: 191 additions & 0 deletions assets/js/base/components/checkbox-list/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/**
* External dependencies
*/
import { __, sprintf } from '@wordpress/i18n';
import PropTypes from 'prop-types';
import {
Fragment,
useCallback,
useMemo,
useState,
useEffect,
} from '@wordpress/element';
import classNames from 'classnames';

/**
* Internal dependencies
*/
import './style.scss';

/**
* Component used to show a list of checkboxes in a group.
*/
const CheckboxList = ( {
className,
onChange = () => {},
options = [],
isLoading = false,
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
limit = 10,
} ) => {
// Holds all checked options.
const [ checked, setChecked ] = useState( [] );
const [ showExpanded, setShowExpanded ] = useState( false );

useEffect( () => {
onChange( checked );
}, [ checked ] );

const placeholder = useMemo( () => {
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
return [ ...Array( 5 ) ].map( ( x, i ) => (
<li
key={ i }
style={ {
/* stylelint-disable */
width: Math.floor( Math.random() * 75 ) + 25 + '%',
} }
/>
) );
}, [] );

const onCheckboxChange = useCallback(
( event ) => {
const isChecked = event.target.checked;
const checkedValue = event.target.value;
const newChecked = checked.filter(
( value ) => value !== checkedValue
);

if ( isChecked ) {
newChecked.push( checkedValue );
newChecked.sort();
}

setChecked( newChecked );
},
[ checked ]
);

const renderedShowMore = useMemo( () => {
const optionCount = options.length;
return (
! showExpanded && (
<li key="show-more" className="show-more">
<button
onClick={ () => {
setShowExpanded( true );
} }
aria-expanded={ false }
aria-label={ sprintf(
__(
'Show %s more options',
'woo-gutenberg-products-block'
),
optionCount - limit
) }
>
{ // translators: %s number of options to reveal.
sprintf(
__(
'Show %s more',
'woo-gutenberg-products-block'
),
optionCount - limit
) }
</button>
</li>
)
);
}, [ options, limit, showExpanded ] );

const renderedShowLess = useMemo( () => {
return (
showExpanded && (
<li key="show-less" className="show-less">
<button
onClick={ () => {
setShowExpanded( false );
} }
aria-expanded={ true }
aria-label={ __(
'Show less options',
'woo-gutenberg-products-block'
) }
>
{ __( 'Show less', 'woo-gutenberg-products-block' ) }
</button>
</li>
)
);
}, [ showExpanded ] );

const renderedOptions = useMemo( () => {
// Truncate options if > the limit + 5.
const optionCount = options.length;
const shouldTruncateOptions = optionCount > limit + 5;
return (
<Fragment>
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
{ options.map( ( option, index ) => (
<Fragment key={ option.key }>
Copy link
Contributor

Choose a reason for hiding this comment

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

Fragment shouldn't be needed here, just put the key on the <li>

Copy link
Member Author

Choose a reason for hiding this comment

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

This fragment wraps 2 sibling <li> elements so is needed here?

Copy link
Contributor

Choose a reason for hiding this comment

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

You're right there is one Fragment that could remain. However there's a couple things:

  1. The inner level fragment should still not be needed.
  2. The key should be on the li items, not the Fragment. The problem with the Fragment having the key is that the "show more options" toggle will get re-rendered everytime the inner list re-renders even if it's not necessary.

The above points might not see important in the context of this particular usage, but it's pattern that could be more problematic in other contexts if it becomes a habit :). So I'm just promoting best practices here.

This code could get changed to:

useMemo( () => {
	// Truncate options if > the limit + 5.
	const optionCount = options.length;
	const shouldTruncateOptions = optionCount > limit + 5;
	return (
		<Fragment>
			{ options.map( ( option, index ) => (
				<li
					key={ option.key }
					{ ...shouldTruncateOptions &&
						! showExpanded &&
						index >= limit && { hidden: true } }
				>
					<input
						type="checkbox"
						id={ option.key }
						value={ option.key }
						onChange={ onCheckboxChange }
						checked={ checked.includes( option.key ) }
					/>
					<label htmlFor={ option.key }>{ option.label }</label>
				</li>
			) ) }
			{ shouldTruncateOptions &&
				options.length > limit &&
				renderedShowMore }
			{ shouldTruncateOptions && renderedShowLess }
		</Fragment>
	);
}, [
	options,
	checked,
	showExpanded,
	limit,
	onCheckboxChange,
	renderedShowLess,
	renderedShowMore,
] );

Even better though, would be to handle the renderedShowMore and renderedShowLess outside of this memoized function. I'd combine the renderedShowMore and renderedShowLess into one memoized function that spits out either of those or null based on the conditions. Then you could get rid of the last unnecessary fragment and end up with something like this:

<ul className={ classes }>
    { isLoading ? placeholder : renderedOptions  }
    { renderedToggleOptions }
</ul>

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm but by having the show more after the list items, if you tab through (with keyboard) when it expands you're at the bottom of the list rather than from the point of expansion.. that was my justification for having it appear inline. Originally I did it similar to you but only rendering visible list items. You were against that particular solution also.

Copy link
Contributor

Choose a reason for hiding this comment

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

🤔 hmm... I see your point. I tried both behaviours and what you have definitely works better from an a11y pov. Let's roll with it.

Copy link
Member Author

Choose a reason for hiding this comment

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

The problem with the Fragment having the key is that the "show more options" toggle will get re-rendered everytime the inner list re-renders even if it's not necessary.

Did we work around this by adding a key to the show more/show less li?

Copy link
Contributor

Choose a reason for hiding this comment

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

Did we work around this by adding a key to the show more/show less li?

Hmm... I don't think so (not sure honestly), but regardless I think in this case any accessibility improvements make it worth potential extra rendering for the time being (until we can think of a better pattern).

<li
{ ...shouldTruncateOptions &&
! showExpanded &&
index >= limit && { hidden: true } }
>
<input
type="checkbox"
id={ option.key }
value={ option.key }
onChange={ onCheckboxChange }
checked={ checked.includes( option.key ) }
/>
<label htmlFor={ option.key }>
{ option.label }
</label>
</li>
{ shouldTruncateOptions &&
index === limit - 1 &&
renderedShowMore }
</Fragment>
) ) }
{ shouldTruncateOptions && renderedShowLess }
</Fragment>
);
}, [
options,
checked,
showExpanded,
limit,
onCheckboxChange,
renderedShowLess,
renderedShowMore,
] );

const classes = classNames(
'wc-block-checkbox-list',
{
'is-loading': isLoading,
},
className
);

return (
<ul className={ classes }>
{ isLoading ? placeholder : renderedOptions }
</ul>
);
};

CheckboxList.propTypes = {
onChange: PropTypes.func,
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
options: PropTypes.arrayOf(
PropTypes.shape( {
key: PropTypes.string.isRequired,
label: PropTypes.node.isRequired,
} )
),
className: PropTypes.string,
isLoading: PropTypes.bool,
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
limit: PropTypes.number,
};

export default CheckboxList;
29 changes: 29 additions & 0 deletions assets/js/base/components/checkbox-list/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
.editor-styles-wrapper .wc-block-checkbox-list,
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
.wc-block-checkbox-list {
margin: 0;
padding: 0;
list-style: none outside;

li {
margin: 0 0 $gap-smallest;
padding: 0;
list-style: none outside;
}

li.show-more,
li.show-less {
button {
background: none;
border: none;
padding: 0;
text-decoration: underline;
cursor: pointer;
}
}

&.is-loading {
li {
@include placeholder();
}
}
}
9 changes: 7 additions & 2 deletions assets/js/base/hooks/use-collection-header.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ import { useShallowEqual } from './use-shallow-equal';
* loading (true) or not.
*/
export const useCollectionHeader = ( headerKey, options ) => {
const { namespace, resourceName, resourceValues, query } = options;
const {
namespace,
resourceName,
resourceValues = [],
query = {},
} = options;
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
if ( ! namespace || ! resourceName ) {
throw new Error(
'The options object must have valid values for the namespace and ' +
Expand All @@ -61,7 +66,7 @@ export const useCollectionHeader = ( headerKey, options ) => {
resourceName,
currentQuery,
currentResourceValues,
].filter( ( item ) => typeof item !== 'undefined' );
];
return {
value: store.getCollectionHeader( ...args ),
isLoading: store.hasFinishedResolution(
Expand Down
9 changes: 7 additions & 2 deletions assets/js/base/hooks/use-collection.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,12 @@ import { useShallowEqual } from './use-shallow-equal';
* loading (true) or not.
*/
export const useCollection = ( options ) => {
const { namespace, resourceName, resourceValues, query } = options;
const {
namespace,
resourceName,
resourceValues = [],
query = {},
} = options;
mikejolley marked this conversation as resolved.
Show resolved Hide resolved
if ( ! namespace || ! resourceName ) {
throw new Error(
'The options object must have valid values for the namespace and ' +
Expand All @@ -55,7 +60,7 @@ export const useCollection = ( options ) => {
resourceName,
currentQuery,
currentResourceValues,
].filter( ( item ) => typeof item !== 'undefined' );
];
return {
results: store.getCollection( ...args ),
isLoading: ! store.hasFinishedResolution(
Expand Down
9 changes: 7 additions & 2 deletions assets/js/base/hooks/use-query-state.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,20 @@ export const useQueryStateByContext = ( context ) => {
*
* @param {string} context What context to retrieve the query state for.
* @param {*} queryKey The specific query key to retrieve the value for.
* @param {*} defaultValue Default value if query does not exist.
*
* @return {*} Whatever value is set at the query state index using the
* provided context and query key.
*/
export const useQueryStateByKey = ( context, queryKey ) => {
export const useQueryStateByKey = (
context,
queryKey,
defaultValue
) => {
const queryValue = useSelect(
( select ) => {
const store = select( storeKey );
return store.getValueForQueryKey( context, queryKey, undefined );
return store.getValueForQueryKey( context, queryKey, defaultValue );
},
[ context, queryKey ]
);
Expand Down
Loading