[Filter Products by Price]: Update view when changing the min/max value (#50651)

* Use handlePriceChange method

* Fix E2E test

* Add changelog

* Remove onBlur

* Add select when clicked

* Fix comment

* Add functionality to experimental block

* Fix E2E test
This commit is contained in:
Fernando Marichal 2024-08-20 10:20:09 -03:00 committed by GitHub
parent 4e76cb11be
commit ce98e51430
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 109 additions and 75 deletions

View File

@ -169,6 +169,22 @@ const PriceSlider = ( {
hasValidConstraints,
] );
/**
* Selects the price field when clicked.
*
* @param {Object} event event data.
*/
const handleSelectOnClick = (
event:
| React.FocusEvent< HTMLInputElement >
| React.MouseEvent< HTMLInputElement >
) => {
const target = event.currentTarget;
if ( target ) {
target.select();
}
};
/**
* Works around an IE issue where only one range selector is visible by changing the display order
* based on the mouse position.
@ -251,30 +267,19 @@ const PriceSlider = ( {
);
/**
* Called when a price input loses focus - commit changes to slider.
* Handles price change logic and calls onChange.
*/
const priceInputOnBlur = useCallback(
( event: React.FocusEvent< HTMLInputElement > ) => {
// Only refresh when finished editing the min and max fields.
if (
event.relatedTarget &&
( event.relatedTarget as Element ).classList &&
( event.relatedTarget as Element ).classList.contains(
'wc-block-price-filter__amount'
)
) {
return;
}
const isMin = event.target.classList.contains(
'wc-block-price-filter__amount--min'
);
const handlePriceChange = useDebouncedCallback(
(
minInsertedPrice: number,
maxInsertedPrice: number,
isMin: boolean
) => {
// When the user inserts in the max price input a value less or equal than the current minimum price,
// we set to 0 the minimum price.
if ( minPriceInput >= maxPriceInput ) {
if ( minInsertedPrice >= maxInsertedPrice ) {
const values = constrainRangeSliderValues(
[ 0, maxPriceInput ],
[ 0, maxInsertedPrice ],
null,
null,
stepValue,
@ -287,7 +292,7 @@ const PriceSlider = ( {
}
const values = constrainRangeSliderValues(
[ minPriceInput, maxPriceInput ],
[ minInsertedPrice, maxInsertedPrice ],
null,
null,
stepValue,
@ -295,7 +300,7 @@ const PriceSlider = ( {
);
onChange( values );
},
[ onChange, stepValue, minPriceInput, maxPriceInput ]
1000
);
const debouncedUpdateQuery = useDebouncedCallback( onSubmit, 600 );
@ -406,7 +411,7 @@ const PriceSlider = ( {
displayType: 'input',
allowNegative: false,
disabled: isLoading || ! hasValidConstraints,
onBlur: priceInputOnBlur,
onClick: handleSelectOnClick,
};
return (
@ -432,6 +437,11 @@ const PriceSlider = ( {
return;
}
setMinPriceInput( value );
handlePriceChange(
value,
maxPriceInput as number,
true
);
} }
value={ minPriceInput }
/>
@ -456,6 +466,11 @@ const PriceSlider = ( {
return;
}
setMaxPriceInput( value );
handlePriceChange(
minPriceInput as number,
value,
false
);
} }
value={ maxPriceInput }
/>

View File

@ -4,6 +4,7 @@
import { store, getContext, getElement } from '@woocommerce/interactivity';
import { formatPrice, getCurrency } from '@woocommerce/price-format';
import { HTMLElementEvent } from '@woocommerce/types';
import { debounce } from '@woocommerce/base-utils';
/**
* Internal dependencies
@ -35,6 +36,46 @@ const getUrl = ( context: PriceFilterContext ) => {
return url.href;
};
const debounceUpdate = debounce( ( context, element, event ) => {
const { decimalSeparator } = getCurrency();
const { minRange, minPrice, maxPrice, maxRange } = context;
const type = event.target.name;
const value = parseInt(
event.target.value
.replace( new RegExp( `[^0-9\\${ decimalSeparator }]+`, 'g' ), '' )
.replace( new RegExp( `\\${ decimalSeparator }`, 'g' ), '.' ),
10
);
const currentMinPrice =
type === 'min'
? Math.min( Number.isNaN( value ) ? minRange : value, maxPrice )
: minPrice;
const currentMaxPrice =
type === 'max'
? Math.max( Number.isNaN( value ) ? maxRange : value, minPrice )
: maxPrice;
if ( type === 'min' ) {
element.ref.value = currentMinPrice;
} else if ( type === 'max' ) {
element.ref.value = currentMaxPrice;
}
context.minPrice = currentMinPrice;
context.maxPrice = currentMaxPrice;
navigate(
getUrl( {
minRange,
maxRange,
minPrice: currentMinPrice,
maxPrice: currentMaxPrice,
} )
);
}, 1000 );
store< PriceFilterStore >( 'woocommerce/product-filter-price', {
state: {
rangeStyle: () => {
@ -61,60 +102,19 @@ store< PriceFilterStore >( 'woocommerce/product-filter-price', {
},
actions: {
updateProducts: ( event: HTMLElementEvent< HTMLInputElement > ) => {
const { decimalSeparator } = getCurrency();
const context = getContext< PriceFilterContext >();
const { minRange, minPrice, maxPrice, maxRange } = context;
const type = event.target.name;
const value = parseInt(
event.target.value
.replace(
new RegExp( `[^0-9\\${ decimalSeparator }]+`, 'g' ),
''
)
.replace(
new RegExp( `\\${ decimalSeparator }`, 'g' ),
'.'
),
10
);
const currentMinPrice =
type === 'min'
? Math.min(
Number.isNaN( value ) ? minRange : value,
maxPrice
)
: minPrice;
const currentMaxPrice =
type === 'max'
? Math.max(
Number.isNaN( value ) ? maxRange : value,
minPrice
)
: maxPrice;
// In some occasions the input element is updated with the incorrect value.
// By using the element that triggered the event, we can ensure the correct value is used for the input.
const element = getElement();
if ( type === 'min' ) {
element.ref.value = currentMinPrice;
debounceUpdate( context, element, event );
},
selectInputContent: () => {
const element = getElement();
if ( element && element.ref ) {
element.ref.select();
}
if ( type === 'max' ) {
element.ref.value = currentMaxPrice;
}
context.minPrice = currentMinPrice;
context.maxPrice = currentMaxPrice;
navigate(
getUrl( {
minRange,
maxRange,
minPrice: currentMinPrice,
maxPrice: currentMaxPrice,
} )
);
},
reset: () => {
navigate(

View File

@ -32,6 +32,7 @@ export type PriceFilterStore = {
state: PriceFilterState;
actions: {
updateProducts: ( event: HTMLElementEvent< HTMLInputElement > ) => void;
selectInputContent: () => void;
reset: () => void;
};
};

View File

@ -93,7 +93,10 @@ test.describe( 'Product Filter: Price Filter Block', () => {
);
const minPriceInput = leftInputContainer.locator( '.min' );
await minPriceInput.fill( String( defaultMinRange ) );
await minPriceInput.blur();
await page.waitForURL(
( url ) => ! url.href.includes( 'min_price' )
);
// Max price input field
const rightInputContainer = page.locator(
@ -101,7 +104,10 @@ test.describe( 'Product Filter: Price Filter Block', () => {
);
const maxPriceInput = rightInputContainer.locator( '.max' );
await maxPriceInput.fill( String( defaultMaxRange ) );
await maxPriceInput.blur();
await page.waitForURL(
( url ) => ! url.href.includes( 'max_price' )
);
const button = page.getByRole( 'button', { name: 'Clear' } );

View File

@ -361,6 +361,12 @@ test.describe( `${ blockData.name } Block - with Product Collection`, () => {
await maxPriceInput.dblclick();
await maxPriceInput.fill( '$5' );
const resetPriceFilterButton = page.getByRole( 'button', {
name: 'Reset price filter',
} );
await expect( resetPriceFilterButton ).toBeVisible();
await page
.getByRole( 'button', { name: 'Apply price filter' } )
.click();

View File

@ -0,0 +1,4 @@
Significance: minor
Type: dev
[Filter Products by Price]: Update view when changing the min/max value #50651

View File

@ -171,7 +171,8 @@ final class ProductFilterPrice extends AbstractBlock {
type="text"
value="%s"
data-wc-bind--value="state.formattedMinPrice"
data-wc-on--change="actions.updateProducts"
data-wc-on--input="actions.updateProducts"
data-wc-on--focus="actions.selectInputContent"
pattern=""
/>',
wp_strip_all_tags( $formatted_min_price )
@ -189,7 +190,8 @@ final class ProductFilterPrice extends AbstractBlock {
type="text"
value="%s"
data-wc-bind--value="state.formattedMaxPrice"
data-wc-on--change="actions.updateProducts"
data-wc-on--input="actions.updateProducts"
data-wc-on--focus="actions.selectInputContent"
/>',
wp_strip_all_tags( $formatted_max_price )
) : sprintf(