diff --git a/docs/src/pages/component-api/MobileStepper/MobileStepper.md b/docs/src/pages/component-api/MobileStepper/MobileStepper.md
new file mode 100644
index 00000000000000..e7ce8c6302025c
--- /dev/null
+++ b/docs/src/pages/component-api/MobileStepper/MobileStepper.md
@@ -0,0 +1,40 @@
+# MobileStepper
+
+
+
+## Props
+| Name | Type | Default | Description |
+|:-----|:-----|:--------|:------------|
+| activeStep | number | 0 | Set the active step (zero based index). This will enable `Step` control helpers. |
+| backButtonText | node | 'Back' | Set the text that appears for the back button. |
+| classes | object | | Useful to extend the style applied to components. |
+| disableBack | bool | false | Set to true to disable the back button. |
+| disableNext | bool | false | Set to true to disable the next button. |
+| nextButtonText | node | 'Next' | Set the text that appears for the next button. |
+| onBack * | function | | Passed into the onTouchTap prop of the Back button. |
+| onNext * | function | | Passed into the onTouchTap prop of the Next button. |
+| position | enum: 'bottom'
'top'
'static'
| 'bottom' | Set the text that appears for the next button. |
+| steps * | number | | The total steps. |
+| type | enum: 'text'
'dots'
'progress'
| 'dots' | The type of mobile stepper to use. |
+
+Any other properties supplied will be spread to the root element.
+## Classes
+
+You can overrides all the class names injected by Material-UI thanks to the `classes` property.
+This property accepts the following keys:
+- `root`
+- `position-bottom`
+- `positon-top`
+- `position-static`
+- `button`
+- `dots`
+- `dot`
+- `dotActive`
+- `progress`
+
+Have a look at [overriding with class names](/customization/overrides#overriding-with-class-names)
+section for more detail.
+
+If using the `overrides` key of the theme as documented
+[here](/customization/themes#customizing-all-instances-of-a-component-type),
+you need to use the following style sheet name: `MuiMobileStepper`.
diff --git a/docs/src/pages/component-demos/stepper/DotsMobileStepper.js b/docs/src/pages/component-demos/stepper/DotsMobileStepper.js
new file mode 100644
index 00000000000000..4a0ddde52f2924
--- /dev/null
+++ b/docs/src/pages/component-demos/stepper/DotsMobileStepper.js
@@ -0,0 +1,54 @@
+// @flow
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withStyles, createStyleSheet } from 'material-ui/styles';
+import MobileStepper from 'material-ui/MobileStepper';
+
+const styleSheet = createStyleSheet('DotsMobileStepper', {
+ root: {
+ maxWidth: 400,
+ flexGrow: 1,
+ },
+});
+
+class DotsMobileStepper extends Component {
+ state = {
+ activeStep: 0,
+ };
+
+ handleNext = () => {
+ this.setState({
+ activeStep: this.state.activeStep + 1,
+ });
+ };
+
+ handleBack = () => {
+ this.setState({
+ activeStep: this.state.activeStep - 1,
+ });
+ };
+
+ render() {
+ const classes = this.props.classes;
+ return (
+
+ );
+ }
+}
+
+DotsMobileStepper.propTypes = {
+ classes: PropTypes.object.isRequired,
+};
+
+export default withStyles(styleSheet)(DotsMobileStepper);
diff --git a/docs/src/pages/component-demos/stepper/ProgressMobileStepper.js b/docs/src/pages/component-demos/stepper/ProgressMobileStepper.js
new file mode 100644
index 00000000000000..93f72d2628629d
--- /dev/null
+++ b/docs/src/pages/component-demos/stepper/ProgressMobileStepper.js
@@ -0,0 +1,54 @@
+// @flow
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withStyles, createStyleSheet } from 'material-ui/styles';
+import MobileStepper from 'material-ui/MobileStepper';
+
+const styleSheet = createStyleSheet('ProgressMobileStepper', {
+ root: {
+ maxWidth: 400,
+ flexGrow: 1,
+ },
+});
+
+class ProgressMobileStepper extends Component {
+ state = {
+ activeStep: 0,
+ };
+
+ handleNext = () => {
+ this.setState({
+ activeStep: this.state.activeStep + 1,
+ });
+ };
+
+ handleBack = () => {
+ this.setState({
+ activeStep: this.state.activeStep - 1,
+ });
+ };
+
+ render() {
+ const classes = this.props.classes;
+ return (
+
+ );
+ }
+}
+
+ProgressMobileStepper.propTypes = {
+ classes: PropTypes.object.isRequired,
+};
+
+export default withStyles(styleSheet)(ProgressMobileStepper);
diff --git a/docs/src/pages/component-demos/stepper/TextMobileStepper.js b/docs/src/pages/component-demos/stepper/TextMobileStepper.js
new file mode 100644
index 00000000000000..6b1a77ff98dac1
--- /dev/null
+++ b/docs/src/pages/component-demos/stepper/TextMobileStepper.js
@@ -0,0 +1,71 @@
+// @flow
+
+import React, { Component } from 'react';
+import PropTypes from 'prop-types';
+import { withStyles, createStyleSheet } from 'material-ui/styles';
+import MobileStepper from 'material-ui/MobileStepper';
+import Paper from 'material-ui/Paper';
+import Typography from 'material-ui/Typography';
+
+const styleSheet = createStyleSheet('TextMobileStepper', theme => ({
+ root: {
+ maxWidth: 400,
+ flexGrow: 1,
+ },
+ header: {
+ display: 'flex',
+ alignItems: 'center',
+ height: 50,
+ paddingLeft: theme.spacing.unit * 5,
+ marginBottom: 20,
+ background: theme.palette.background.default,
+ },
+}));
+
+class TextMobileStepper extends Component {
+ state = {
+ activeStep: 0,
+ };
+
+ handleNext = () => {
+ this.setState({
+ activeStep: this.state.activeStep + 1,
+ });
+ };
+
+ handleBack = () => {
+ this.setState({
+ activeStep: this.state.activeStep - 1,
+ });
+ };
+
+ render() {
+ const classes = this.props.classes;
+ return (
+
+
+
+ Step {this.state.activeStep + 1} of 6
+
+
+
+
+ );
+ }
+}
+
+TextMobileStepper.propTypes = {
+ classes: PropTypes.object.isRequired,
+};
+
+export default withStyles(styleSheet)(TextMobileStepper);
diff --git a/docs/src/pages/component-demos/stepper/stepper.md b/docs/src/pages/component-demos/stepper/stepper.md
new file mode 100644
index 00000000000000..3438946c1dd5e9
--- /dev/null
+++ b/docs/src/pages/component-demos/stepper/stepper.md
@@ -0,0 +1,31 @@
+---
+components: MobileStepper
+---
+
+# Stepper
+
+[Steppers](https://material.io/guidelines/components/steppers.html) convey progress through numbered steps.
+
+## Mobile Stepper
+
+The [mobile steps](https://material.io/guidelines/components/steppers.html#steppers-types-of-steps) implements a compact stepper suitable for a mobile device.
+
+### Mobile Stepper - Text
+
+This is essentially a back/next button positioned correctly.
+You must implement the textual description yourself however an example is provided below for reference.
+
+{{demo='pages/component-demos/stepper/TextMobileStepper.js'}}
+
+### Mobile Stepper - Dots
+
+Use dots when the number of steps isn’t large.
+
+{{demo='pages/component-demos/stepper/DotsMobileStepper.js'}}
+
+### Mobile Stepper - Progress
+
+Use a progress bar when there are many steps, or if there are steps that need to be inserted during the process (based on responses to earlier steps).
+
+{{demo='pages/component-demos/stepper/ProgressMobileStepper.js'}}
+
diff --git a/docs/src/pages/getting-started/supported-components.md b/docs/src/pages/getting-started/supported-components.md
index dc239579be2e2a..a1e152196530b3 100644
--- a/docs/src/pages/getting-started/supported-components.md
+++ b/docs/src/pages/getting-started/supported-components.md
@@ -96,6 +96,7 @@ to discuss the approach before submitting a PR.
- [Steppers](https://www.google.com/design/spec/components/steppers.html)
- [Horizontal](https://www.google.com/design/spec/components/steppers.html#steppers-types-of-steppers)
- [Vertical](https://www.google.com/design/spec/components/steppers.html#steppers-types-of-steppers)
+ - **[Mobile steps](https://material.io/guidelines/components/steppers.html#steppers-types-of-steps) ✓**
- **[Tabs](https://www.google.com/design/spec/components/tabs.html) ✓**
- Usage
- **[Mobile (Full width)](https://www.google.com/design/spec/components/tabs.html#tabs-usage) ✓**
diff --git a/src/MobileStepper/MobileStepper.js b/src/MobileStepper/MobileStepper.js
new file mode 100644
index 00000000000000..b1633c084be119
--- /dev/null
+++ b/src/MobileStepper/MobileStepper.js
@@ -0,0 +1,167 @@
+// @flow weak
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import classNames from 'classnames';
+import { createStyleSheet } from 'jss-theme-reactor';
+import withStyles from '../styles/withStyles';
+import Paper from '../Paper';
+import Button from '../Button';
+import KeyboardArrowLeft from '../svg-icons/keyboard-arrow-left';
+import KeyboardArrowRight from '../svg-icons/keyboard-arrow-right';
+import { LinearProgress } from '../Progress';
+
+export const styleSheet = createStyleSheet('MuiMobileStepper', theme => ({
+ root: {
+ display: 'flex',
+ flexDirection: 'row',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ background: theme.palette.background.default,
+ height: 50,
+ },
+ 'position-bottom': {
+ position: 'fixed',
+ bottom: 0,
+ left: 0,
+ right: 0,
+ zIndex: theme.zIndex.mobileStepper,
+ },
+ 'position-top': {
+ position: 'fixed',
+ top: 0,
+ left: 0,
+ right: 0,
+ zIndex: theme.zIndex.mobileStepper,
+ },
+ 'position-static': {},
+ button: {},
+ dots: {
+ display: 'flex',
+ flexDirection: 'row',
+ },
+ dot: {
+ backgroundColor: theme.palette.action.disabled,
+ borderRadius: '50%',
+ width: theme.spacing.unit,
+ height: theme.spacing.unit,
+ margin: '0 2px',
+ },
+ dotActive: {
+ backgroundColor: theme.palette.primary[500],
+ },
+ progress: {
+ width: '50%',
+ },
+}));
+
+function MobileStepper(props) {
+ const {
+ activeStep,
+ backButtonText,
+ classes,
+ className: classNameProp,
+ disableBack,
+ disableNext,
+ position,
+ type,
+ nextButtonText,
+ onBack,
+ onNext,
+ steps,
+ ...other
+ } = props;
+
+ const className = classNames(classes.root, classes[`position-${position}`], classNameProp);
+
+ return (
+
+
+ {type === 'dots' &&
+
+ {Array.from(Array(steps)).map((_, step) => {
+ const dotClassName = classNames(
+ {
+ [classes.dotActive]: step === activeStep,
+ },
+ classes.dot,
+ );
+ // eslint-disable-next-line react/no-array-index-key
+ return
;
+ })}
+
}
+ {type === 'progress' &&
+
+
+
}
+
+
+ );
+}
+
+MobileStepper.propTypes = {
+ /**
+ * Set the active step (zero based index). This will enable `Step` control helpers.
+ */
+ activeStep: PropTypes.number,
+ /**
+ * Set the text that appears for the back button.
+ */
+ backButtonText: PropTypes.node,
+ /**
+ * Useful to extend the style applied to components.
+ */
+ classes: PropTypes.object.isRequired,
+ /**
+ * @ignore
+ */
+ className: PropTypes.string,
+ /**
+ * Set to true to disable the back button.
+ */
+ disableBack: PropTypes.bool,
+ /**
+ * Set to true to disable the next button.
+ */
+ disableNext: PropTypes.bool,
+ /**
+ * Set the text that appears for the next button.
+ */
+ nextButtonText: PropTypes.node,
+ /**
+ * Passed into the onTouchTap prop of the Back button.
+ */
+ onBack: PropTypes.func.isRequired,
+ /**
+ * Passed into the onTouchTap prop of the Next button.
+ */
+ onNext: PropTypes.func.isRequired,
+ /**
+ * Set the text that appears for the next button.
+ */
+ position: PropTypes.oneOf(['bottom', 'top', 'static']),
+ /**
+ * The total steps.
+ */
+ steps: PropTypes.number.isRequired,
+ /**
+ * The type of mobile stepper to use.
+ */
+ type: PropTypes.oneOf(['text', 'dots', 'progress']),
+};
+
+MobileStepper.defaultProps = {
+ activeStep: 0,
+ backButtonText: 'Back',
+ disableBack: false,
+ disableNext: false,
+ nextButtonText: 'Next',
+ position: 'bottom',
+ type: 'dots',
+};
+
+export default withStyles(styleSheet)(MobileStepper);
diff --git a/src/MobileStepper/MobileStepper.spec.js b/src/MobileStepper/MobileStepper.spec.js
new file mode 100644
index 00000000000000..0dadff72c6bf60
--- /dev/null
+++ b/src/MobileStepper/MobileStepper.spec.js
@@ -0,0 +1,174 @@
+// @flow
+
+import React from 'react';
+import { assert } from 'chai';
+import { createShallow } from '../test-utils';
+import MobileStepper, { styleSheet } from './MobileStepper';
+import Button from '../Button/Button';
+import KeyboardArrowLeft from '../svg-icons/keyboard-arrow-left';
+import KeyboardArrowRight from '../svg-icons/keyboard-arrow-right';
+import { LinearProgress } from '../Progress';
+
+describe('', () => {
+ let shallow;
+ let classes;
+ const defaultProps = {
+ steps: 2,
+ onBack: () => {},
+ onNext: () => {},
+ };
+
+ before(() => {
+ shallow = createShallow({ dive: true });
+ classes = shallow.context.styleManager.render(styleSheet);
+ });
+
+ it('should render a Paper component', () => {
+ const wrapper = shallow();
+ assert.strictEqual(wrapper.name(), 'withStyles(Paper)');
+ assert.strictEqual(wrapper.props().elevation, 0, 'should have no elevation');
+ });
+
+ it('should render with the root class', () => {
+ const wrapper = shallow();
+ assert.strictEqual(wrapper.hasClass(classes.root), true, 'should have the root class');
+ });
+
+ it('should render the custom className and the root class', () => {
+ const wrapper = shallow();
+ assert.strictEqual(wrapper.is('.test-class-name'), true, 'should pass the test className');
+ assert.strictEqual(wrapper.hasClass(classes.root), true, 'should have the mobileStepper class');
+ });
+
+ it('should render with the bottom class if position prop is set to bottom', () => {
+ const wrapper = shallow();
+ assert.strictEqual(wrapper.hasClass(classes['position-bottom']), true);
+ });
+
+ it('should render with the top class if position prop is set to top', () => {
+ const wrapper = shallow();
+ assert.strictEqual(wrapper.hasClass(classes['position-top']), true);
+ });
+
+ it('should render two buttons', () => {
+ const wrapper = shallow();
+ assert.lengthOf(wrapper.find(Button), 2, 'should render two buttons');
+ });
+
+ it('should render the back button', () => {
+ const wrapper = shallow();
+ const backButton = wrapper.childAt(0);
+ assert.strictEqual(backButton.childAt(1).text(), 'Back', 'should set the back button text');
+ assert.lengthOf(
+ backButton.find(KeyboardArrowLeft),
+ 1,
+ 'should render a single component',
+ );
+ });
+
+ it('should render next button', () => {
+ const wrapper = shallow();
+ const nextButton = wrapper.childAt(2);
+ assert.strictEqual(nextButton.childAt(0).text(), 'Next', 'should set the next button text');
+ assert.lengthOf(
+ nextButton.find(KeyboardArrowRight),
+ 1,
+ 'should render a single component',
+ );
+ });
+
+ it('should set the backButtonText', () => {
+ const wrapper = shallow();
+ assert.strictEqual(
+ wrapper.childAt(0).childAt(1).text(),
+ 'Past',
+ 'should set the back button text',
+ );
+ });
+
+ it('should set the nextButtonText', () => {
+ const wrapper = shallow();
+ assert.strictEqual(
+ wrapper.childAt(2).childAt(0).text(),
+ 'Future',
+ 'should set the back button text',
+ );
+ });
+
+ it('should disable the back button if prop disableBack is passed', () => {
+ const wrapper = shallow();
+ const backButton = wrapper.childAt(0);
+ assert.strictEqual(backButton.props().disabled, true, 'should disable the back button');
+ });
+
+ it('should disable the next button if prop disableNext is passed', () => {
+ const wrapper = shallow();
+ const nextButton = wrapper.childAt(2);
+ assert.strictEqual(nextButton.props().disabled, true, 'should disable the next button');
+ });
+
+ it('should render just two buttons when supplied with type text', () => {
+ const wrapper = shallow();
+ assert.lengthOf(wrapper.children(), 2, 'should render exactly two children');
+ });
+
+ it('should render dots when supplied with type dots', () => {
+ const wrapper = shallow();
+ assert.lengthOf(wrapper.children(), 3, 'should render exactly three children');
+ assert.strictEqual(
+ wrapper.childAt(1).hasClass(classes.dots),
+ true,
+ 'should have a single dots class',
+ );
+ });
+
+ it('should render a dot for each step when using dots type', () => {
+ const wrapper = shallow();
+ assert.lengthOf(wrapper.find(`.${classes.dot}`), 2, 'should render exactly two dots');
+ });
+
+ it('should render the first dot as active if activeStep is not set', () => {
+ const wrapper = shallow();
+ assert.strictEqual(
+ wrapper.childAt(1).childAt(0).hasClass(classes.dotActive),
+ true,
+ 'should render the first dot active',
+ );
+ });
+
+ it('should honour the activeStep prop', () => {
+ const wrapper = shallow();
+ assert.strictEqual(
+ wrapper.childAt(1).childAt(1).hasClass(classes.dotActive),
+ true,
+ 'should render the second dot active',
+ );
+ });
+
+ it('should render a when supplied with type progress', () => {
+ const wrapper = shallow();
+ assert.lengthOf(wrapper.find(LinearProgress), 1, 'should render a ');
+ });
+
+ it('should calculate the value correctly', () => {
+ const props = {
+ onBack: defaultProps.onBack,
+ onNext: defaultProps.onNext,
+ };
+ let wrapper = shallow();
+ let linearProgressProps = wrapper.find(LinearProgress).props();
+ assert.strictEqual(linearProgressProps.value, 0, 'should set value to 0');
+
+ wrapper = shallow();
+ linearProgressProps = wrapper.find(LinearProgress).props();
+ assert.strictEqual(linearProgressProps.value, 50, 'should set value to 50');
+
+ wrapper = shallow();
+ linearProgressProps = wrapper.find(LinearProgress).props();
+ assert.strictEqual(
+ linearProgressProps.value,
+ 100,
+ 'should set value to 100',
+ );
+ });
+});
diff --git a/src/MobileStepper/index.js b/src/MobileStepper/index.js
new file mode 100644
index 00000000000000..dbd564de92a83f
--- /dev/null
+++ b/src/MobileStepper/index.js
@@ -0,0 +1,3 @@
+// @flow
+
+export { default } from './MobileStepper';
diff --git a/src/styles/muiThemeProviderFactory.js b/src/styles/muiThemeProviderFactory.js
index da9980e8bea1a0..c0ae1314af11bb 100644
--- a/src/styles/muiThemeProviderFactory.js
+++ b/src/styles/muiThemeProviderFactory.js
@@ -96,6 +96,8 @@ export const MUI_SHEET_ORDER = [
'MuiToolbar',
'MuiBadge',
+
+ 'MuiMobileStepper',
];
export default function muiThemeProviderFactory(defaultTheme) {
diff --git a/src/styles/zIndex.js b/src/styles/zIndex.js
index f522efbb7ad5b2..fcc42e5b8da657 100644
--- a/src/styles/zIndex.js
+++ b/src/styles/zIndex.js
@@ -2,6 +2,7 @@
// Needed as the zIndex works with absolute values.
export default {
+ mobileStepper: 900,
menu: 1000,
appBar: 1100,
drawerOverlay: 1200,