diff --git a/docs/src/modules/components/withRoot.js b/docs/src/modules/components/withRoot.js
index ecb111e318d9c7..3d8b7a25042309 100644
--- a/docs/src/modules/components/withRoot.js
+++ b/docs/src/modules/components/withRoot.js
@@ -186,6 +186,9 @@ const pages = [
{
pathname: '/lab/speed-dial',
},
+ {
+ pathname: '/lab/slider',
+ },
findPages[2].children[1],
],
},
diff --git a/docs/src/pages/getting-started/supported-components/supported-components.md b/docs/src/pages/getting-started/supported-components/supported-components.md
index 19466330a981b1..bf160e472d55fa 100644
--- a/docs/src/pages/getting-started/supported-components/supported-components.md
+++ b/docs/src/pages/getting-started/supported-components/supported-components.md
@@ -80,9 +80,10 @@ to discuss the approach before submitting a PR.
- **[Checkbox](https://material.io/design/components/selection-controls.html#checkboxes) ✓**
- **[Radio button](https://material.io/design/components/selection-controls.html#radio-buttons) ✓**
- **[Switch](https://material.io/design/components/selection-controls.html#switches) ✓**
-- [Sliders](https://material.io/design/components/sliders.html)
- - [Continuous](https://material.io/design/components/sliders.html#continuous-slider)
- - [Discrete](https://material.io/design/components/sliders.html#discrete-slider)
+- **[Sliders](https://material.io/design/components/sliders.html) ~
+ **([Lab](/lab/about))
+ - **[Continuous](https://material.io/design/components/sliders.html#continuous-slider) ✓**
+ - **[Discrete](https://material.io/design/components/sliders.html#discrete-slider) ~** (WIP)
- **[Snackbars](https://material.io/archive/guidelines/components/snackbars-toasts.html) ✓** (*Legacy Material v1*)
- **[Subheaders](https://material.io/archive/guidelines/components/subheaders.html) ✓** (*Legacy Material v1*)
- **[List](https://material.io/archive/guidelines/components/subheaders.html#subheaders-list-subheaders) ✓**
diff --git a/docs/src/pages/lab/slider/DisabledSlider.js b/docs/src/pages/lab/slider/DisabledSlider.js
new file mode 100644
index 00000000000000..6ed0d28711ea96
--- /dev/null
+++ b/docs/src/pages/lab/slider/DisabledSlider.js
@@ -0,0 +1,26 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withStyles } from 'material-ui/styles';
+import Slider from '@material-ui/lab/Slider';
+
+const styles = {
+ container: {
+ width: 300,
+ },
+};
+
+function DisabledSlider({ classes }) {
+ return (
+
+
+
+
+
+ );
+}
+
+DisabledSlider.propTypes = {
+ classes: PropTypes.object.isRequired,
+};
+
+export default withStyles(styles)(DisabledSlider);
diff --git a/docs/src/pages/lab/slider/ReverseSlider.js b/docs/src/pages/lab/slider/ReverseSlider.js
new file mode 100644
index 00000000000000..f0c55a9f1b2e9c
--- /dev/null
+++ b/docs/src/pages/lab/slider/ReverseSlider.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withStyles } from 'material-ui/styles';
+import Slider from '@material-ui/lab/Slider';
+
+const styles = {
+ container: {
+ width: 300,
+ },
+};
+
+class ReverseSlider extends React.Component {
+ state = { value: 50 };
+
+ handleChange = (event, value) => this.setState({ value });
+ render() {
+ const { classes } = this.props;
+ const { value } = this.state;
+
+ return (
+
+
+
+
+ );
+ }
+}
+
+ReverseSlider.propTypes = {
+ classes: PropTypes.object.isRequired,
+};
+
+export default withStyles(styles)(ReverseSlider);
diff --git a/docs/src/pages/lab/slider/SimpleSlider.js b/docs/src/pages/lab/slider/SimpleSlider.js
new file mode 100644
index 00000000000000..2cd21339ba38ed
--- /dev/null
+++ b/docs/src/pages/lab/slider/SimpleSlider.js
@@ -0,0 +1,35 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withStyles } from 'material-ui/styles';
+import Typography from 'material-ui/Typography';
+import Slider from '@material-ui/lab/Slider';
+
+const styles = {
+ container: {
+ width: 300,
+ },
+};
+
+class SimpleSlider extends React.Component {
+ state = { value: 50 };
+
+ handleChange = (event, value) => this.setState({ value });
+
+ render() {
+ const { classes } = this.props;
+ const { value } = this.state;
+
+ return (
+
+ Slider label
+
+
+ );
+ }
+}
+
+SimpleSlider.propTypes = {
+ classes: PropTypes.object.isRequired,
+};
+
+export default withStyles(styles)(SimpleSlider);
diff --git a/docs/src/pages/lab/slider/StepSlider.js b/docs/src/pages/lab/slider/StepSlider.js
new file mode 100644
index 00000000000000..63a2fa74ebbaa3
--- /dev/null
+++ b/docs/src/pages/lab/slider/StepSlider.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withStyles } from 'material-ui/styles';
+import Slider from '@material-ui/lab/Slider';
+
+const styles = {
+ container: {
+ width: 300,
+ },
+};
+
+class StepSlider extends React.Component {
+ state = { value: 3 };
+
+ handleChange = (event, value) => this.setState({ value });
+
+ render() {
+ const { classes } = this.props;
+ const { value } = this.state;
+
+ return (
+
+
+
+ );
+ }
+}
+
+StepSlider.propTypes = {
+ classes: PropTypes.object.isRequired,
+};
+
+export default withStyles(styles)(StepSlider);
diff --git a/docs/src/pages/lab/slider/VerticalSlider.js b/docs/src/pages/lab/slider/VerticalSlider.js
new file mode 100644
index 00000000000000..0bd1bdf69c3b76
--- /dev/null
+++ b/docs/src/pages/lab/slider/VerticalSlider.js
@@ -0,0 +1,33 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withStyles } from 'material-ui/styles';
+import Slider from '@material-ui/lab/Slider';
+
+const styles = {
+ container: {
+ display: 'flex',
+ height: 300,
+ },
+};
+
+class VerticalSlider extends React.Component {
+ state = { value: 50 };
+ handleChange = (event, value) => this.setState({ value });
+ render() {
+ const { classes } = this.props;
+ const { value } = this.state;
+
+ return (
+
+
+
+
+ );
+ }
+}
+
+VerticalSlider.propTypes = {
+ classes: PropTypes.object.isRequired,
+};
+
+export default withStyles(styles)(VerticalSlider);
diff --git a/docs/src/pages/lab/slider/slider.md b/docs/src/pages/lab/slider/slider.md
new file mode 100644
index 00000000000000..877122c2b6e515
--- /dev/null
+++ b/docs/src/pages/lab/slider/slider.md
@@ -0,0 +1,22 @@
+---
+components: Slider
+---
+
+# Slider
+
+A [slider](https://material.io/guidelines/components/sliders.html) is an interface for users to input a value in a range. Sliders can be continuous or discrete and can be enabled or disabled.
+
+## Simple slider
+{{"demo": "pages/lab/slider/SimpleSlider.js"}}
+
+## Slider with steps
+{{"demo": "pages/lab/slider/StepSlider.js"}}
+
+## Disabled slider
+{{"demo": "pages/lab/slider/DisabledSlider.js"}}
+
+## Vertical slider
+{{"demo": "pages/lab/slider/VerticalSlider.js"}}
+
+## Reverse slider
+{{"demo": "pages/lab/slider/ReverseSlider.js"}}
\ No newline at end of file
diff --git a/packages/material-ui-lab/src/Slider/Slider.js b/packages/material-ui-lab/src/Slider/Slider.js
new file mode 100644
index 00000000000000..90c876a9212b86
--- /dev/null
+++ b/packages/material-ui-lab/src/Slider/Slider.js
@@ -0,0 +1,523 @@
+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 wrapper container */
+ container: {
+ 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,
+ value,
+ min,
+ max,
+ vertical,
+ reverse,
+ disabled,
+ ...otherProps
+ } = 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 containerClasses = classNames(classes.container, {
+ [classes.vertical]: vertical,
+ [classes.reverse]: reverse,
+ [classes.disabled]: disabled,
+ });
+
+ 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 (
+ {
+ this.container = findDOMNode(node);
+ }}
+ {...otherProps}
+ >
+
+
+
+
+ );
+ }
+}
+
+Slider.propTypes = {
+ /**
+ * Useful to extend the style applied to components.
+ */
+ classes: PropTypes.object.isRequired,
+ /**
+ * @ignore
+ */
+ 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,
+ /**
+ * If `true`, the slider will be vertical.
+ */
+ vertical: PropTypes.bool,
+};
+
+Slider.defaultProps = {
+ min: 0,
+ max: 100,
+ value: 50,
+ component: 'div',
+};
+
+export default withStyles(style, { name: 'MuiSlider', withTheme: true })(Slider);
diff --git a/packages/material-ui-lab/src/Slider/Slider.test.js b/packages/material-ui-lab/src/Slider/Slider.test.js
new file mode 100644
index 00000000000000..3b84838700d511
--- /dev/null
+++ b/packages/material-ui-lab/src/Slider/Slider.test.js
@@ -0,0 +1,146 @@
+import React from 'react';
+import { spy, useFakeTimers } from 'sinon';
+import { assert } from 'chai';
+import { createMount, createShallow, getClasses } from '@material-ui/core/test-utils';
+import Slider from './Slider';
+
+describe('', () => {
+ let mount;
+ let shallow;
+ let classes;
+
+ before(() => {
+ shallow = createShallow({ dive: true });
+ classes = getClasses();
+ mount = createMount();
+ });
+
+ it('should render a div', () => {
+ const wrapper = shallow();
+ assert.strictEqual(wrapper.name(), 'div');
+ });
+
+ it('should render with the default classes', () => {
+ const wrapper = shallow();
+ assert.strictEqual(wrapper.hasClass(classes.container), true);
+ });
+
+ it('should call handlers', () => {
+ const handleChange = spy();
+ const handleDragStart = spy();
+ const handleDragEnd = spy();
+
+ const wrapper = mount(
+ ,
+ );
+ const button = wrapper.find('button');
+
+ wrapper.simulate('click');
+ button.simulate('mousedown');
+ button.simulate('mouseup');
+
+ assert.strictEqual(handleChange.callCount, 1, 'should have called the handleChange cb');
+ assert.strictEqual(handleDragStart.callCount, 1, 'should have called the handleDragStart cb');
+ assert.strictEqual(handleDragEnd.callCount, 1, 'should have called the handleDragEnd cb');
+ });
+
+ describe('prop: vertical', () => {
+ it('should render with the default and vertical classes', () => {
+ const wrapper = shallow();
+ assert.strictEqual(wrapper.hasClass(classes.container), true);
+ assert.strictEqual(wrapper.hasClass(classes.vertical), true);
+ });
+ });
+
+ describe('prop: reverse', () => {
+ it('should render with the default and reverse classes', () => {
+ const wrapper = shallow();
+ assert.strictEqual(wrapper.hasClass(classes.container), true);
+ assert.strictEqual(wrapper.hasClass(classes.reverse), true);
+ });
+ });
+
+ describe('props: vertical & reverse', () => {
+ it('should render with the default, reverse and vertical classes', () => {
+ const wrapper = shallow();
+ assert.strictEqual(wrapper.hasClass(classes.container), true);
+ assert.strictEqual(wrapper.hasClass(classes.reverse), true);
+ assert.strictEqual(wrapper.hasClass(classes.vertical), true);
+ });
+ });
+
+ describe('prop: disabled', () => {
+ const handleChange = spy();
+ let wrapper;
+
+ before(() => {
+ wrapper = mount();
+ });
+
+ it('should render thumb with the disabled classes', () => {
+ const button = wrapper.find('button');
+
+ assert.strictEqual(button.hasClass(classes.thumb), true);
+ assert.strictEqual(button.hasClass(classes.disabled), true);
+ });
+
+ it('should render tracks with the disabled classes', () => {
+ const tracks = wrapper.find('div').filterWhere(n => n.hasClass(classes.track));
+
+ assert.strictEqual(tracks.everyWhere(n => n.hasClass(classes.disabled)), true);
+ });
+
+ it("should not call 'onChange' handler", () => {
+ wrapper.simulate('click');
+
+ assert.strictEqual(handleChange.callCount, 0);
+ });
+ });
+
+ describe('prop: value', () => {
+ const transitionComplexDuration = 375;
+ let wrapper;
+ let clock;
+
+ before(() => {
+ clock = useFakeTimers();
+ wrapper = mount();
+ });
+
+ after(() => {
+ clock.restore();
+ });
+
+ it('should render thumb in initial state', () => {
+ const button = wrapper.find('button');
+ assert.strictEqual(button.prop('style').left, '0%');
+ });
+
+ it('should render tracks in initial state', () => {
+ const tracks = wrapper.find('div').filterWhere(n => n.hasClass(classes.track));
+ const trackBefore = tracks.at(0);
+ const trackAfter = tracks.at(1);
+
+ assert.strictEqual(trackBefore.prop('style').width, 'calc(0% - 0px)');
+ assert.strictEqual(trackAfter.prop('style').width, 'calc(100% - 5px)');
+ });
+
+ it('after change value should change position of thumb', () => {
+ wrapper.setProps({ value: 0.5 });
+
+ clock.tick(transitionComplexDuration);
+
+ const button = wrapper.find('button');
+ assert.strictEqual(button.prop('style').left, '50%');
+ });
+
+ it('should render tracks in new state', () => {
+ const tracks = wrapper.find('div').filterWhere(n => n.hasClass(classes.track));
+ const trackBefore = tracks.at(0);
+ const trackAfter = tracks.at(1);
+
+ assert.strictEqual(trackBefore.prop('style').width, 'calc(50% - 0px)');
+ assert.strictEqual(trackAfter.prop('style').width, 'calc(50% - 7px)');
+ });
+ });
+});
diff --git a/packages/material-ui-lab/src/Slider/index.js b/packages/material-ui-lab/src/Slider/index.js
new file mode 100644
index 00000000000000..9898d6a85d1d01
--- /dev/null
+++ b/packages/material-ui-lab/src/Slider/index.js
@@ -0,0 +1 @@
+export { default } from './Slider';
diff --git a/packages/material-ui-lab/src/utils/clamp.js b/packages/material-ui-lab/src/utils/clamp.js
new file mode 100644
index 00000000000000..c0aa7667a1d1e6
--- /dev/null
+++ b/packages/material-ui-lab/src/utils/clamp.js
@@ -0,0 +1,3 @@
+export default function clamp(value, min = 0, max = 100) {
+ return Math.min(Math.max(value, min), max);
+}
diff --git a/pages/lab/api/slider.js b/pages/lab/api/slider.js
new file mode 100644
index 00000000000000..c1fc1c5861e383
--- /dev/null
+++ b/pages/lab/api/slider.js
@@ -0,0 +1,10 @@
+import React from 'react';
+import withRoot from 'docs/src/modules/components/withRoot';
+import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs';
+import markdown from './slider.md';
+
+function Page() {
+ return ;
+}
+
+export default withRoot(Page);
diff --git a/pages/lab/api/slider.md b/pages/lab/api/slider.md
new file mode 100644
index 00000000000000..1cd180064bf156
--- /dev/null
+++ b/pages/lab/api/slider.md
@@ -0,0 +1,32 @@
+---
+filename: /packages/material-ui-lab/src/Slider/Slider.js
+---
+
+
+
+# Slider
+
+
+
+## Props
+
+| Name | Type | Default | Description |
+|:-----|:-----|:--------|:------------|
+| classes | object | | Useful to extend the style applied to components. |
+| disabled | bool | | If `true`, the slider will be disabled. |
+| max | number | 100 | The maximum allowed value of the slider. Should not be equal to min. |
+| min | number | 0 | The minimum allowed value of the slider. Should not be equal to max. |
+| onChange | func | | Callback function that is fired when the slider's value changed. |
+| onDragEnd | func | | Callback function that is fired when the slide has stopped moving. |
+| onDragStart | func | | Callback function that is fired when the slider has begun to move. |
+| reverse | bool | | If `true`, the slider will be reversed. |
+| step | number | | The granularity the slider can step through values. |
+| value | number | 50 | The value of the slider. |
+| vertical | bool | | If `true`, the slider will be vertical. |
+
+Any other properties supplied will be [spread to the root element](/guides/api#spread).
+
+## Demos
+
+- [Slider](/lab/slider)
+
diff --git a/pages/lab/slider.js b/pages/lab/slider.js
new file mode 100644
index 00000000000000..d482fb6a00c97a
--- /dev/null
+++ b/pages/lab/slider.js
@@ -0,0 +1,51 @@
+import React from 'react';
+import withRoot from 'docs/src/modules/components/withRoot';
+import MarkdownDocs from 'docs/src/modules/components/MarkdownDocs';
+import markdown from 'docs/src/pages/lab/slider/slider.md';
+
+function Page() {
+ return (
+
+ );
+}
+
+export default withRoot(Page);