-
Notifications
You must be signed in to change notification settings - Fork 215
Filter all products block by attribute #1127
Changes from 68 commits
47a36b2
dd1fe39
7f36fdf
63c3489
2a4bf48
3f2e1df
378d861
8941166
d1695c5
b92b99d
7d90236
98fae73
4aeaed1
dcb704c
7c26a00
2792bac
9e3ef5d
56f8d95
62711cf
505ce96
b02150c
b8a7780
e95e19c
d11aa29
18562a0
c5dfef4
9b955e8
21c244d
df96edb
8aa6811
0109336
24bba5c
0af54f9
e830631
a0dc217
97561a6
171fb41
18cb5d8
81b8abb
0ba0591
ae21f20
d9a691c
1a8e4af
b1a24d6
b4bcd39
9c14ae7
2270310
bc8dbfd
ca426f4
4a02e64
cdb3f09
0867c94
1d53d56
e250eba
fba12a2
7631fb8
4e1f250
dc492a2
7785207
53ba361
8c22ba0
129aebe
4a77711
0432ef7
607aeee
ab2351f
ded36b6
e87da8c
bbcebcb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
| 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 }> | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fragment shouldn't be needed here, just put the
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This fragment wraps 2 sibling
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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:
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 <ul className={ classes }>
{ isLoading ? placeholder : renderedOptions }
{ renderedToggleOptions }
</ul>
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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.
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
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; | ||
| 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(); | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.