UNPKG

vcc-ui

Version:

VCC UI is a collection of React UI Components that can be used for developing front-end applications at Volvo Car Corporation.

395 lines (346 loc) 10.4 kB
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { withTheme } from 'react-fela'; import { Arrow } from '../arrow'; import { Block } from '../block'; import { Click } from '../click'; import { getThemeStyle } from '../../get-theme-style'; const KEY_LEFT = 37; const KEY_UP = 38; const KEY_RIGHT = 39; const KEY_DOWN = 40; const THUMB_WIDTH = 40; const THUMB_HEIGHT = 40; const thumbStyle = ({ left, isDragging, theme, disabled }) => ({ position: 'absolute', top: 0, left: left, width: THUMB_WIDTH, height: THUMB_HEIGHT, border: '1px solid ' + theme.tokens.inputBorder, background: theme.tokens.inputBackground, cursor: disabled ? 'not-allowed' : 'ew-resize', display: 'flex', alignItems: 'center', justifyContent: 'center', outline: 0, ':focus': { borderColor: theme.tokens.inputBorderFocus, }, extend: { condition: !isDragging, style: { transition: 'left 200ms ease-out', }, }, }); class SliderComponent extends Component { static propTypes = { initialValue: PropTypes.number.isRequired, minValue: PropTypes.number, maxValue: PropTypes.number, step: PropTypes.number, valueList: PropTypes.array, onMoveStart: PropTypes.func, onMoveEnd: PropTypes.func, onChange: PropTypes.func, theme: PropTypes.object, disabled: PropTypes.bool, }; state = { isDragging: false, lastStep: 0, currentLeft: 0, }; constructor(props) { super(props); this.trackRef = React.createRef(); this.thumbRef = React.createRef(); } componentDidMount() { const { initialValue } = this.props; this.setState({ currentStep: this.getStepForValue(initialValue), }); let resizeTimer; this.resizeEventHandler = () => { clearTimeout(resizeTimer); resizeTimer = setTimeout(() => { this.forceUpdate(); }, 1); }; window.addEventListener('resize', this.resizeEventHandler); } componentDidUpdate = prevProps => { const { initialValue } = this.props; const { initialValue: prevInitialValue } = prevProps; if (initialValue !== prevInitialValue) { this.setState(() => ({ currentStep: this.getStepForValue(initialValue), })); } }; componentWillUnmount() { window.removeEventListener('resize', this.resizeEventHandler); } handleChange = () => { const { onChange = () => {} } = this.props; const { lastStep, currentStep } = this.state; const currentValue = this.getCurrentValue(); if (lastStep !== currentStep && typeof onChange === 'function') { onChange(currentValue); } }; getElementProperty = (element, property) => { return element && element.getBoundingClientRect ? element.getBoundingClientRect()[property] : 0; }; getElementWidth = element => { return this.getElementProperty(element, 'width'); }; getElementLeft = element => { return this.getElementProperty(element, 'left'); }; getNumberOfSteps = () => { const { step, minValue, maxValue, valueList = [] } = this.props; if (valueList.length > 0) { return valueList.length; } else { return (maxValue - minValue) / step + 1; } }; getStepAtPercentagePosition = percentagePosition => { const steps = this.getNumberOfSteps(); return Math.round(percentagePosition * (steps - 1)); }; getStepForValue = value => { const { minValue, maxValue, valueList = [] } = this.props; const steps = this.getNumberOfSteps(); const thisValue = value - minValue; const range = maxValue - minValue; if (valueList.length > 0) { return valueList.indexOf(value); } else { return Math.round(((steps - 1) * thisValue) / range); } }; getValueAtStepPosition = step => { const { minValue, maxValue, valueList = [] } = this.props; const steps = this.getNumberOfSteps(); if (valueList.length > 0) { return valueList.find((value, index) => index === step); } else { return minValue + step * ((maxValue - minValue) / (steps - 1)); } }; getCurrentValue = () => { const { currentStep } = this.state; return this.getValueAtStepPosition(currentStep); }; updateCurrentStep = position => { const trackWidth = this.getElementWidth(this.trackRef.current); const trackLeft = this.getElementLeft(this.trackRef.current); const thumbWidth = THUMB_WIDTH; // we invert the leftOffset value on rtl layout const leftOffset = position - trackLeft - thumbWidth / 2; const newPositionLeft = this.props.theme.direction === 'ltr' ? leftOffset : trackWidth - leftOffset; const isMax = newPositionLeft >= trackWidth - thumbWidth; const isMin = newPositionLeft <= 0; const currentPositionLeft = isMax ? trackWidth - thumbWidth : isMin ? 0 : newPositionLeft; const currentPositionPercent = currentPositionLeft / (trackWidth - thumbWidth); if (typeof position === 'number') { this.setState({ currentStep: this.getStepAtPercentagePosition(currentPositionPercent), currentLeft: currentPositionLeft, }); } }; getLeftPositionFromCurrentStep = () => { if (!this.trackRef || !this.thumbRef) { return 0; } const { currentStep = 0, currentLeft = 0 } = this.state; const steps = this.getNumberOfSteps(); const trackWidth = this.getElementWidth(this.trackRef.current); const leftPos = (currentStep * (trackWidth - THUMB_WIDTH)) / (steps - 1); return currentLeft ? currentLeft : Math.round(leftPos); }; resetCurrentLeft() { this.setState({ currentLeft: 0 }); } handleMouseDown = e => { this.updateCurrentStep(e.clientX); this.setState({ lastStep: this.state.currentStep, }); }; handleMouseUp() { this.resetCurrentLeft(); this.handleChange(); } handleDragStart = () => { this.setState({ isDragging: true, lastStep: this.state.currentStep, }); document.addEventListener('mousemove', this.handleDrag); document.addEventListener('mouseup', this.handleDragEnd); }; handleDrag = e => { const { onMoveStart } = this.props; const currentValue = this.getCurrentValue(); e.preventDefault(); e.stopPropagation(); if (this.state.isDragging) { this.updateCurrentStep(e.touches ? e.touches[0].clientX : e.clientX); if (typeof onMoveStart === 'function') { onMoveStart(currentValue); } } }; handleDragEnd = () => { const { onMoveEnd } = this.props; this.setState({ isDragging: false, }); document.removeEventListener('mousemove', this.handleDrag); document.removeEventListener('mouseup', this.handleDragEnd); this.thumbRef.current.blur(); this.resetCurrentLeft(); this.handleChange(); if (typeof onMoveEnd === 'function') { onMoveEnd(); } }; adjustCurrentStepBy = diff => { const { currentStep } = this.state; const steps = this.getNumberOfSteps(); const nextStep = currentStep + diff; this.setState({ currentStep: nextStep <= steps - 1 && nextStep >= 0 ? nextStep : currentStep, }); }; handleKeyUp() { this.setState({ lastStep: this.state.currentStep, }); this.handleChange(); } handleKeyDown = e => { e.preventDefault(); e.stopPropagation(); const key = e.which; if (key === KEY_UP || key === KEY_RIGHT) { this.adjustCurrentStepBy(1); } else if (key === KEY_DOWN || key === KEY_LEFT) { this.adjustCurrentStepBy(-1); } }; renderThumb() { const { minValue, maxValue, theme, disabled } = this.props; const value = this.getCurrentValue(); const left = this.getLeftPositionFromCurrentStep(); const styleProps = { isDragging: this.state.isDragging, left, theme, disabled, }; return ( <Click ref={this.thumbRef} extend={[ thumbStyle(styleProps), getThemeStyle('sliderThumb', theme, styleProps), ]} role="slider" aria-valuemin={minValue} aria-valuemax={maxValue} aria-valuenow={value} aria-orientation="horizontal" disabled={disabled} onMouseDown={() => this.handleDragStart()} onMouseMove={e => this.handleDrag(e)} onMouseUp={() => this.handleDragEnd()} onContextMenu={() => this.handleDragEnd()} onTouchStart={() => this.handleDragStart()} onTouchMove={e => this.handleDrag(e)} onTouchEnd={() => this.handleDragEnd()} onKeyDown={e => this.handleKeyDown(e)} onKeyUp={() => this.handleKeyUp()} > <Arrow color={ theme.tokens[disabled ? 'inputDisabledControl' : 'inputControl'] } direction="left" /> <Arrow color={ theme.tokens[disabled ? 'inputDisabledControl' : 'inputControl'] } direction="right" /> </Click> ); } render() { const { theme } = this.props; return ( <Block extend={[ { position: 'relative', height: 42, }, getThemeStyle('slider', theme), ]} > <Block extend={[ { position: 'absolute', top: 9, left: 0, right: 0, width: '100%', boxSizing: 'border-box', height: 21, borderWidth: 1, borderStyle: 'solid', borderColor: theme.tokens.inputBorder, background: theme.tokens.inputBackground, cursor: this.props.disabled ? 'not-allwoed' : 'pointer', }, getThemeStyle('sliderTrack', theme), ]} ref={this.trackRef} onMouseDown={e => { if (!this.props.disabled) { this.handleMouseDown(e); } }} onMouseUp={() => { if (!this.props.disabled) { this.handleMouseUp(); } }} /> {typeof this.state.currentStep !== 'undefined' && this.renderThumb()} </Block> ); } } SliderComponent.displayName = 'Slider'; const Slider = withTheme(SliderComponent); export { Slider };