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
JavaScript
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 };