@activelylearn/material-ui
Version:
Material-UI's workspace package
531 lines (465 loc) • 13.5 kB
JavaScript
import React from 'react';
import { findDOMNode } from 'react-dom';
import PropTypes from 'prop-types';
import keycode from 'keycode';
import classNames from 'classnames';
import withStyles from '@material-ui/core/styles/withStyles';
import ButtonBase from '@material-ui/core/ButtonBase';
import { fade } from '@material-ui/core/styles/colorManipulator';
import clamp from '../utils/clamp';
export const style = theme => {
const commonTransitionsOptions = {
duration: theme.transitions.duration.short,
easing: theme.transitions.easing.easeOut,
};
const commonTransitionsProperty = ['width', 'height', 'box-shadow', 'left', 'top'];
const commonTransitions = theme.transitions.create(
commonTransitionsProperty,
commonTransitionsOptions,
);
const colors = {
primary: theme.palette.primary.main,
secondary: theme.palette.grey[400],
focused: theme.palette.grey[500],
disabled: theme.palette.grey[400],
};
return {
// /* Styles for root node */
root: {
position: 'relative',
width: '100%',
margin: '10px 0',
padding: '6px 0',
cursor: 'pointer',
WebkitTapHighlightColor: 'transparent',
'&$disabled': {
cursor: 'no-drop',
},
'&$vertical': {
height: '100%',
margin: '0 10px',
padding: '0 6px',
},
'&$reverse': {
transform: 'scaleX(-1)',
},
'&$vertical$reverse': {
transform: 'scaleY(-1)',
},
},
/* Tracks styles */
track: {
position: 'absolute',
transform: 'translate(0, -50%)',
top: '50%',
height: 2,
'&$focused, &$activated': {
transition: 'none',
backgroundColor: colors.focused,
},
'&$disabled': {
backgroundColor: colors.secondary,
},
'&$vertical': {
transform: 'translate(-50%, 0)',
left: '50%',
top: 'initial',
width: 2,
},
'&$jumped': {
backgroundColor: colors.focused,
},
},
trackBefore: {
zIndex: 1,
left: 0,
backgroundColor: colors.primary,
transition: commonTransitions,
'&$focused, &$activated, &$jumped': {
backgroundColor: colors.primary,
},
},
trackAfter: {
right: 0,
backgroundColor: colors.secondary,
transition: commonTransitions,
'&$vertical': {
bottom: 0,
},
},
/* Thumb styles */
thumb: {
position: 'absolute',
zIndex: 2,
transform: 'translate(-50%, -50%)',
width: 12,
height: 12,
borderRadius: '50%',
transition: commonTransitions,
backgroundColor: colors.primary,
'&$focused': {
boxShadow: `0px 0px 0px 9px ${fade(colors.primary, 0.16)}`,
},
'&$activated': {
width: 17,
height: 17,
transition: 'none',
},
'&$disabled': {
cursor: 'no-drop',
width: 9,
height: 9,
backgroundColor: colors.disabled,
},
'&$zero': {
border: `2px solid ${colors.disabled}`,
backgroundColor: 'transparent',
},
'&$focused$zero': {
border: `2px solid ${colors.focused}`,
backgroundColor: fade(colors.focused, 0.34),
boxShadow: `0px 0px 0px 9px ${fade(colors.focused, 0.34)}`,
},
'&$activated$zero': {
border: `2px solid ${colors.focused}`,
},
'&$jumped': {
width: 17,
height: 17,
},
},
focused: {},
activated: {},
disabled: {},
zero: {},
vertical: {},
reverse: {},
jumped: {},
};
};
function addEventListener(node, event, handler, capture) {
node.addEventListener(event, handler, capture);
return {
remove: function remove() {
node.removeEventListener(event, handler, capture);
},
};
}
function percentToValue(percent, min, max) {
return ((max - min) * percent) / 100 + min;
}
function roundToStep(number, step) {
return Math.round(number / step) * step;
}
function getOffset(node) {
const { scrollY, scrollX } = global;
const { left, top } = node.getBoundingClientRect();
return {
top: top + scrollY,
left: left + scrollX,
};
}
function getMousePosition(event) {
if (event.changedTouches && event.changedTouches[0]) {
return {
x: event.changedTouches[0].pageX,
y: event.changedTouches[0].pageY,
};
}
return {
x: event.pageX,
y: event.pageY,
};
}
function calculatePercent(node, event, isVertical, isReverted) {
const { width, height } = node.getBoundingClientRect();
const { top, left } = getOffset(node);
const { x, y } = getMousePosition(event);
const value = isVertical ? y - top : x - left;
const onePercent = (isVertical ? height : width) / 100;
return isReverted ? 100 - clamp(value / onePercent) : clamp(value / onePercent);
}
function preventPageScrolling(event) {
event.preventDefault();
}
class Slider extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.disabled) {
return { currentState: 'disabled' };
}
if (!nextProps.disabled && prevState.currentState === 'disabled') {
return { currentState: 'normal' };
}
return null;
}
state = { currentState: 'initial' };
componentDidMount() {
if (this.container) {
this.container.addEventListener('touchstart', preventPageScrolling, { passive: false });
}
}
componentWillUnmount() {
this.container.removeEventListener('touchstart', preventPageScrolling, { passive: false });
}
emitChange(event, rawValue, callback) {
const { step, value: previousValue, onChange } = this.props;
let value = rawValue;
if (step) {
value = roundToStep(rawValue, step);
} else {
value = Number(rawValue.toFixed(3));
}
if (typeof onChange === 'function' && value !== previousValue) {
onChange(event, value);
if (typeof callback === 'function') {
callback();
}
}
}
calculateTrackAfterStyles(percent) {
const { currentState } = this.state;
switch (currentState) {
case 'activated':
return `calc(100% - ${percent === 0 ? 7 : 5}px)`;
case 'disabled':
return `calc(${100 - percent}% - 6px)`;
default:
return 'calc(100% - 5px)';
}
}
calculateTrackBeforeStyles(percent) {
const { currentState } = this.state;
switch (currentState) {
case 'disabled':
return `calc(${percent}% - 6px)`;
default:
return `${percent}%`;
}
}
handleKeyDown = event => {
const { min, max, value: currentValue } = this.props;
const onePercent = Math.abs((max - min) / 100);
const step = this.props.step || onePercent;
let value;
switch (keycode(event)) {
case 'home':
value = min;
break;
case 'end':
value = max;
break;
case 'page up':
value = currentValue + onePercent * 10;
break;
case 'page down':
value = currentValue - onePercent * 10;
break;
case 'right':
case 'up':
value = currentValue + step;
break;
case 'left':
case 'down':
value = currentValue - step;
break;
default:
return;
}
event.preventDefault();
value = clamp(value, min, max);
this.emitChange(event, value);
};
handleFocus = () => {
this.setState({ currentState: 'focused' });
};
handleBlur = () => {
this.setState({ currentState: 'normal' });
};
handleClick = event => {
const { min, max, vertical, reverse } = this.props;
const percent = calculatePercent(this.container, event, vertical, reverse);
const value = percentToValue(percent, min, max);
this.emitChange(event, value, () => {
this.playJumpAnimation();
});
};
handleTouchStart = event => {
this.setState({ currentState: 'activated' });
this.globalMouseUpListener = addEventListener(document, 'touchend', this.handleMouseUp);
if (typeof this.props.onDragStart === 'function') {
this.props.onDragStart(event);
}
};
handleMouseDown = event => {
this.setState({ currentState: 'activated' });
this.globalMouseUpListener = addEventListener(document, 'mouseup', this.handleMouseUp);
this.globalMouseMoveListener = addEventListener(document, 'mousemove', this.handleMouseMove);
if (typeof this.props.onDragEnd === 'function') {
this.props.onDragEnd(event);
}
};
handleMouseUp = event => {
this.setState({ currentState: 'normal' });
if (this.globalMouseUpListener) {
this.globalMouseUpListener.remove();
}
if (this.globalMouseMoveListener) {
this.globalMouseMoveListener.remove();
}
if (typeof this.props.onDragEnd === 'function') {
this.props.onDragEnd(event);
}
};
handleMouseMove = event => {
const { min, max, vertical, reverse } = this.props;
const percent = calculatePercent(this.container, event, vertical, reverse);
const value = percentToValue(percent, min, max);
this.emitChange(event, value);
};
playJumpAnimation() {
this.setState({ currentState: 'jumped' }, () => {
setTimeout(() => {
this.setState({ currentState: 'normal' });
}, this.props.theme.transitions.duration.complex);
});
}
render() {
const { currentState } = this.state;
const {
component: Component,
classes,
className: classNameProp,
value,
min,
max,
vertical,
reverse,
disabled,
...other
} = this.props;
const percent = clamp(((value - min) * 100) / (max - min));
const commonClasses = {
[classes.disabled]: disabled,
[classes.jumped]: !disabled && currentState === 'jumped',
[classes.focused]: !disabled && currentState === 'focused',
[classes.activated]: !disabled && currentState === 'activated',
};
const rootClasses = classNames(classes.root, {
[classes.vertical]: vertical,
[classes.reverse]: reverse,
[classes.disabled]: disabled,
classNameProp,
});
const trackBeforeClasses = classNames(classes.track, classes.trackBefore, commonClasses, {
[classes.vertical]: vertical,
});
const trackAfterClasses = classNames(classes.track, classes.trackAfter, commonClasses, {
[classes.vertical]: vertical,
});
const thumbClasses = classNames(classes.thumb, commonClasses, {
[classes.zero]: percent === 0,
});
const trackProperty = vertical ? 'height' : 'width';
const thumbProperty = vertical ? 'top' : 'left';
const inlineTrackBeforeStyles = { [trackProperty]: this.calculateTrackBeforeStyles(percent) };
const inlineTrackAfterStyles = { [trackProperty]: this.calculateTrackAfterStyles(percent) };
const inlineThumbStyles = { [thumbProperty]: `${percent}%` };
return (
<Component
role="slider"
className={rootClasses}
aria-valuenow={value}
aria-valuemin={min}
aria-valuemax={max}
aria-orientation={vertical ? 'vertical' : 'horizontal'}
onClick={this.handleClick}
ref={node => {
this.container = findDOMNode(node);
}}
{...other}
>
<div className={trackBeforeClasses} style={inlineTrackBeforeStyles} />
<ButtonBase
className={thumbClasses}
disableRipple
style={inlineThumbStyles}
onBlur={this.handleBlur}
onKeyDown={this.handleKeyDown}
onMouseDown={this.handleMouseDown}
onTouchStartCapture={this.handleTouchStart}
onTouchMove={this.handleMouseMove}
onFocusVisible={this.handleFocus}
/>
<div className={trackAfterClasses} style={inlineTrackAfterStyles} />
</Component>
);
}
}
Slider.propTypes = {
/**
* Override or extend the styles applied to the component.
* See [CSS API](#css-api) below for more details.
*/
classes: PropTypes.object.isRequired,
/**
* @ignore
*/
className: PropTypes.string,
/**
* The component used for the root node.
* Either a string to use a DOM element or a component.
*/
component: PropTypes.oneOfType([PropTypes.string, PropTypes.func]),
/**
* If `true`, the slider will be disabled.
*/
disabled: PropTypes.bool,
/**
* The maximum allowed value of the slider.
* Should not be equal to min.
*/
max: PropTypes.number,
/**
* The minimum allowed value of the slider.
* Should not be equal to max.
*/
min: PropTypes.number,
/**
* Callback function that is fired when the slider's value changed.
*/
onChange: PropTypes.func,
/**
* Callback function that is fired when the slide has stopped moving.
*/
onDragEnd: PropTypes.func,
/**
* Callback function that is fired when the slider has begun to move.
*/
onDragStart: PropTypes.func,
/**
* If `true`, the slider will be reversed.
*/
reverse: PropTypes.bool,
/**
* The granularity the slider can step through values.
*/
step: PropTypes.number,
/**
* @ignore
*/
theme: PropTypes.object.isRequired,
/**
* The value of the slider.
*/
value: PropTypes.number.isRequired,
/**
* If `true`, the slider will be vertical.
*/
vertical: PropTypes.bool,
};
Slider.defaultProps = {
min: 0,
max: 100,
component: 'div',
};
export default withStyles(style, { name: 'MuiSlider', withTheme: true })(Slider);