Add dropdown version of Filter by Stock Status (https://github.com/woocommerce/woocommerce-blocks/pull/7831)

* Extend Filter by Stock Editor options with dropdown and single/multiple choice

* Add dropdown implementation for Filter by Stock Status

* Adjust font-sizes to the rest of the filters

* Add tests to Filter by Stock: dropdown and list variants

* Change test file extension from .js to .tsx, so it handles types as well

* Add E2E test to Filter by Stock checking if display style can be toggled

* When typing in Filter by Stock dropdown, handle the space so it highlights the suggestions

* Change the name of the filter blocks in the test files

* Remove unnecessary waiting step in E2E test for Filter by Stock

toMatchElement waits for an element for 30s by itself, hence waitForSelector usage was removed

* Improve the STOCK_STATUS_OPTIONS type handling

* Extract onDropdownChange function instead of inline arrow function

* Fix overlaping dropdown content with the wrapper when Filter by Stock was set to single
This commit is contained in:
kmanijak 2022-12-14 08:16:37 +01:00 committed by GitHub
parent 6c40524dfe
commit 6aa8a72f8e
12 changed files with 946 additions and 130 deletions

View File

@ -16,7 +16,7 @@ jest.mock( '@woocommerce/base-context/hooks', () => ( {
...jest.requireActual( '@woocommerce/base-context/hooks' ),
} ) );
const setWindowUrl = ( { url }: SetWindowUrlParams ) => {
const setWindowUrl = ( { url }: { url: string } ) => {
global.window = Object.create( window );
Object.defineProperty( window, 'location', {
value: {

View File

@ -39,6 +39,14 @@
"type": "boolean",
"default": false
},
"displayStyle": {
"type": "string",
"default": "list"
},
"selectType": {
"type": "string",
"default": "multiple"
},
"isPreview": {
"type": "boolean",
"default": false

View File

@ -3,7 +3,12 @@
*/
import { __, sprintf } from '@wordpress/i18n';
import { speak } from '@wordpress/a11y';
import { usePrevious, useShallowEqual } from '@woocommerce/base-hooks';
import { Icon, chevronDown } from '@wordpress/icons';
import {
usePrevious,
useShallowEqual,
useBorderProps,
} from '@woocommerce/base-hooks';
import {
useQueryStateByKey,
useQueryStateByContext,
@ -22,11 +27,13 @@ import FilterSubmitButton from '@woocommerce/base-components/filter-submit-butto
import FilterResetButton from '@woocommerce/base-components/filter-reset-button';
import FilterTitlePlaceholder from '@woocommerce/base-components/filter-placeholder';
import Label from '@woocommerce/base-components/filter-element-label';
import FormTokenField from '@woocommerce/base-components/form-token-field';
import isShallowEqual from '@wordpress/is-shallow-equal';
import { decodeEntities } from '@wordpress/html-entities';
import { isBoolean, objectHasProp } from '@woocommerce/types';
import { addQueryArgs, removeQueryArgs } from '@wordpress/url';
import { changeUrl, PREFIX_QUERY_ARG_FILTER_TYPE } from '@woocommerce/utils';
import { difference } from 'lodash';
import classnames from 'classnames';
/**
@ -34,8 +41,8 @@ import classnames from 'classnames';
*/
import { previewOptions } from './preview';
import './style.scss';
import { getActiveFilters } from './utils';
import { Attributes, DisplayOption } from './types';
import { formatSlug, getActiveFilters, generateUniqueId } from './utils';
import { Attributes, DisplayOption, Current } from './types';
import { useSetWraperVisibility } from '../filter-wrapper/context';
export const QUERY_PARAM_KEY = PREFIX_QUERY_ARG_FILTER_TYPE + 'stock_status';
@ -74,7 +81,7 @@ const StockStatusFilterBlock = ( {
? []
: getSettingWithCoercion( 'product_ids', [], Array.isArray );
const STOCK_STATUS_OPTIONS = useRef(
const STOCK_STATUS_OPTIONS: { current: Current } = useRef(
getSetting( 'hideOutOfStockItems', false )
? otherStockStatusOptions
: { outofstock, ...otherStockStatusOptions }
@ -127,6 +134,15 @@ const StockStatusFilterBlock = ( {
[ filteredCounts ]
);
/*
FormTokenField forces the dropdown to reopen on reset, so we create a unique ID to use as the components key.
This will force the component to remount on reset when we change this value.
More info: https://github.com/woocommerce/woocommerce-blocks/pull/6920#issuecomment-1222402482
*/
const [ remountKey, setRemountKey ] = useState( generateUniqueId() );
const borderProps = useBorderProps( blockAttributes );
/**
* Compare intersection of all stock statuses and filtered counts to get a list of options to display.
*/
@ -173,11 +189,15 @@ const StockStatusFilterBlock = ( {
count={ blockAttributes.showCounts ? count : null }
/>
),
textLabel: blockAttributes.showCounts
? `${ decodeEntities( status.name ) } (${ count })`
: decodeEntities( status.name ),
};
} )
.filter( ( option ): option is DisplayOption => !! option );
setDisplayedOptions( newOptions );
setRemountKey( generateUniqueId() );
}, [
blockAttributes.showCounts,
blockAttributes.isPreview,
@ -221,6 +241,8 @@ const StockStatusFilterBlock = ( {
changeUrl( newUrl );
};
const allowsMultipleOptions = blockAttributes.selectType !== 'single';
const onSubmit = useCallback(
( checkedOptions ) => {
if ( isEditor ) {
@ -327,23 +349,59 @@ const StockStatusFilterBlock = ( {
const previouslyChecked = checked.includes( checkedValue );
const newChecked = checked.filter(
( value ) => value !== checkedValue
);
if ( ! previouslyChecked ) {
newChecked.push( checkedValue );
newChecked.sort();
announceFilterChange( { filterAdded: checkedValue } );
} else {
announceFilterChange( { filterRemoved: checkedValue } );
if ( ! allowsMultipleOptions ) {
const newChecked = previouslyChecked ? [] : [ checkedValue ];
announceFilterChange(
previouslyChecked
? { filterRemoved: checkedValue }
: { filterAdded: checkedValue }
);
setChecked( newChecked );
return;
}
if ( previouslyChecked ) {
const newChecked = checked.filter(
( value ) => value !== checkedValue
);
announceFilterChange( { filterRemoved: checkedValue } );
setChecked( newChecked );
return;
}
const newChecked = [ ...checked, checkedValue ].sort();
announceFilterChange( { filterAdded: checkedValue } );
setChecked( newChecked );
},
[ checked, displayedOptions ]
[ checked, allowsMultipleOptions, displayedOptions ]
);
const onDropdownChange = ( tokens: string[] ) => {
if ( ! allowsMultipleOptions && tokens.length > 1 ) {
tokens = tokens.slice( -1 );
}
tokens = tokens.map( ( token ) => {
const displayOption = displayedOptions.find(
( option ) => option.value === token
);
return displayOption ? displayOption.value : token;
} );
const added = difference( tokens, checked );
if ( added.length === 1 ) {
return onChange( added[ 0 ] );
}
const removed = difference( checked, tokens );
if ( removed.length === 1 ) {
onChange( removed[ 0 ] );
}
};
if ( ! filteredCountsLoading && displayedOptions.length === 0 ) {
setWrapperVisibility( false );
return null;
@ -385,18 +443,76 @@ const StockStatusFilterBlock = ( {
<>
{ ! isEditor && blockAttributes.heading && filterHeading }
<div
className={ classnames( 'wc-block-stock-filter', {
'is-loading': isLoading,
} ) }
className={ classnames(
'wc-block-stock-filter',
`style-${ blockAttributes.displayStyle }`,
{
'is-loading': isLoading,
}
) }
>
<CheckboxList
className={ 'wc-block-stock-filter-list' }
options={ displayedOptions }
checked={ checked }
onChange={ onChange }
isLoading={ isLoading }
isDisabled={ isDisabled }
/>
{ blockAttributes.displayStyle === 'dropdown' ? (
<>
<FormTokenField
key={ remountKey }
className={ classnames( borderProps.className, {
'single-selection': ! allowsMultipleOptions,
'is-loading': isLoading,
} ) }
style={ { ...borderProps.style } }
suggestions={ displayedOptions
.filter(
( option ) =>
! checked.includes( option.value )
)
.map( ( option ) => option.value ) }
disabled={ isLoading }
placeholder={ __(
'Select stock status',
'woo-gutenberg-products-block'
) }
onChange={ onDropdownChange }
value={ checked }
displayTransform={ ( value: string ) => {
const result = displayedOptions.find(
( option ) => option.value === value
);
return result ? result.textLabel : value;
} }
saveTransform={ formatSlug }
messages={ {
added: __(
'Stock filter added.',
'woo-gutenberg-products-block'
),
removed: __(
'Stock filter removed.',
'woo-gutenberg-products-block'
),
remove: __(
'Remove stock filter.',
'woo-gutenberg-products-block'
),
__experimentalInvalid: __(
'Invalid stock filter.',
'woo-gutenberg-products-block'
),
} }
/>
{ allowsMultipleOptions && (
<Icon icon={ chevronDown } size={ 30 } />
) }
</>
) : (
<CheckboxList
className={ 'wc-block-stock-filter-list' }
options={ displayedOptions }
checked={ checked }
onChange={ onChange }
isLoading={ isLoading }
isDisabled={ isDisabled }
/>
) }
</div>
{
<div className="wc-block-stock-filter__actions">

View File

@ -4,14 +4,18 @@
import { __ } from '@wordpress/i18n';
import classnames from 'classnames';
import { useBlockProps, InspectorControls } from '@wordpress/block-editor';
import BlockTitle from '@woocommerce/editor-components/block-title';
import type { BlockEditProps } from '@wordpress/blocks';
import {
Disabled,
PanelBody,
ToggleControl,
withSpokenMessages,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControl as ToggleGroupControl,
// eslint-disable-next-line @wordpress/no-unsafe-wp-apis
__experimentalToggleGroupControlOption as ToggleGroupControlOption,
} from '@wordpress/components';
import BlockTitle from '@woocommerce/editor-components/block-title';
import type { BlockEditProps } from '@wordpress/blocks';
/**
* Internal dependencies
@ -26,8 +30,15 @@ const Edit = ( {
attributes,
setAttributes,
}: BlockEditProps< Attributes > ) => {
const { className, heading, headingLevel, showCounts, showFilterButton } =
attributes;
const {
className,
heading,
headingLevel,
showCounts,
showFilterButton,
selectType,
displayStyle,
} = attributes;
const blockProps = useBlockProps( {
className: classnames( 'wc-block-stock-filter', className ),
@ -37,7 +48,10 @@ const Edit = ( {
return (
<InspectorControls key="inspector">
<PanelBody
title={ __( 'Content', 'woo-gutenberg-products-block' ) }
title={ __(
'Display Settings',
'woo-gutenberg-products-block'
) }
>
<ToggleControl
label={ __(
@ -51,10 +65,62 @@ const Edit = ( {
} )
}
/>
</PanelBody>
<PanelBody
title={ __( 'Settings', 'woo-gutenberg-products-block' ) }
>
<ToggleGroupControl
label={ __(
'Allow selecting multiple options?',
'woo-gutenberg-products-block'
) }
value={ selectType || 'multiple' }
onChange={ ( value: string ) =>
setAttributes( {
selectType: value,
} )
}
className="wc-block-attribute-filter__multiple-toggle"
>
<ToggleGroupControlOption
value="multiple"
label={ __(
'Multiple',
'woo-gutenberg-products-block'
) }
/>
<ToggleGroupControlOption
value="single"
label={ __(
'Single',
'woo-gutenberg-products-block'
) }
/>
</ToggleGroupControl>
<ToggleGroupControl
label={ __(
'Display Style',
'woo-gutenberg-products-block'
) }
value={ displayStyle }
onChange={ ( value ) =>
setAttributes( {
displayStyle: value,
} )
}
className="wc-block-attribute-filter__display-toggle"
>
<ToggleGroupControlOption
value="list"
label={ __(
'List',
'woo-gutenberg-products-block'
) }
/>
<ToggleGroupControlOption
value="dropdown"
label={ __(
'Dropdown',
'woo-gutenberg-products-block'
) }
/>
</ToggleGroupControl>
<ToggleControl
label={ __(
"Show 'Apply filters' button",

View File

@ -8,15 +8,18 @@ export const previewOptions = [
value: 'preview-1',
name: 'In Stock',
label: <Label name="In Stock" count={ 3 } />,
textLabel: 'In Stock (3)',
},
{
value: 'preview-2',
name: 'Out of sotck',
name: 'Out of stock',
label: <Label name="Out of stock" count={ 3 } />,
textLabel: 'Out of stock (3)',
},
{
value: 'preview-3',
name: 'On backorder',
label: <Label name="On backorder" count={ 2 } />,
textLabel: 'On backorder (2)',
},
];

View File

@ -1,3 +1,5 @@
@import "../shared/styles/style";
.wp-block-woocommerce-stock-filter {
h1,
h2,
@ -33,6 +35,116 @@
}
}
}
&.style-dropdown {
@include includeFormTokenFieldFix();
position: relative;
display: flex;
gap: $gap;
align-items: flex-start;
.wc-block-components-filter-submit-button {
height: 36px;
line-height: 1;
}
> svg {
position: absolute;
right: 8px;
top: 50%;
transform: translateY(-50%);
pointer-events: none;
}
}
.wc-blocks-components-form-token-field-wrapper {
flex-grow: 1;
max-width: unset;
width: 0;
height: max-content;
&:not(.is-loading) {
border: 1px solid $gray-700 !important;
border-radius: 4px;
}
&.is-loading {
border-radius: em(4px);
}
.components-form-token-field {
border-radius: inherit;
}
}
.wc-blocks-components-form-token-field-wrapper .components-form-token-field__input-container {
@include reset-typography();
border: 0;
padding: $gap-smaller;
border-radius: inherit;
.components-form-token-field__input {
@include font-size(small);
&::placeholder {
color: $black;
}
}
.components-form-token-field__suggestions-list {
border: 1px solid $gray-700;
border-radius: 4px;
margin-top: $gap-smaller;
max-height: 21em;
.components-form-token-field__suggestion {
color: $black;
border: 1px solid $gray-400;
border-radius: 4px;
margin: $gap-small;
padding: $gap-small;
}
}
.components-form-token-field__token,
.components-form-token-field__suggestion {
@include font-size(small);
}
}
.wc-block-components-product-rating {
margin-bottom: 0;
}
}
.wc-blocks-components-form-token-field-wrapper:not(.single-selection) .components-form-token-field__input-container {
padding: $gap-smallest 30px $gap-smallest $gap-smaller;
.components-form-token-field__token-text {
background-color: $white;
border: 1px solid;
border-right: 0;
border-radius: 25px 0 0 25px;
padding: em($gap-smallest) em($gap-smaller) em($gap-smallest) em($gap-small);
line-height: 22px;
}
> .components-form-token-field__input {
margin: em($gap-smallest) 0;
}
.components-button.components-form-token-field__remove-token {
background-color: $white;
border: 1px solid;
border-left: 0;
border-radius: 0 25px 25px 0;
padding: 1px em($gap-smallest) 0 0;
&.has-icon svg {
background-color: $gray-200;
border-radius: 25px;
}
}
}
.wc-block-stock-filter__actions {

View File

@ -1,9 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Testing stock filter renders the stock filter block 1`] = `
exports[`Filter by Stock block renders the stock filter block 1`] = `
<div>
<div
class="wc-block-stock-filter"
class="wc-block-stock-filter style-list"
>
<ul
class="wc-block-checkbox-list wc-block-components-checkbox-list wc-block-stock-filter-list"
@ -109,10 +110,11 @@ exports[`Testing stock filter renders the stock filter block 1`] = `
</div>
`;
exports[`Testing stock filter renders the stock filter block with the filter button 1`] = `
exports[`Filter by Stock block renders the stock filter block with the filter button 1`] = `
<div>
<div
class="wc-block-stock-filter"
class="wc-block-stock-filter style-list"
>
<ul
class="wc-block-checkbox-list wc-block-components-checkbox-list wc-block-stock-filter-list"
@ -234,10 +236,11 @@ exports[`Testing stock filter renders the stock filter block with the filter but
</div>
`;
exports[`Testing stock filter renders the stock filter block with the product counts 1`] = `
exports[`Filter by Stock block renders the stock filter block with the product counts 1`] = `
<div>
<div
class="wc-block-stock-filter"
class="wc-block-stock-filter style-list"
>
<ul
class="wc-block-checkbox-list wc-block-components-checkbox-list wc-block-stock-filter-list"

View File

@ -1,87 +0,0 @@
/**
* Internal dependencies
*/
/**
* External dependencies
*/
import { render } from '@testing-library/react';
import { default as fetchMock } from 'jest-fetch-mock';
/**
* Internal dependencies
*/
import Block from '../block';
import { allSettings } from '../../../settings/shared/settings-init';
const mockResults = {
stock_status_counts: [
{ status: 'instock', count: '18' },
{ status: 'outofstock', count: '1' },
{ status: 'onbackorder', count: '5' },
],
};
jest.mock( '@woocommerce/base-context/hooks', () => {
return {
...jest.requireActual( '@woocommerce/base-context/hooks' ),
useCollectionData: () => ( { isLoading: false, results: mockResults } ),
};
} );
jest.mock( '@woocommerce/settings', () => {
return {
...jest.requireActual( '@woocommerce/settings' ),
getSettingWithCoercion: jest
.fn()
.mockImplementation( ( key, defaultValue ) => {
if ( key === 'has_filterable_products' ) {
return true;
}
return defaultValue;
} ),
};
} );
const StockFilterBlock = ( props ) => <Block { ...props } />;
describe( 'Testing stock filter', () => {
beforeEach( () => {
allSettings.stockStatusOptions = {
instock: 'In stock',
outofstock: 'Out of stock',
onbackorder: 'On backorder',
};
} );
afterEach( () => {
fetchMock.resetMocks();
} );
it( 'renders the stock filter block', async () => {
const { container } = render(
<StockFilterBlock attributes={ { isPreview: false } } />
);
expect( container ).toMatchSnapshot();
} );
it( 'renders the stock filter block with the filter button', async () => {
const { container } = render(
<StockFilterBlock
attributes={ { isPreview: false, showFilterButton: true } }
/>
);
expect( container ).toMatchSnapshot();
} );
it( 'renders the stock filter block with the product counts', async () => {
const { container } = render(
<StockFilterBlock
attributes={ {
isPreview: false,
showCounts: true,
} }
/>
);
expect( container ).toMatchSnapshot();
} );
} );

View File

@ -0,0 +1,552 @@
/**
* External dependencies
*/
import React from 'react';
import { render, screen, within, waitFor } from '@testing-library/react';
import { default as fetchMock } from 'jest-fetch-mock';
import userEvent from '@testing-library/user-event';
/**
* Internal dependencies
*/
import Block from '../block';
import { allSettings } from '../../../settings/shared/settings-init';
import { Attributes } from '../types';
const setWindowUrl = ( { url }: { url: string } ) => {
global.window = Object.create( window );
Object.defineProperty( window, 'location', {
value: {
href: url,
},
writable: true,
} );
};
const mockResults = {
stock_status_counts: [
{ status: 'instock', count: '18' },
{ status: 'outofstock', count: '1' },
{ status: 'onbackorder', count: '5' },
],
};
jest.mock( '@woocommerce/base-context/hooks', () => {
return {
...jest.requireActual( '@woocommerce/base-context/hooks' ),
useCollectionData: () => ( { isLoading: false, results: mockResults } ),
};
} );
jest.mock( '@woocommerce/settings', () => {
return {
...jest.requireActual( '@woocommerce/settings' ),
getSettingWithCoercion: jest
.fn()
.mockImplementation( ( key, defaultValue ) => {
if ( key === 'has_filterable_products' ) {
return true;
}
return defaultValue;
} ),
};
} );
type DisplayStyle = 'list' | 'dropdown';
type SelectType = 'single' | 'multiple';
interface SetupParams {
filterStock?: string;
displayStyle?: DisplayStyle;
selectType?: SelectType;
showCounts?: boolean;
showFilterButton?: boolean;
}
const selectors = {
list: '.wc-block-stock-filter.style-list',
suggestionsContainer: '.components-form-token-field__suggestions-list',
chipsContainer: '.components-form-token-field__token',
};
const setup = ( params: SetupParams = {} ) => {
const url = `http://woo.local/${
params.filterStock ? '?filter_stock_status=' + params.filterStock : ''
}`;
setWindowUrl( { url } );
const attributes: Attributes = {
displayStyle: params.displayStyle || 'list',
selectType: params.selectType || 'single',
showCounts: params.showCounts !== undefined ? params.showCounts : true,
showFilterButton:
params.showFilterButton !== undefined
? params.showFilterButton
: true,
isPreview: false,
heading: '',
headingLevel: 3,
};
const { container, ...utils } = render(
<Block attributes={ attributes } />
);
const getList = () => container.querySelector( selectors.list );
const getDropdown = () => screen.queryByRole( 'combobox' );
const getChipsContainers = () =>
container.querySelectorAll( selectors.chipsContainer );
const getSuggestionsContainer = () =>
container.querySelector( selectors.suggestionsContainer );
const getChips = ( value: string ) => {
const chipsContainers = getChipsContainers();
const chips = Array.from( chipsContainers ).find( ( chipsContainer ) =>
chipsContainer
? within( chipsContainer ).queryByText( value, {
exact: false,
ignore: '.components-visually-hidden',
} )
: false
);
return chips || null;
};
const getSuggestion = ( value: string ) => {
const suggestionsContainer = getSuggestionsContainer();
if ( suggestionsContainer ) {
return within( suggestionsContainer ).queryByText( value, {
exact: false,
} );
}
return null;
};
const getCheckbox = ( value: string ) => {
const checkboxesContainer = getList();
const checkboxes = checkboxesContainer
? checkboxesContainer.querySelectorAll( 'input' )
: [];
const checkbox = Array.from( checkboxes ).find(
( input ) => input.id === value
);
return checkbox;
};
const getRemoveButtonFromChips = ( chips: HTMLElement | null ) =>
chips ? within( chips ).getByLabelText( 'Remove stock filter.' ) : null;
const inStockLabel = 'In stock';
const outOfStockLabel = 'Out of stock';
const onBackstockLabel = 'On backorder';
const inStockId = 'instock';
const outOfStockId = 'outofstock';
const onBackstoreId = 'onbackorder';
const getInStockChips = () => getChips( inStockLabel );
const getOutOfStockChips = () => getChips( outOfStockLabel );
const getOnBackorderChips = () => getChips( onBackstockLabel );
const getInStockSuggestion = () => getSuggestion( inStockLabel );
const getOutOfStockSuggestion = () => getSuggestion( outOfStockLabel );
const getOnBackorderSuggestion = () => getSuggestion( onBackstockLabel );
const getInStockCheckbox = () => getCheckbox( inStockId );
const getOutOfStockCheckbox = () => getCheckbox( outOfStockId );
const getOnBackorderCheckbox = () => getCheckbox( onBackstoreId );
return {
...utils,
container,
getDropdown,
getList,
getInStockChips,
getOutOfStockChips,
getOnBackorderChips,
getInStockSuggestion,
getOutOfStockSuggestion,
getOnBackorderSuggestion,
getInStockCheckbox,
getOutOfStockCheckbox,
getOnBackorderCheckbox,
getRemoveButtonFromChips,
};
};
interface SetupParams {
filterStock?: string;
displayStyle?: DisplayStyle;
selectType?: SelectType;
}
const setupSingleChoiceList = ( filterStock = 'instock' ) =>
setup( {
filterStock,
displayStyle: 'list',
selectType: 'single',
} );
const setupMultipleChoiceList = ( filterStock = 'instock' ) =>
setup( {
filterStock,
displayStyle: 'list',
selectType: 'multiple',
} );
const setupSingleChoiceDropdown = ( filterStock = 'instock' ) =>
setup( {
filterStock,
displayStyle: 'dropdown',
selectType: 'single',
} );
const setupMultipleChoiceDropdown = ( filterStock = 'instock' ) =>
setup( {
filterStock,
displayStyle: 'dropdown',
selectType: 'multiple',
} );
describe( 'Filter by Stock block', () => {
beforeEach( () => {
allSettings.stockStatusOptions = {
instock: 'In stock',
outofstock: 'Out of stock',
onbackorder: 'On backorder',
};
} );
afterEach( () => {
fetchMock.resetMocks();
} );
it( 'renders the stock filter block', async () => {
const { container } = setup( {
showFilterButton: false,
showCounts: false,
} );
expect( container ).toMatchSnapshot();
} );
it( 'renders the stock filter block with the filter button', async () => {
const { container } = setup( {
showFilterButton: true,
showCounts: false,
} );
expect( container ).toMatchSnapshot();
} );
it( 'renders the stock filter block with the product counts', async () => {
const { container } = setup( {
showFilterButton: false,
showCounts: true,
} );
expect( container ).toMatchSnapshot();
} );
describe( 'Single choice Dropdown', () => {
test( 'renders dropdown', () => {
const { getDropdown, getList } = setupSingleChoiceDropdown();
expect( getDropdown() ).toBeInTheDocument();
expect( getList() ).toBeNull();
} );
test( 'renders chips based on URL params', () => {
const ratingParam = 'instock';
const { getInStockChips, getOutOfStockChips, getOnBackorderChips } =
setupSingleChoiceDropdown( ratingParam );
expect( getInStockChips() ).toBeInTheDocument();
expect( getOutOfStockChips() ).toBeNull();
expect( getOnBackorderChips() ).toBeNull();
} );
test( 'replaces chosen option when another one is clicked', () => {
const ratingParam = 'instock';
const {
getDropdown,
getInStockChips,
getOutOfStockChips,
getOutOfStockSuggestion,
} = setupSingleChoiceDropdown( ratingParam );
expect( getInStockChips() ).toBeInTheDocument();
expect( getOutOfStockChips() ).toBeNull();
const dropdown = getDropdown();
if ( dropdown ) {
userEvent.click( dropdown );
}
const outOfStockSuggestion = getOutOfStockSuggestion();
if ( outOfStockSuggestion ) {
userEvent.click( outOfStockSuggestion );
}
expect( getInStockChips() ).toBeNull();
expect( getOutOfStockChips() ).toBeInTheDocument();
} );
test( 'removes the option when the X button is clicked', () => {
const ratingParam = 'outofstock';
const {
getInStockChips,
getOutOfStockChips,
getOnBackorderChips,
getRemoveButtonFromChips,
} = setupMultipleChoiceDropdown( ratingParam );
expect( getInStockChips() ).toBeNull();
expect( getOutOfStockChips() ).toBeInTheDocument();
expect( getOnBackorderChips() ).toBeNull();
const removeOutOfStockButton = getRemoveButtonFromChips(
getOutOfStockChips()
);
if ( removeOutOfStockButton ) {
userEvent.click( removeOutOfStockButton );
}
expect( getInStockChips() ).toBeNull();
expect( getOutOfStockChips() ).toBeNull();
expect( getOnBackorderChips() ).toBeNull();
} );
} );
describe( 'Multiple choice Dropdown', () => {
test( 'renders dropdown', () => {
const { getDropdown, getList } = setupMultipleChoiceDropdown();
expect( getDropdown() ).toBeDefined();
expect( getList() ).toBeNull();
} );
test( 'renders chips based on URL params', () => {
const ratingParam = 'instock,onbackorder';
const { getInStockChips, getOutOfStockChips, getOnBackorderChips } =
setupMultipleChoiceDropdown( ratingParam );
expect( getInStockChips() ).toBeInTheDocument();
expect( getOutOfStockChips() ).toBeNull();
expect( getOnBackorderChips() ).toBeInTheDocument();
} );
test( 'adds chosen option to another one that is clicked', async () => {
const ratingParam = 'onbackorder';
const {
getDropdown,
getInStockChips,
getOutOfStockChips,
getOnBackorderChips,
getInStockSuggestion,
getOutOfStockSuggestion,
} = setupMultipleChoiceDropdown( ratingParam );
expect( getInStockChips() ).toBeNull();
expect( getOutOfStockChips() ).toBeNull();
expect( getOnBackorderChips() ).toBeInTheDocument();
const dropdown = getDropdown();
if ( dropdown ) {
userEvent.click( dropdown );
}
const inStockSuggestion = getInStockSuggestion();
if ( inStockSuggestion ) {
userEvent.click( inStockSuggestion );
}
expect( getInStockChips() ).toBeInTheDocument();
expect( getOutOfStockChips() ).toBeNull();
expect( getOnBackorderChips() ).toBeInTheDocument();
const freshDropdown = getDropdown();
if ( freshDropdown ) {
userEvent.click( freshDropdown );
}
const outOfStockSuggestion = getOutOfStockSuggestion();
if ( outOfStockSuggestion ) {
userEvent.click( outOfStockSuggestion );
}
await waitFor( () => {
expect( getInStockChips() ).toBeInTheDocument();
expect( getOutOfStockChips() ).toBeInTheDocument();
expect( getOnBackorderChips() ).toBeInTheDocument();
} );
} );
test( 'removes the option when the X button is clicked', () => {
const ratingParam = 'instock,outofstock,onbackorder';
const {
getInStockChips,
getOutOfStockChips,
getOnBackorderChips,
getRemoveButtonFromChips,
} = setupMultipleChoiceDropdown( ratingParam );
expect( getInStockChips() ).toBeInTheDocument();
expect( getOutOfStockChips() ).toBeInTheDocument();
expect( getOnBackorderChips() ).toBeInTheDocument();
const removeOutOfStockButton = getRemoveButtonFromChips(
getOutOfStockChips()
);
if ( removeOutOfStockButton ) {
userEvent.click( removeOutOfStockButton );
}
expect( getInStockChips() ).toBeInTheDocument();
expect( getOutOfStockChips() ).toBeNull();
expect( getOnBackorderChips() ).toBeInTheDocument();
} );
} );
describe( 'Single choice List', () => {
test( 'renders list', () => {
const { getDropdown, getList } = setupSingleChoiceList();
expect( getDropdown() ).toBeNull();
expect( getList() ).toBeInTheDocument();
} );
test( 'renders checked options based on URL params', () => {
const ratingParam = 'instock';
const {
getInStockCheckbox,
getOutOfStockCheckbox,
getOnBackorderCheckbox,
} = setupSingleChoiceList( ratingParam );
expect( getInStockCheckbox()?.checked ).toBeTruthy();
expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
} );
test( 'replaces chosen option when another one is clicked', async () => {
const ratingParam = 'outofstock';
const {
getInStockCheckbox,
getOutOfStockCheckbox,
getOnBackorderCheckbox,
} = setupSingleChoiceList( ratingParam );
expect( getInStockCheckbox()?.checked ).toBeFalsy();
expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
const onBackorderCheckbox = getOnBackorderCheckbox();
if ( onBackorderCheckbox ) {
userEvent.click( onBackorderCheckbox );
}
expect( getInStockCheckbox()?.checked ).toBeFalsy();
expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
} );
test( 'removes the option when it is clicked again', async () => {
const ratingParam = 'onbackorder';
const {
getInStockCheckbox,
getOutOfStockCheckbox,
getOnBackorderCheckbox,
} = setupMultipleChoiceList( ratingParam );
expect( getInStockCheckbox()?.checked ).toBeFalsy();
expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
const onBackorderCheckbox = getOnBackorderCheckbox();
if ( onBackorderCheckbox ) {
userEvent.click( onBackorderCheckbox );
}
await waitFor( () => {
expect( getInStockCheckbox()?.checked ).toBeFalsy();
expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
} );
} );
} );
describe( 'Multiple choice List', () => {
test( 'renders list', () => {
const { getDropdown, getList } = setupMultipleChoiceList();
expect( getDropdown() ).toBeNull();
expect( getList() ).toBeInTheDocument();
} );
test( 'renders chips based on URL params', () => {
const ratingParam = 'instock,onbackorder';
const {
getInStockCheckbox,
getOutOfStockCheckbox,
getOnBackorderCheckbox,
} = setupMultipleChoiceList( ratingParam );
expect( getInStockCheckbox()?.checked ).toBeTruthy();
expect( getOutOfStockCheckbox()?.checked ).toBeFalsy();
expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
} );
test( 'adds chosen option to another one that is clicked', async () => {
const ratingParam = 'outofstock,onbackorder';
const {
getInStockCheckbox,
getOutOfStockCheckbox,
getOnBackorderCheckbox,
} = setupMultipleChoiceList( ratingParam );
expect( getInStockCheckbox()?.checked ).toBeFalsy();
expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
const inStockCheckbox = getInStockCheckbox();
if ( inStockCheckbox ) {
userEvent.click( inStockCheckbox );
}
await waitFor( () => {
expect( getInStockCheckbox()?.checked ).toBeTruthy();
expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
expect( getOnBackorderCheckbox()?.checked ).toBeTruthy();
} );
} );
test( 'removes the option when it is clicked again', async () => {
const ratingParam = 'instock,outofstock';
const {
getInStockCheckbox,
getOutOfStockCheckbox,
getOnBackorderCheckbox,
} = setupMultipleChoiceList( ratingParam );
expect( getInStockCheckbox()?.checked ).toBeTruthy();
expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
const inStockCheckbox = getInStockCheckbox();
if ( inStockCheckbox ) {
userEvent.click( inStockCheckbox );
}
await waitFor( () => {
expect( getInStockCheckbox()?.checked ).toBeFalsy();
expect( getOutOfStockCheckbox()?.checked ).toBeTruthy();
expect( getOnBackorderCheckbox()?.checked ).toBeFalsy();
} );
} );
} );
} );

View File

@ -5,10 +5,18 @@ export interface Attributes {
showCounts: boolean;
showFilterButton: boolean;
isPreview?: boolean;
displayStyle: string;
selectType: string;
}
export interface DisplayOption {
value: string;
name: string;
label: JSX.Element;
textLabel: string;
}
export type Current = {
slug: string;
name: string;
};

View File

@ -28,6 +28,18 @@ export const getActiveFilters = (
);
};
export function generateUniqueId() {
return Math.floor( Math.random() * Date.now() );
}
export const formatSlug = ( slug: string ) =>
slug
.trim()
.replace( /\s/g, '' )
.replace( /_/g, '-' )
.replace( /-+/g, '-' )
.replace( /[^a-zA-Z0-9-]/g, '' );
export const parseAttributes = ( data: Record< string, unknown > ) => {
return {
heading: isString( data?.heading ) ? data.heading : '',
@ -38,5 +50,11 @@ export const parseAttributes = ( data: Record< string, unknown > ) => {
showFilterButton: data?.showFilterButton === 'true',
showCounts: data?.showCounts !== 'false',
isPreview: false,
displayStyle:
( isString( data?.displayStyle ) && data.displayStyle ) ||
metadata.attributes.displayStyle.default,
selectType:
( isString( data?.selectType ) && data.selectType ) ||
metadata.attributes.selectType.default,
};
};

View File

@ -49,5 +49,22 @@ describe( `${ block.name } Block`, () => {
`${ block.class } .wc-block-filter-submit-button`
);
} );
it( 'allows changing the Display Style', async () => {
// Turn the display style to Dropdown
await expect( page ).toClick( 'button', { text: 'Dropdown' } );
await expect( page ).toMatchElement(
'.wc-block-stock-filter.style-dropdown'
);
// Turn the display style to List
await expect( page ).toClick( 'button', {
text: 'List',
} );
await expect( page ).toMatchElement(
'.wc-block-stock-filter.style-list'
);
} );
} );
} );