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

Commit 9bec813

Browse files
authored
Active Filters Loading Placeholders (#7083)
* Active Filters Loading Placeholders * Use flexbox for active filters loading placeholders * Clear all placeholder loading styles * Ensure active filters which arent attribute filters render null when in a loading state * Refactor loading placeholders and state setting * Add useIsMounted to shared hooks and check productAttributes only when mounted * Add componentHasMounted to useMemo deps * Check URL for attribute filter hint * Check URL for attribute filter hint * Remove border-radius from placeholder for clear all button * Fix filter loading when no filters are active on shop page
1 parent 63a21c8 commit 9bec813

File tree

7 files changed

+329
-46
lines changed

7 files changed

+329
-46
lines changed

assets/js/base/hooks/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,4 @@ export * from './use-spacing-props';
88
export * from './use-typography-props';
99
export * from './use-color-props';
1010
export * from './use-border-props';
11+
export * from './use-is-mounted';
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { useCallback, useEffect, useRef } from '@wordpress/element';
5+
6+
/**
7+
* Returns a boolean value based on whether the current component has been mounted.
8+
*
9+
* @return {boolean} If the component has been mounted.
10+
*
11+
* @example
12+
*
13+
* ```js
14+
* const App = () => {
15+
* const isMounted = useIsMounted();
16+
*
17+
* if ( ! isMounted() ) {
18+
* return null;
19+
* }
20+
*
21+
* return </div>;
22+
* };
23+
* ```
24+
*/
25+
26+
export function useIsMounted() {
27+
const isMounted = useRef( false );
28+
29+
useEffect( () => {
30+
isMounted.current = true;
31+
32+
return () => {
33+
isMounted.current = false;
34+
};
35+
}, [] );
36+
37+
return useCallback( () => isMounted.current, [] );
38+
}

assets/js/blocks/active-filters/active-attribute-filters.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/**
22
* External dependencies
33
*/
4+
import { useEffect } from '@wordpress/element';
45
import {
56
useCollection,
67
useQueryStateByKey,
@@ -26,22 +27,25 @@ interface ActiveAttributeFiltersProps {
2627
operator: 'and' | 'in';
2728
slugs: string[];
2829
attributeObject: AttributeObject;
30+
isLoadingCallback: ( val: boolean ) => void;
2931
}
3032

3133
/**
3234
* Component that renders active attribute (terms) filters.
3335
*
34-
* @param {Object} props Incoming props for the component.
35-
* @param {Object} props.attributeObject The attribute object.
36-
* @param {Array} props.slugs The slugs for attributes.
37-
* @param {string} props.operator The operator for the filter.
38-
* @param {string} props.displayStyle The style used for displaying the filters.
36+
* @param {Object} props Incoming props for the component.
37+
* @param {Object} props.attributeObject The attribute object.
38+
* @param {Array} props.slugs The slugs for attributes.
39+
* @param {string} props.operator The operator for the filter.
40+
* @param {string} props.displayStyle The style used for displaying the filters.
41+
* @param {string} props.isLoadingCallback The callback to trigger the loading complete state.
3942
*/
4043
const ActiveAttributeFilters = ( {
4144
attributeObject,
4245
slugs = [],
4346
operator = 'in',
4447
displayStyle,
48+
isLoadingCallback,
4549
}: ActiveAttributeFiltersProps ) => {
4650
const { results, isLoading } = useCollection( {
4751
namespace: '/wc/store/v1',
@@ -54,8 +58,11 @@ const ActiveAttributeFilters = ( {
5458
[]
5559
);
5660

61+
useEffect( () => {
62+
isLoadingCallback( isLoading );
63+
}, [ isLoading, isLoadingCallback ] );
64+
5765
if (
58-
isLoading ||
5966
! Array.isArray( results ) ||
6067
! isAttributeTermCollection( results ) ||
6168
! isAttributeQueryCollection( productAttributes )
@@ -100,6 +107,7 @@ const ActiveAttributeFilters = ( {
100107
type: attributeLabel,
101108
name: decodeEntities( termObject.name || slug ),
102109
prefix,
110+
isLoading,
103111
removeCallback: () => {
104112
const currentAttribute = productAttributes.find(
105113
( { attribute } ) =>

assets/js/blocks/active-filters/block.tsx

Lines changed: 87 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import { __, sprintf } from '@wordpress/i18n';
55
import { useQueryStateByKey } from '@woocommerce/base-context/hooks';
66
import { getSetting, getSettingWithCoercion } from '@woocommerce/settings';
7-
import { useMemo, useEffect } from '@wordpress/element';
7+
import { useMemo, useEffect, useState } from '@wordpress/element';
88
import classnames from 'classnames';
99
import PropTypes from 'prop-types';
1010
import Label from '@woocommerce/base-components/label';
@@ -16,6 +16,8 @@ import {
1616
isStockStatusOptions,
1717
} from '@woocommerce/types';
1818
import { getUrlParameter } from '@woocommerce/utils';
19+
import FilterTitlePlaceholder from '@woocommerce/base-components/filter-placeholder';
20+
import { useIsMounted } from '@woocommerce/base-hooks';
1921

2022
/**
2123
* Internal dependencies
@@ -27,8 +29,11 @@ import {
2729
renderRemovableListItem,
2830
removeArgsFromFilterUrl,
2931
cleanFilterUrl,
32+
maybeUrlContainsFilters,
33+
urlContainsAttributeFilter,
3034
} from './utils';
3135
import ActiveAttributeFilters from './active-attribute-filters';
36+
import FilterPlaceholders from './filter-placeholders';
3237
import { Attributes } from './types';
3338

3439
/**
@@ -45,11 +50,20 @@ const ActiveFiltersBlock = ( {
4550
attributes: Attributes;
4651
isEditor?: boolean;
4752
} ) => {
53+
const isMounted = useIsMounted();
54+
const componentHasMounted = isMounted();
4855
const filteringForPhpTemplate = getSettingWithCoercion(
4956
'is_rendering_php_template',
5057
false,
5158
isBoolean
5259
);
60+
const [ isLoading, setIsLoading ] = useState( true );
61+
/*
62+
activeAttributeFilters is the only async query in this block. Because of this the rest of the filters will render null
63+
when in a loading state and activeAttributeFilters renders the placeholders.
64+
*/
65+
const shouldShowLoadingPlaceholders =
66+
maybeUrlContainsFilters() && ! isEditor && isLoading;
5367
const [ productAttributes, setProductAttributes ] = useQueryStateByKey(
5468
'attributes',
5569
[]
@@ -62,8 +76,10 @@ const ActiveFiltersBlock = ( {
6276
const [ maxPrice, setMaxPrice ] = useQueryStateByKey( 'max_price' );
6377

6478
const STOCK_STATUS_OPTIONS = getSetting( 'stockStatusOptions', [] );
79+
const STORE_ATTRIBUTES = getSetting( 'attributes', [] );
6580
const activeStockStatusFilters = useMemo( () => {
6681
if (
82+
shouldShowLoadingPlaceholders ||
6783
productStockStatus.length === 0 ||
6884
! isStockStatusQueryCollection( productStockStatus ) ||
6985
! isStockStatusOptions( STOCK_STATUS_OPTIONS )
@@ -92,6 +108,7 @@ const ActiveFiltersBlock = ( {
92108
} );
93109
} );
94110
}, [
111+
shouldShowLoadingPlaceholders,
95112
STOCK_STATUS_OPTIONS,
96113
productStockStatus,
97114
setProductStockStatus,
@@ -100,7 +117,10 @@ const ActiveFiltersBlock = ( {
100117
] );
101118

102119
const activePriceFilters = useMemo( () => {
103-
if ( ! Number.isFinite( minPrice ) && ! Number.isFinite( maxPrice ) ) {
120+
if (
121+
shouldShowLoadingPlaceholders ||
122+
( ! Number.isFinite( minPrice ) && ! Number.isFinite( maxPrice ) )
123+
) {
104124
return null;
105125
}
106126
return renderRemovableListItem( {
@@ -116,6 +136,7 @@ const ActiveFiltersBlock = ( {
116136
displayStyle: blockAttributes.displayStyle,
117137
} );
118138
}, [
139+
shouldShowLoadingPlaceholders,
119140
minPrice,
120141
maxPrice,
121142
blockAttributes.displayStyle,
@@ -125,7 +146,13 @@ const ActiveFiltersBlock = ( {
125146
] );
126147

127148
const activeAttributeFilters = useMemo( () => {
128-
if ( ! isAttributeQueryCollection( productAttributes ) ) {
149+
if (
150+
( ! isAttributeQueryCollection( productAttributes ) &&
151+
componentHasMounted ) ||
152+
( ! productAttributes.length &&
153+
! urlContainsAttributeFilter( STORE_ATTRIBUTES ) )
154+
) {
155+
setIsLoading( false );
129156
return null;
130157
}
131158

@@ -135,6 +162,7 @@ const ActiveFiltersBlock = ( {
135162
);
136163

137164
if ( ! attributeObject ) {
165+
setIsLoading( false );
138166
return null;
139167
}
140168

@@ -145,10 +173,16 @@ const ActiveFiltersBlock = ( {
145173
slugs={ attribute.slug }
146174
key={ attribute.attribute }
147175
operator={ attribute.operator }
176+
isLoadingCallback={ setIsLoading }
148177
/>
149178
);
150179
} );
151-
}, [ productAttributes, blockAttributes.displayStyle ] );
180+
}, [
181+
componentHasMounted,
182+
setIsLoading,
183+
productAttributes,
184+
blockAttributes.displayStyle,
185+
] );
152186

153187
const [ productRatings, setProductRatings ] =
154188
useQueryStateByKey( 'ratings' );
@@ -177,6 +211,7 @@ const ActiveFiltersBlock = ( {
177211

178212
const activeRatingFilters = useMemo( () => {
179213
if (
214+
shouldShowLoadingPlaceholders ||
180215
productRatings.length === 0 ||
181216
! isRatingQueryCollection( productRatings )
182217
) {
@@ -206,6 +241,7 @@ const ActiveFiltersBlock = ( {
206241
} );
207242
} );
208243
}, [
244+
shouldShowLoadingPlaceholders,
209245
productRatings,
210246
setProductRatings,
211247
blockAttributes.displayStyle,
@@ -222,12 +258,25 @@ const ActiveFiltersBlock = ( {
222258
);
223259
};
224260

225-
if ( ! hasFilters() && ! isEditor ) {
261+
if ( ! shouldShowLoadingPlaceholders && ! hasFilters() && ! isEditor ) {
226262
return null;
227263
}
228264

229265
const TagName =
230266
`h${ blockAttributes.headingLevel }` as keyof JSX.IntrinsicElements;
267+
268+
const heading = (
269+
<TagName className="wc-block-active-filters__title">
270+
{ blockAttributes.heading }
271+
</TagName>
272+
);
273+
274+
const filterHeading = shouldShowLoadingPlaceholders ? (
275+
<FilterTitlePlaceholder>{ heading }</FilterTitlePlaceholder>
276+
) : (
277+
heading
278+
);
279+
231280
const hasFilterableProducts = getSettingWithCoercion(
232281
'has_filterable_products',
233282
false,
@@ -241,15 +290,12 @@ const ActiveFiltersBlock = ( {
241290
const listClasses = classnames( 'wc-block-active-filters__list', {
242291
'wc-block-active-filters__list--chips':
243292
blockAttributes.displayStyle === 'chips',
293+
'wc-block-active-filters--loading': shouldShowLoadingPlaceholders,
244294
} );
245295

246296
return (
247297
<>
248-
{ ! isEditor && blockAttributes.heading && (
249-
<TagName className="wc-block-active-filters__title">
250-
{ blockAttributes.heading }
251-
</TagName>
252-
) }
298+
{ ! isEditor && blockAttributes.heading && filterHeading }
253299
<div className="wc-block-active-filters">
254300
<ul className={ listClasses }>
255301
{ isEditor ? (
@@ -279,36 +325,44 @@ const ActiveFiltersBlock = ( {
279325
</>
280326
) : (
281327
<>
328+
<FilterPlaceholders
329+
isLoading={ shouldShowLoadingPlaceholders }
330+
displayStyle={ blockAttributes.displayStyle }
331+
/>
282332
{ activePriceFilters }
283333
{ activeStockStatusFilters }
284334
{ activeAttributeFilters }
285335
{ activeRatingFilters }
286336
</>
287337
) }
288338
</ul>
289-
<button
290-
className="wc-block-active-filters__clear-all"
291-
onClick={ () => {
292-
cleanFilterUrl();
293-
if ( ! filteringForPhpTemplate ) {
294-
setMinPrice( undefined );
295-
setMaxPrice( undefined );
296-
setProductAttributes( [] );
297-
setProductStockStatus( [] );
298-
}
299-
} }
300-
>
301-
<Label
302-
label={ __(
303-
'Clear All',
304-
'woo-gutenberg-products-block'
305-
) }
306-
screenReaderLabel={ __(
307-
'Clear All Filters',
308-
'woo-gutenberg-products-block'
309-
) }
310-
/>
311-
</button>
339+
{ shouldShowLoadingPlaceholders ? (
340+
<span className="wc-block-active-filters__clear-all-placeholder" />
341+
) : (
342+
<button
343+
className="wc-block-active-filters__clear-all"
344+
onClick={ () => {
345+
cleanFilterUrl();
346+
if ( ! filteringForPhpTemplate ) {
347+
setMinPrice( undefined );
348+
setMaxPrice( undefined );
349+
setProductAttributes( [] );
350+
setProductStockStatus( [] );
351+
}
352+
} }
353+
>
354+
<Label
355+
label={ __(
356+
'Clear All',
357+
'woo-gutenberg-products-block'
358+
) }
359+
screenReaderLabel={ __(
360+
'Clear All Filters',
361+
'woo-gutenberg-products-block'
362+
) }
363+
/>
364+
</button>
365+
) }
312366
</div>
313367
</>
314368
);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
const FilterPlaceholders = ( {
2+
displayStyle,
3+
isLoading,
4+
}: {
5+
isLoading: boolean;
6+
displayStyle: string;
7+
} ) => {
8+
if ( ! isLoading ) {
9+
return null;
10+
}
11+
12+
return (
13+
<>
14+
{ [ ...Array( displayStyle === 'list' ? 2 : 3 ) ].map( ( x, i ) => (
15+
<li
16+
className={
17+
displayStyle === 'list'
18+
? 'show-loading-state-list'
19+
: 'show-loading-state-chips'
20+
}
21+
key={ i }
22+
>
23+
<span className="show-loading-state__inner" />
24+
</li>
25+
) ) }
26+
</>
27+
);
28+
};
29+
30+
export default FilterPlaceholders;

0 commit comments

Comments
 (0)