[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:
parent
4e76cb11be
commit
ce98e51430
|
@ -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 }
|
||||
/>
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -32,6 +32,7 @@ export type PriceFilterStore = {
|
|||
state: PriceFilterState;
|
||||
actions: {
|
||||
updateProducts: ( event: HTMLElementEvent< HTMLInputElement > ) => void;
|
||||
selectInputContent: () => void;
|
||||
reset: () => void;
|
||||
};
|
||||
};
|
||||
|
|
|
@ -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' } );
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
Significance: minor
|
||||
Type: dev
|
||||
|
||||
[Filter Products by Price]: Update view when changing the min/max value #50651
|
|
@ -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(
|
||||
|
|
Loading…
Reference in New Issue