UNPKG

@momentum-ui/react

Version:

Cisco Momentum UI framework for ReactJs applications

245 lines (210 loc) 6.98 kB
/** @component slider */ import React from 'react'; import PropTypes from 'prop-types'; import SliderPointer from './SliderPointer'; class Slider extends React.Component { constructor(props) { super(props); this.state = { sliderLow: props.value.low || props.min, sliderHigh: props.value.high || props.value, scale: this.getScale(), selectionWidth: null, }; } componentDidMount() { const { min, max } = this.props; const { scale } = this.state; this.getSelectionWidth(); this.setScalePos(scale, min, max); } setScalePos = (scale, min, max) => { this.ticksContainer && this.ticksContainer.childNodes.forEach((child, idx) => { const leftValue = `${(scale[idx] - min) / max * 100}%`; child.style.left = `calc(${leftValue} - ${(child.offsetWidth/2)}px)`; }); }; getScale = () => { const { min, max, tick } = this.props; if (tick) { let value = max; let ticksArray = [max]; while (value > 0 && (value - tick) >= min) { value -= tick; ticksArray.unshift(Math.abs(Math.round(value / tick) * tick)); } return ticksArray; } else { return [min, max]; } }; getSliderWidth = () => { return this.sliderBar.getBoundingClientRect().width; }; getStepWidth = () => { const maxValue = this.props.max - this.props.min; const maxWidth = this.getSliderWidth(); return (this.props.step / maxValue) * maxWidth; }; getSteps = (position) => { if (position.isKeyBoard) return 1; const diff = position.direction === 1 ? position.to - position.from : position.from - position.to; if (diff < 0) return 0; const steps = diff / this.getStepWidth(); return steps - Math.floor(steps) >= 0.5 ? Math.ceil(steps) : Math.floor(steps); }; getLimit = (pointerKey, direction) => { if (pointerKey === 'sliderLow') { return this.props.canCross ? direction === 1 ? this.props.max : this.props.min : direction === 1 ? this.state.sliderHigh : this.props.min; } else if (pointerKey === 'sliderHigh') { return this.props.canCross ? direction === 1 ? this.props.max : this.props.min : direction === 1 ? this.props.max : this.state.sliderLow; } }; returnCurrentValues = () => { this.getSelectionWidth(); if (this.props.onChange) { return this.props.onChange( typeof this.props.value === 'object' ? { low: Math.min(this.state.sliderHigh, this.state.sliderLow), high: Math.max(this.state.sliderHigh, this.state.sliderLow) } : this.state.sliderHigh ); } }; moveForward = (key, pixelMove, limit) => { const newPosition = this.state[key] + pixelMove <= limit ? this.state[key] + pixelMove : limit; this.setState({ [key]: newPosition }, () => this.returnCurrentValues()); }; moveBack = (key, pixelMove, limit) => { const newPosition = this.state[key] - pixelMove >= limit ? this.state[key] - pixelMove : limit; this.setState({ [key]: newPosition }, () => this.returnCurrentValues()); }; onSliderMove = (key, position) => { if (this.props.disabled) return; const limit = this.getLimit(key, position.direction); const pixelMove = this.getSteps(position) * this.props.step; position.direction === 1 ? this.moveForward(key, pixelMove, limit) : this.moveBack(key, pixelMove, limit); }; getSelectionWidth = () => { const baseValue = Number.isInteger(this.state.sliderLow) ? this.state.sliderLow : this.props.min; this.setState({ selectionWidth: { width: `${(Math.abs(this.state.sliderHigh - baseValue) / this.props.max) * 100}%`, left: `${((Math.min(baseValue, this.state.sliderHigh) - this.props.min) / this.props.max) * 100}%` } }); }; render() { const { value, disabled, className, max, min, translateFn } = this.props; const { sliderHigh, sliderLow, scale, selectionWidth } = this.state; const renderTicks = () => { const ticks = scale.map((tickValue, idx) => { const tickLabel = translateFn ? translateFn(tickValue) : tickValue; return ( <span key={`tick-${idx}`} className='md-slider__hashlabel'> {tickLabel} </span> ); }); return ( <div ref={ref => this.ticksContainer = ref}> {ticks} </div> ); }; return ( <div className={ `md-slider` + `${(disabled && ` md-slider--disabled`) || ''}` + `${(className && ` ${className}`) || ''}` } role='slider' aria-valuemin={min} aria-valuemax={max} aria-valuenow={ typeof this.props.value !== 'object' ? sliderHigh : undefined } aria-valuetext={ typeof this.props.value === 'object' ? `Low is ${Math.min(sliderLow, sliderHigh)}, high is ${Math.max(sliderLow, sliderHigh)}` : undefined } > <span className='md-slider__bar' ref={ref => this.sliderBar = ref} /> <span className='md-slider__selection' style={selectionWidth} /> { Number.isInteger(value.low) && <SliderPointer position={((sliderLow - min) / (max - min)) * 100} onMove={(b) => this.onSliderMove('sliderLow', b)} /> } <SliderPointer position={((sliderHigh - min) / (max - min)) * 100} onMove={(b) => this.onSliderMove('sliderHigh', b)} /> {renderTicks()} </div> ); } } Slider.propTypes = { /** @prop Determines if minimum pointer can cross over maximum pointer | false */ canCross: PropTypes.bool, /** @prop Optional CSS class string | '' */ className: PropTypes.string, /** @prop Set the attribute disabled to Slider | false */ disabled: PropTypes.bool, /** @prop Set the initial maximum value */ max: PropTypes.number.isRequired, /** @prop Set the initial minimum value | 0 */ min: PropTypes.number, /** @prop Callback function invoked by user when change a pointer position | null */ onChange: PropTypes.func, /** @prop Set visual step measurement | 1 */ step: PropTypes.number, /** @prop Set increment of x-axis labels | 0 */ tick: PropTypes.number, /** @prop Function to compute layout of Slider | null */ translateFn: PropTypes.func, /** @prop Set either maximum pointer value or a combination of high and low pointer values | 0 */ value: PropTypes.oneOfType([ PropTypes.shape({ high: PropTypes.number.isRequired, low: PropTypes.number.isRequired, }), PropTypes.number ]) }; Slider.defaultProps = { canCross: false, className: '', disabled: false, min: 0, onChange: null, step: 1, tick: 0, translateFn: null, value: 0 }; Slider.displayName = 'Slider'; export default Slider;