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.

383 lines (339 loc) 9.3 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 }; 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; const newPositionLeft = position - trackLeft - thumbWidth / 2; 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 innerRef={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) ]} innerRef={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 };