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