Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Range slider component #793

Closed
wants to merge 25 commits into from
Closed
Changes from 2 commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -8,6 +8,7 @@ import classnames from 'classnames';
* Internal dependencies
*/
import PriceSlider from '../../frontend-components/price-slider';
import { currency } from '../../data';

/**
* Component displaying a price filter.
@@ -40,7 +41,14 @@ class PriceFilterBlock extends Component {
<div className={ classes }>
<p>Current Min: { min }</p>
<p>Current Max: { max }</p>
<PriceSlider min={ 0 } max={ 200 } step={ 10 } onChange={ this.onChange } />
<PriceSlider
min={ 0 }
max={ 200 }
step={ 10 }
onChange={ this.onChange }
currencySymbol={ currency.symbol }
priceFormat={ currency.price_format }
/>
</div>
);
}
@@ -0,0 +1,14 @@
/**
* Wrapper for the wcSettings global, which sets defaults if data is missing.
*
* Only settings used by blocks are defined here. Component settings are left out.
*/
export const currency = wcSettings.currency || {
code: 'USD',
precision: 2,
symbol: '$',
position: 'left',
decimal_separator: '.',
thousand_separator: ',',
price_format: '%1$s%2$s',
};
This conversation was marked as resolved by mikejolley

This comment has been minimized.

Copy link
@nerrad

nerrad Aug 12, 2019

Contributor

Yup, me likey 👍

So one thing we may also want to consider is add an alias to the webpack config so we can avoid doing relative imports everytime we need to use data. We could alias wcdata for instance to point to this file and then only have to do import { currency } from 'wcdata' in our modules.

@@ -9,21 +9,15 @@ import PropTypes from 'prop-types';
* Internal dependencies
*/
import './style.scss';

const get = ( obj, path, defaultValue ) => {
const result = String.prototype.split.call( path, /[,[\].]+?/ )
.filter( Boolean )
.reduce( ( res, key ) => ( res !== null && res !== undefined ) ? res[ key ] : res, obj );
return ( result === undefined || result === obj ) ? defaultValue : result;
};
import { constrainRangeSliderValues } from './utils';

class PriceSlider extends Component {
constructor( props ) {
const { min, max } = props;
super( ...arguments );
this.state = {
currentMin: min,
currentMax: max,
currentMin: parseInt( min, 10 ),
currentMax: parseInt( max, 10 ),
inputMin: this.formatCurrencyForInput( min ),
inputMax: this.formatCurrencyForInput( max ),
};
@@ -35,7 +29,6 @@ class PriceSlider extends Component {
this.onInputChange = this.onInputChange.bind( this );
this.onInputBlur = this.onInputBlur.bind( this );
this.findClosestRange = this.findClosestRange.bind( this );
this.validateValues = this.validateValues.bind( this );
this.getProgressStyle = this.getProgressStyle.bind( this );
this.formatCurrencyForInput = this.formatCurrencyForInput.bind( this );
}
@@ -57,50 +50,17 @@ class PriceSlider extends Component {
if ( '' === value ) {
return '';
}
const { currencySymbol, priceFormat } = this.props;

const formattedNumber = parseInt( value, 10 );
const currencySymbol = get( wcSettings, [ 'currency', 'symbol' ], '$' );
const priceFormat = get( wcSettings, [ 'currency', 'price_format' ], '%1$s%2$s' );
const formattedValue = sprintf( priceFormat, currencySymbol, formattedNumber );

This comment has been minimized.

Copy link
@nerrad

nerrad Aug 12, 2019

Contributor

What about currencies that have the symbol after the number and not before? With that in mind, I wonder if we should make a currency value object that we can pass around (and more easily type check for) as a prop? I built a currency vo for Event Espresso - we could do something similar?

This comment has been minimized.

Copy link
@mikejolley

mikejolley Aug 12, 2019

Author Member

Price format handles this :)

This comment has been minimized.

Copy link
@nerrad

nerrad Aug 12, 2019

Contributor

ahh, meaning it could be %2$s%1$s?

This comment has been minimized.

Copy link
@mikejolley

mikejolley Aug 13, 2019

Author Member

Yup thats it. It comes from the PHP side which already has this system in place.


// This uses a textarea to magically decode HTML currency symbols.
const formattedValue = sprintf( priceFormat, currencySymbol, formattedNumber );
const txt = document.createElement( 'textarea' );
txt.innerHTML = formattedValue;
return txt.value;
This conversation was marked as resolved by mikejolley

This comment has been minimized.

Copy link
@nerrad

nerrad Aug 7, 2019

Contributor

I'm not quite following why you're creating an element here, setting it's innerHTML and then just returning the value for that input? Why not just return formattedValue?

This comment has been minimized.

Copy link
@mikejolley

mikejolley Aug 7, 2019

Author Member

If I recall, it handles decoding HTML entities (currency symbols)

}

validateValues( values, isMin ) {
const { min, max, step } = this.props;

let minValue = parseInt( values[ 0 ], 10 ) || min;
let maxValue = parseInt( values[ 1 ], 10 ) || step;

if ( min > minValue ) {
minValue = min;
}

if ( max <= minValue ) {
minValue = max - step;
}

if ( min >= maxValue ) {
maxValue = min + step;
}

if ( max < maxValue ) {
maxValue = max;
}

if ( ! isMin && minValue >= maxValue ) {
minValue = maxValue - step;
}

if ( isMin && maxValue <= minValue ) {
maxValue = minValue + step;
}

return [ minValue, maxValue ];
}

onInputChange( event ) {
const newValue = event.target.value.replace( /[^0-9.-]+/g, '' );
const isMin = event.target.classList.contains( 'wc-block-price-filter__amount--min' );
@@ -112,36 +72,44 @@ class PriceSlider extends Component {
}

onInputBlur( event ) {
const { min, max, step } = this.props;
const isMin = event.target.classList.contains( 'wc-block-price-filter__amount--min' );
const values = this.validateValues(
const values = constrainRangeSliderValues(
[
this.minInput.current.value.replace( /[^0-9.-]+/g, '' ),
this.maxInput.current.value.replace( /[^0-9.-]+/g, '' ),
],
min,
max,
step,
isMin
);

this.setState( {
currentMin: values[ 0 ],
currentMax: values[ 1 ],
currentMin: parseInt( values[ 0 ], 10 ),
currentMax: parseInt( values[ 1 ], 10 ),
inputMin: this.formatCurrencyForInput( values[ 0 ] ),
inputMax: this.formatCurrencyForInput( values[ 1 ] ),
} );
}

onDrag( event ) {
const { min, max, step } = this.props;
const isMin = event.target.classList.contains( 'wc-block-price-filter__range-input--min' );
const values = this.validateValues(
const values = constrainRangeSliderValues(
[
this.minRange.current.value,
this.maxRange.current.value,
],
min,
max,
step,
isMin
);

this.setState( {
currentMin: values[ 0 ],
currentMax: values[ 1 ],
currentMin: parseInt( values[ 0 ], 10 ),
currentMax: parseInt( values[ 1 ], 10 ),
inputMin: this.formatCurrencyForInput( values[ 0 ] ),
inputMax: this.formatCurrencyForInput( values[ 1 ] ),
} );
@@ -270,11 +238,21 @@ PriceSlider.propTypes = {
* Step for slider inputs.
*/
step: PropTypes.number,
/**
* Currency symbol to use when formatting prices for display.
*/
currencySymbol: PropTypes.string,
/**
* Price format to use when formatting prices for display.
*/
priceFormat: PropTypes.string,
};

PriceSlider.defaultProps = {
min: 0,
step: 1,
currencySymbol: '$',
priceFormat: '%1$s%2$s',
};

export default PriceSlider;
@@ -0,0 +1,40 @@
/**
* Validate a min and max value for a range slider againt defined constraints (min, max, step).
*
* @param {array} values Array containing min and max values.
* @param {int} min Min allowed value for the sliders.
* @param {int} max Max allowed value for the sliders.
* @param {step} step Step value for the sliders.
* @param {boolean} isMin Whether we're currently interacting with the min range slider or not, so we update the correct values.
* @returns {array} Validated and updated min/max values that fit within the range slider constraints.
*/
export const constrainRangeSliderValues = ( values, min, max, step, isMin ) => {
let minValue = parseInt( values[ 0 ], 10 ) || min;
let maxValue = parseInt( values[ 1 ], 10 ) || step; // Max should be one step above min if invalid or 0.

if ( min > minValue ) {
minValue = min;
}

if ( max <= minValue ) {
minValue = max - step;
}

if ( min >= maxValue ) {
maxValue = min + step;
}

if ( max < maxValue ) {
maxValue = max;
}

if ( ! isMin && minValue >= maxValue ) {
minValue = maxValue - step;
}

if ( isMin && maxValue <= minValue ) {
maxValue = minValue + step;
}

return [ minValue, maxValue ];
};
@@ -64,40 +64,36 @@ public static function add_theme_body_class( $classes = array() ) {
}
/**
* These are used by @woocommerce/components & the block library to set up defaults
* These are used by @woocommerce/components and the block library to set up defaults
* based on user-controlled settings from WordPress. Only use this in wp-admin.
*/
public static function print_script_wc_settings() {
global $wp_locale;
$code = get_woocommerce_currency();
// NOTE: wcSettings is not used directly, it's only for @woocommerce/components
//
// Settings and variables can be passed here for access in the app.
// Will need `wcAdminAssetUrl` if the ImageAsset component is used.
// Will need `dataEndpoints.countries` if Search component is used with 'country' type.
// Will need `orderStatuses` if the OrderStatus component is used.
// Deliberately excluding: `embedBreadcrumbs`, `trackingEnabled`.
$settings = array(
'adminUrl' => admin_url(),
'wcAssetUrl' => plugins_url( 'assets/', WC_PLUGIN_FILE ),
'siteLocale' => esc_attr( get_bloginfo( 'language' ) ),
'currency' => array(
'code' => $code,
'precision' => wc_get_price_decimals(),
'symbol' => get_woocommerce_currency_symbol( $code ),
'position' => get_option( 'woocommerce_currency_pos' ),
),
'stockStatuses' => wc_get_product_stock_status_options(),
'siteTitle' => get_bloginfo( 'name' ),
'dataEndpoints' => array(),
'l10n' => array(
'userLocale' => get_user_locale(),
'weekdaysShort' => array_values( $wp_locale->weekday_abbrev ),
),
$code = get_woocommerce_currency();
$settings = apply_filters(
'woocommerce_components_settings',
array(
'adminUrl' => admin_url(),
'wcAssetUrl' => plugins_url( 'assets/', WC_PLUGIN_FILE ),
'siteLocale' => esc_attr( get_bloginfo( 'language' ) ),
'currency' => array(
'code' => $code,
'precision' => wc_get_price_decimals(),
'symbol' => html_entity_decode( get_woocommerce_currency_symbol( $code ) ),
'position' => get_option( 'woocommerce_currency_pos' ),
'decimal_separator' => wc_get_price_decimal_separator(),
'thousand_separator' => wc_get_price_thousand_separator(),
'price_format' => html_entity_decode( get_woocommerce_price_format() ),
),
'stockStatuses' => wc_get_product_stock_status_options(),
'siteTitle' => get_bloginfo( 'name' ),
'dataEndpoints' => array(),
'l10n' => array(
'userLocale' => get_user_locale(),
'weekdaysShort' => array_values( $wp_locale->weekday_abbrev ),
),
)
);
// NOTE: wcSettings is not used directly, it's only for @woocommerce/components.
$settings = apply_filters( 'woocommerce_components_settings', $settings );
?>
<script type="text/javascript">
var wcSettings = wcSettings || JSON.parse( decodeURIComponent( '<?php echo rawurlencode( wp_json_encode( $settings ) ); ?>' ) );
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.