diff --git a/docs/src/app/AppRoutes.jsx b/docs/src/app/AppRoutes.jsx
index 407a303403ef3a..8a0a9960269bec 100644
--- a/docs/src/app/AppRoutes.jsx
+++ b/docs/src/app/AppRoutes.jsx
@@ -62,6 +62,8 @@ import Contributing from './components/pages/discover-more/Contributing';
import Showcase from './components/pages/discover-more/Showcase';
import RelatedProjects from './components/pages/discover-more/RelatedProjects';
+import StepperPage from './components/pages/components/Stepper/Page';
+
/**
* Routes: https://github.com/rackt/react-router/blob/master/docs/api/components/Route.md
*
@@ -121,6 +123,7 @@ const AppRoutes = (
+
diff --git a/docs/src/app/components/AppLeftNav.jsx b/docs/src/app/components/AppLeftNav.jsx
index 5de8164fb5a6c0..ccdb1d2bc755bf 100644
--- a/docs/src/app/components/AppLeftNav.jsx
+++ b/docs/src/app/components/AppLeftNav.jsx
@@ -237,6 +237,7 @@ const AppLeftNav = React.createClass({
]}
/>,
,
+ ,
,
,
,
diff --git a/docs/src/app/components/pages/components/Stepper/HorizontalLinearStepper.jsx b/docs/src/app/components/pages/components/Stepper/HorizontalLinearStepper.jsx
new file mode 100644
index 00000000000000..587ea1256039bb
--- /dev/null
+++ b/docs/src/app/components/pages/components/Stepper/HorizontalLinearStepper.jsx
@@ -0,0 +1,132 @@
+import React from 'react';
+
+import Stepper from 'material-ui/lib/Stepper/Stepper';
+import Step from 'material-ui/Stepper/HorizontalStep';
+
+import Paper from 'material-ui/lib/paper';
+import FontIcon from 'material-ui/lib/font-icon';
+import RaisedButton from 'material-ui/lib/raised-button';
+import FlatButton from 'material-ui/lib/flat-button';
+
+const HorizontalStepper = React.createClass({
+ getInitialState() {
+ return {
+ activeStepIndex: -1,
+ lastActiveStepIndex: 0,
+ };
+ },
+
+ selectStep(stepIndex) {
+ const {
+ lastActiveStepIndex,
+ activeStepIndex,
+
+ } = this.state;
+
+ if (stepIndex > lastActiveStepIndex) {
+ return;
+ }
+
+ this.setState({
+ activeStepIndex: stepIndex,
+ lastActiveStepIndex: Math.max(lastActiveStepIndex, activeStepIndex),
+ });
+ },
+
+ updateCompletedSteps(stepIndex) {
+ return stepIndex < this.state.lastActiveStepIndex;
+ },
+
+ createIcon(step) {
+ if (step.props.isCompleted) {
+ return (
+
+ done
+
+ );
+ }
+
+ return {step.props.orderStepLabel};
+ },
+
+ continue() {
+ const {
+ activeStepIndex,
+ lastActiveStepIndex,
+ } = this.state;
+
+ this.setState({
+ activeStepIndex: activeStepIndex + 1,
+ lastActiveStepIndex: Math.max(lastActiveStepIndex, activeStepIndex + 1),
+ });
+ },
+
+ render() {
+ return (
+
+
+ How to say goodbye to your 'css'
+
+
+ ,
+ ,
+ ]}
+ >
+
+ Don't take your time on your 'css'. And then you will see what will happen.
+
+
+ ,
+ ,
+ ]}
+ >
+
+ You don't update your knowledge and you will be out of date. You no longer understand
+ your 'css'. Then you will see what will happen
+
+
+
+ ,
+ ,
+ ]}
+ >
+
+ Good bye
+
+
+
+
+ );
+ },
+});
+
+export default HorizontalStepper;
diff --git a/docs/src/app/components/pages/components/Stepper/Page.jsx b/docs/src/app/components/pages/components/Stepper/Page.jsx
new file mode 100644
index 00000000000000..9a959e6be716a9
--- /dev/null
+++ b/docs/src/app/components/pages/components/Stepper/Page.jsx
@@ -0,0 +1,89 @@
+import React from 'react';
+
+import CodeExample from '../../../CodeExample';
+import PropTypeDescription from '../../../PropTypeDescription';
+import MarkdownElement from '../../../MarkdownElement';
+
+import stepperReadmeText from './README';
+
+import VerticalLinearStepper from './VerticalLinearStepper';
+import VerticalNonLinearStepper from './VerticalNonLinearStepper';
+import VerticalLinearStepperWithOptionalStep from './VerticalLinearStepperWithOptionalStep';
+import VerticalLinearStepperCode from '!raw!./VerticalLinearStepper';
+import VerticalLinearStepperWithOptionalStepCode from '!raw!./VerticalLinearStepperWithOptionalStep';
+import VerticalNonLinearStepperCode from '!raw!./VerticalNonLinearStepper';
+
+import HorizontalLinearStepper from './HorizontalLinearStepper';
+import HorizontalLinearStepperCode from '!raw!./HorizontalLinearStepper';
+
+import stepperCode from '!raw!material-ui/lib/Stepper/Stepper';
+import verticalStepCode from '!raw!material-ui/lib/Stepper/VerticalStep';
+import horizontalStepCode from '!raw!material-ui/lib/Stepper/HorizontalStep';
+
+
+const descriptions = {
+ verticalLinearStepper: 'As for the vertical linear stepper, it requires steps be completed in specific order',
+ verticalLinearStepperWithOptionalStep: 'Set the `optional` property to `true` for optional steps.'
+ + 'Pass a custom label view through `stepLabel` property to show'
+ + ' the difference between optional step and normal step.',
+ verticalNonLinearStepper: 'As for the vertical non linear stepper, steps can be completed in any order.',
+ horizontalLinearStepper: 'As for the horizontal linear stepper, it is the same with vertical linear stepper.',
+};
+
+
+const styles = {
+ stepperWrapper: {
+ marginBottom: 50,
+ },
+};
+
+const StepperPage = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+);
+
+export default StepperPage;
diff --git a/docs/src/app/components/pages/components/Stepper/README.md b/docs/src/app/components/pages/components/Stepper/README.md
new file mode 100644
index 00000000000000..24bbae339d0749
--- /dev/null
+++ b/docs/src/app/components/pages/components/Stepper/README.md
@@ -0,0 +1,7 @@
+## Stepper
+A [stepper](https://www.google.com/design/spec/components/steppers.html)
+is an interface for users to show numbered steps or for navigation. It just provides
+views, not handling logic (when the step is active, or when the step is completed, or how to move
+to the next step). We delegate that to the parent component. We just pass `activeStepIndex`
+to show which step is active.
+### Examples
diff --git a/docs/src/app/components/pages/components/Stepper/VerticalLinearStepper.jsx b/docs/src/app/components/pages/components/Stepper/VerticalLinearStepper.jsx
new file mode 100644
index 00000000000000..e89b279526b338
--- /dev/null
+++ b/docs/src/app/components/pages/components/Stepper/VerticalLinearStepper.jsx
@@ -0,0 +1,137 @@
+import React from 'react';
+
+import Stepper from 'material-ui/lib/Stepper/Stepper';
+import Step from 'material-ui/Stepper/VerticalStep';
+
+import Paper from 'material-ui/lib/paper';
+import FontIcon from 'material-ui/lib/font-icon';
+import RaisedButton from 'material-ui/lib/raised-button';
+import FlatButton from 'material-ui/lib/flat-button';
+
+const VerticalLinearStepper = React.createClass({
+ getInitialState() {
+ return {
+ activeStepIndex: -1,
+ lastActiveStepIndex: 0,
+ };
+ },
+
+ selectStep(stepIndex) {
+ const {
+ lastActiveStepIndex,
+ activeStepIndex,
+
+ } = this.state;
+
+ if (stepIndex > lastActiveStepIndex) {
+ return;
+ }
+
+ this.setState({
+ activeStepIndex: stepIndex,
+ lastActiveStepIndex: Math.max(lastActiveStepIndex, activeStepIndex),
+ });
+ },
+
+ updateCompletedSteps(stepIndex) {
+ return stepIndex < this.state.lastActiveStepIndex;
+ },
+
+ continue() {
+ const {
+ activeStepIndex,
+ lastActiveStepIndex,
+ } = this.state;
+
+ this.setState({
+ activeStepIndex: activeStepIndex + 1,
+ lastActiveStepIndex: Math.max(lastActiveStepIndex, activeStepIndex + 1),
+ });
+ },
+
+ createIcon(step) {
+ if (step.props.isCompleted) {
+ return (
+
+ done
+
+ );
+ }
+
+ return {step.props.orderStepLabel};
+ },
+
+ render() {
+ return (
+
+
+ How to find the real "css" of your life
+
+
+ ,
+ ,
+ ]}
+ >
+
+ If you code everyday, you may just know css for web not for your life.
+ So stoping coding first.
+ If you agree, let 's press Continue
+
+
+ ,
+ ,
+ ]}
+ >
+
+ The important thing is getting away your computer. If you follow
+ the step 1, but you should still sit in front of the computer, you
+ can't find destination css for your life.
+ So if you agree, let's press continue
+
+
+
+ ,
+ ,
+ ]}
+ >
+
+ Start your new journey in real life and find your real "css" for your
+ life. Hope you find out soon.
+ Press Finish if you find out.
+
+
+
+
+ );
+ },
+});
+
+export default VerticalLinearStepper;
diff --git a/docs/src/app/components/pages/components/Stepper/VerticalLinearStepperWithOptionalStep.jsx b/docs/src/app/components/pages/components/Stepper/VerticalLinearStepperWithOptionalStep.jsx
new file mode 100644
index 00000000000000..92f43c5f783a52
--- /dev/null
+++ b/docs/src/app/components/pages/components/Stepper/VerticalLinearStepperWithOptionalStep.jsx
@@ -0,0 +1,160 @@
+import React from 'react';
+
+import Stepper from 'material-ui/lib/Stepper/Stepper';
+import Step from 'material-ui/Stepper/VerticalStep';
+
+import Paper from 'material-ui/lib/paper';
+import FontIcon from 'material-ui/lib/font-icon';
+import RaisedButton from 'material-ui/lib/raised-button';
+import FlatButton from 'material-ui/lib/flat-button';
+
+const VerticalLinearStepper = React.createClass({
+ getInitialState() {
+ return {
+ activeStepIndex: -1,
+ lastActiveStepIndex: 0,
+ statusSteps: [],
+ };
+ },
+
+ selectStep(stepIndex, step) {
+ const {
+ lastActiveStepIndex,
+ activeStepIndex,
+
+ } = this.state;
+
+ if (stepIndex > lastActiveStepIndex && lastActiveStepIndex < step.props.previousStepOptionalIndex) {
+ return;
+ }
+
+ this.setState({
+ activeStepIndex: stepIndex,
+ lastActiveStepIndex: Math.max(lastActiveStepIndex, activeStepIndex),
+ });
+ },
+
+ updateCompletedSteps(stepIndex) {
+ return this.state.statusSteps[stepIndex];
+ },
+
+ createIcon(step) {
+ if (step.props.isCompleted) {
+ return (
+
+ done
+
+ );
+ }
+
+ return {step.props.orderStepLabel};
+ },
+
+ continue() {
+ const {
+ activeStepIndex,
+ lastActiveStepIndex,
+ statusSteps,
+ } = this.state;
+
+ statusSteps[activeStepIndex] = true;
+
+ this.setState({
+ activeStepIndex: activeStepIndex + 1,
+ statusSteps: statusSteps,
+ lastActiveStepIndex: Math.max(lastActiveStepIndex, activeStepIndex + 1),
+ });
+ },
+
+ render() {
+ return (
+
+
+ How to keep your 'css' for a long time
+
+
+ ,
+ ,
+ ]}
+ >
+
+ After finding out your 'css', if you really love your 'css' and want
+ to keep 'css', you should spend more time and time on her/him.
+ All your time 24h in a day is not only for coding, but also for your lover.
+ Rememeber it.
+ This step really need to be done first. If not, we can't continue the rest steps
+
+ If you agree with me, let 's continue.
+
+
+
+
+ Let's up-to-date
+ optional
+
+ }
+ stepHeaderStyle={{
+ alignItems: 'center',
+ }}
+ controlButtonsGroup={[
+ ,
+ ,
+ ]}
+ >
+
+ In css world, let's up-to-date your knowledge to know which is no longer used
+ and which is useful
+
+ And it is the same with your 'css' in real world. But sometimes, the old things are better
+ So this step is optional
+
+
+
+ ,
+ ,
+ ]}
+ >
+
+ In css, we can use 'absolute' or 'relative' or something else to describe
+ position of html tag depend on situation.
+
+ And it is the same with your 'css' in your real life. So be careful when talking with
+ her/him. Let's be good programmer =D
+
+
+
+
+ );
+ },
+});
+
+export default VerticalLinearStepper;
diff --git a/docs/src/app/components/pages/components/Stepper/VerticalNonLinearStepper.jsx b/docs/src/app/components/pages/components/Stepper/VerticalNonLinearStepper.jsx
new file mode 100644
index 00000000000000..5739a6705c0865
--- /dev/null
+++ b/docs/src/app/components/pages/components/Stepper/VerticalNonLinearStepper.jsx
@@ -0,0 +1,121 @@
+import React from 'react';
+
+import Stepper from 'material-ui/lib/Stepper/Stepper';
+import Step from 'material-ui/Stepper/VerticalStep';
+
+import Paper from 'material-ui/lib/paper';
+import FontIcon from 'material-ui/lib/font-icon';
+import RaisedButton from 'material-ui/lib/raised-button';
+import FlatButton from 'material-ui/lib/flat-button';
+
+const VerticalNonLinearStepper = React.createClass({
+ getInitialState() {
+ return {
+ activeStepIndex: -1,
+ statusSteps: [],
+ };
+ },
+
+ selectStep(stepIndex) {
+ this.setState({
+ activeStepIndex: stepIndex,
+ });
+ },
+
+ updateCompletedSteps(stepIndex) {
+ return this.state.statusSteps[stepIndex];
+ },
+
+ createIcon(step) {
+ if (step.props.isCompleted) {
+ return (
+
+ done
+
+ );
+ }
+
+ return {step.props.orderStepLabel};
+ },
+
+ continue() {
+ const {
+ activeStepIndex,
+ statusSteps,
+ } = this.state;
+
+ statusSteps[activeStepIndex] = true;
+
+ this.setState({
+ activeStepIndex: activeStepIndex + 1,
+ statusSteps: statusSteps,
+ });
+ },
+
+ render() {
+ return (
+
+
+ How to get a lot of money
+
+
+ ,
+ ,
+ ]}
+ >
+
+ Get a good job and it will bring for you many things.
+
+
+ ,
+ ,
+ ]}
+ >
+
+ Sell your house then you can earn money
+
+
+
+ ,
+ ,
+ ]}
+ >
+
+ Buy a ticket and wait for getting money
+
+
+
+
+ );
+ },
+});
+
+export default VerticalNonLinearStepper;
diff --git a/src/Stepper/HorizontalStep.jsx b/src/Stepper/HorizontalStep.jsx
new file mode 100644
index 00000000000000..f7cea555910c4e
--- /dev/null
+++ b/src/Stepper/HorizontalStep.jsx
@@ -0,0 +1,234 @@
+import React from 'react';
+
+import TouchRipple from '../ripples/touch-ripple';
+
+import Avatar from '../avatar';
+
+import {getMuiTheme} from '../styles';
+
+const HorizontalStep = React.createClass({
+ propTypes: {
+
+ /**
+ * The width of step header, unit is % which passed from Stepper.
+ * @ignore
+ */
+ headerWidth: React.PropTypes.string,
+
+ /**
+ * If true, the step is active.
+ * @ignore
+ */
+ isActive: React.PropTypes.bool,
+
+ /**
+ * If true, the step is completed.
+ * @ignore
+ */
+ isCompleted: React.PropTypes.bool,
+
+ /**
+ * If true, the step is the first step.
+ * @ignore
+ */
+ isFirstStep: React.PropTypes.bool,
+
+ /**
+ * If true, the step is the last step.
+ * @ignore
+ */
+ isLastStep: React.PropTypes.bool,
+
+ /**
+ * If true, the step header is hovered.
+ * @ignore
+ */
+ isStepHeaderHovered: React.PropTypes.bool,
+
+ /**
+ * Callback function will be called when step header is hovered.
+ * @ignore
+ */
+ onStepHeaderHover: React.PropTypes.func,
+
+ /**
+ * Call back function will be called when step header is touched.
+ * @ignore
+ */
+ onStepHeaderTouch: React.PropTypes.func,
+
+ /**
+ * Override inline style of step header wrapper.
+ */
+ stepHeaderWrapperStyle: React.PropTypes.object,
+
+ /**
+ * The index of step in array of Steps.
+ * @ignore
+ */
+ stepIndex: React.PropTypes.number,
+
+ /**
+ * The label of step which be shown in step header.
+ */
+ stepLabel: React.PropTypes.node,
+ },
+
+ contextTypes: {
+ muiTheme: React.PropTypes.object,
+ createIcon: React.PropTypes.func,
+ updateAvatarBackgroundColor: React.PropTypes.func,
+ },
+
+ //for passing default theme context to children
+ childContextTypes: {
+ muiTheme: React.PropTypes.object,
+ },
+
+ getInitialState() {
+ return {
+ muiTheme: this.context.muiTheme || getMuiTheme(),
+ };
+ },
+
+ getChildContext() {
+ return {
+ muiTheme: this.state.muiTheme,
+ };
+ },
+
+
+ getStyles() {
+ const {
+ headerWidth,
+ isActive,
+ isCompleted,
+ isStepHeaderHovered,
+ stepHeaderWrapperStyle,
+ } = this.props;
+
+ const theme = this.state.muiTheme.stepper;
+
+ const customAvatarBackgroundColor = this.context.updateAvatarBackgroundColor(this);
+ const avatarBackgroundColor = customAvatarBackgroundColor ||
+ ((isActive || isCompleted)
+ ? theme.activeAvatarColor
+ : isStepHeaderHovered
+ ? theme.hoveredAvatarColor
+ : theme.inactiveAvatarColor);
+
+ const stepHeaderWrapper = Object.assign({
+ width: headerWidth,
+ display: 'table-cell',
+ position: 'relative',
+ padding: 24,
+ color: theme.inactiveTextColor,
+ cursor: 'pointer',
+ },
+ stepHeaderWrapperStyle,
+ isStepHeaderHovered && !isActive && {
+ backgroundColor: theme.hoveredHeaderColor,
+ color: theme.hoveredTextColor,
+
+ }, (isActive || (isActive && isStepHeaderHovered) || isCompleted) && {
+ color: theme.activeTextColor,
+
+ }
+ );
+
+ const avatar = {
+ backgroundColor: avatarBackgroundColor,
+ color: 'white',
+ margin: '0 auto',
+ // display: 'block',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ };
+
+ const stepLabel = {
+ marginTop: 8,
+ fontSize: 14,
+ fontWeight: 'normal',
+ textAlign: 'center',
+ };
+
+ const connectorLine = {
+ top: 36,
+ height: 1,
+ borderTop: '1px solid #BDBDBD',
+ position: 'absolute',
+ };
+
+ const connectorLineLeft = Object.assign({
+ left: 0,
+ right: '50%',
+ marginRight: 16,
+ }, connectorLine);
+
+ const connectorLineRight = Object.assign({
+ right: 0,
+ left: '50%',
+ marginLeft: 16,
+ }, connectorLine);
+
+ const stepLabelWrapper = {
+ margin: '0 auto',
+ textAlign: 'center',
+ };
+
+ const styles = {
+ stepHeaderWrapper: stepHeaderWrapper,
+ avatar: avatar,
+ stepLabel: stepLabel,
+ connectorLineLeft: connectorLineLeft,
+ connectorLineRight: connectorLineRight,
+ stepLabelWrapper: stepLabelWrapper,
+ };
+
+ return styles;
+ },
+
+ handleStepHeaderTouch() {
+ this.props.onStepHeaderTouch(this.props.stepIndex, this);
+ },
+
+
+ handleStepHeaderMouseHover() {
+ this.props.onStepHeaderHover(this.props.stepIndex);
+ },
+
+ handleStepHeaderMouseLeave() {
+ this.props.onStepHeaderHover(-1);
+ },
+
+ render() {
+ const styles = this.getStyles();
+ const {
+ isFirstStep,
+ isLastStep,
+ stepLabel,
+ } = this.props;
+
+ const icon = this.context.createIcon(this);
+ const avatarView = ;
+
+ return (
+
+
+ {avatarView}
+ {stepLabel}
+ {!isFirstStep && }
+ {!isLastStep && }
+
+
+ );
+ },
+});
+
+export default HorizontalStep;
diff --git a/src/Stepper/Stepper.jsx b/src/Stepper/Stepper.jsx
new file mode 100644
index 00000000000000..3d2407a3e5dd76
--- /dev/null
+++ b/src/Stepper/Stepper.jsx
@@ -0,0 +1,319 @@
+import React, {PropTypes} from 'react';
+
+import {getMuiTheme} from '../styles';
+import Paper from '../paper';
+
+const Stepper = React.createClass({
+ propTypes: {
+
+ /**
+ * The current active step index which passed by parent component.
+ */
+ activeStepIndex: PropTypes.number,
+
+ /**
+ * Children should be Step type.
+ */
+ children: PropTypes.node,
+
+ /*
+ * Override inline-style of the content container.
+ */
+ containerStyle: PropTypes.object,
+
+ /**
+ * Function used to create suitable icon for step base on state of the step.
+ *
+ * @param {node} Step component which is being updated .
+ * @returns {node} - which will be shown in the left avatar.
+ */
+ createIcon: PropTypes.func,
+
+ /**
+ * If true, it will be horizontal stepper.
+ */
+ horizontal: PropTypes.bool,
+
+ /**
+ * Callback function that is fired when the header of step is touched.
+ *
+ * @param {number} stepIndex - The index of step is being touched.
+ * @param {node} Step component which is being touched
+ */
+ onStepHeaderTouch: PropTypes.func,
+
+ /**
+ * Overrie inline-style of the step header wrapper.
+ */
+ stepHeadersWrapperStyle: PropTypes.object,
+
+ /**
+ * Override the inline-styles of the root element.
+ */
+ style: PropTypes.object,
+
+ /**
+ * Callback function that is fired when re-render to update the background of left avatar.
+ If not passed, it will use default theme
+ *
+ * @param {node} Step component which is being updated
+ * @returns {string} the background color of avatar
+ */
+ updateAvatarBackgroundColor: PropTypes.func,
+
+ /**
+ * Callback function that is fired when re-render to update complete status of Step.
+ *
+ * @param {number} stepIndex - The step is being updated.
+ * @param {node} Step component which is being updated
+ * @returns {boolean} `true` if the step is completed.
+ */
+ updateCompletedStatusOfStep: PropTypes.func,
+
+
+ },
+
+ contextTypes: {
+ muiTheme: PropTypes.object,
+ },
+
+ childContextTypes: {
+ muiTheme: PropTypes.object,
+ createIcon: PropTypes.func,
+ updateAvatarBackgroundColor: PropTypes.func,
+ },
+
+ getDefaultProps() {
+ return {
+ activeStepIndex: -1,
+ onStepHeaderTouch: () => {},
+ updateAvatarBackgroundColor: () => null,
+ style: {},
+ horizontal: false,
+ };
+ },
+
+ getInitialState() {
+ return {
+ hoveredHeaderStepIndex: -1,
+ muiTheme: this.context.muiTheme || getMuiTheme(),
+ itemWidth: 0,
+ };
+ },
+
+
+ getChildContext() {
+ return {
+ muiTheme: this.state.muiTheme,
+ createIcon: this.props.createIcon,
+ updateAvatarBackgroundColor: this.props.updateAvatarBackgroundColor,
+ };
+ },
+
+ componentWillReceiveProps(nextProps) {
+ if (!this.props.horizontal) {
+ return;
+ }
+
+ const childrenWrapperNode = this.refs.childrenWrapper;
+ const containerWrapperNode = this.refs.containerWrapper;
+ const controlButtonsGroupNode = this.refs.controlButtonsGroup;
+
+ if (containerWrapperNode.style.height === '0px'
+ && nextProps.activeStepIndex > -1) {
+ containerWrapperNode.style.height = `${(childrenWrapperNode.offsetHeight +
+ controlButtonsGroupNode.offsetHeight + 40)}px`;
+ childrenWrapperNode.style.transition = 'none';
+ } else if (nextProps.activeStepIndex > this.getTotalSteps() - 1) {
+ containerWrapperNode.style.height = '0px';
+ } else {
+ childrenWrapperNode.style.transition = 'all 1s';
+ }
+ },
+
+ getTotalSteps() {
+ return React.Children.count(this.props.children);
+ },
+
+ getStylesForHorizontalStepper() {
+ const {
+ stepHeadersWrapperStyle,
+ containerStyle,
+ style,
+ activeStepIndex,
+ } = this.props;
+
+ const itemWidth = this.state.itemWidth;
+ const translateX = -activeStepIndex * itemWidth;
+
+ const childrenWrapper = {
+ transform: `translate3d(${translateX}px, 0px, 0px)`,
+ transition: 'all 1s',
+ };
+
+ const stepHeadersWrapper = Object.assign({
+ display: 'flex',
+ width: '100%',
+ margin: '0 auto',
+ }, stepHeadersWrapperStyle);
+
+ const wrapper = Object.assign({
+ overflow: 'hidden',
+ },
+ activeStepIndex > -1 && {
+ transition: 'all 0.5s',
+ },
+ style
+ );
+
+
+ const container = Object.assign({
+ transition: 'all 0.5s',
+ height: 0,
+ }, containerStyle);
+
+ return {
+ wrapper: wrapper,
+ container: container,
+ stepHeadersWrapper: stepHeadersWrapper,
+ childrenWrapper: childrenWrapper,
+ };
+ },
+
+ _handleHeaderStepHover(stepIndex) {
+ this.setState({
+ hoveredHeaderStepIndex: stepIndex,
+ });
+ },
+
+ findFurthestOptionalStep(index) {
+ const {
+ children,
+ } = this.props;
+
+ while (index > 0 && children[index - 1].props.optional) {
+ index--;
+ }
+ return index;
+ },
+
+ renderHorizontalStepper() {
+ const {
+ children,
+ onStepHeaderTouch,
+ activeStepIndex,
+ updateCompletedStatusOfStep,
+ } = this.props;
+
+ const {
+ hoveredHeaderStepIndex,
+ } = this.state;
+
+ const setOfChildrens = [];
+ const setOfControlButtonsGroup = [];
+
+ const steps = React.Children.map(children, (step, index) => {
+ setOfChildrens.push(step.props.children);
+ setOfControlButtonsGroup.push(step.props.controlButtonsGroup);
+
+ return React.cloneElement(step, {
+ headerWidth: `${100 / this.getTotalSteps()}%`,
+ key: index,
+ stepIndex: index,
+ isActive: activeStepIndex === index,
+ isStepHeaderHovered: hoveredHeaderStepIndex === index,
+ onStepHeaderTouch: onStepHeaderTouch,
+ onStepHeaderHover: this._handleHeaderStepHover,
+ isLastStep: index === (this.getTotalSteps() - 1),
+ isFirstStep: index === 0,
+ isCompleted: updateCompletedStatusOfStep(index, step),
+ previousStepOptionalIndex: this.findFurthestOptionalStep(index),
+ });
+ });
+
+ const itemWidth = this.state.itemWidth;
+ const styles = this.getStylesForHorizontalStepper();
+
+ return (
+ {
+ if (input !== null && !this.state.itemWidth) {
+ this.setState({
+ itemWidth: input.offsetWidth,
+ });
+ }
+ }
+ }
+ >
+
+ {steps}
+
+
+
+
+
+
+ {setOfChildrens.map((children, index) =>
+
+ {children}
+
)}
+
+
+
+ {setOfControlButtonsGroup[activeStepIndex]}
+
+
+
+ );
+ },
+
+ renderVerticalStepper() {
+ const {
+ style,
+ children,
+ onStepHeaderTouch,
+ activeStepIndex,
+ updateCompletedStatusOfStep,
+ } = this.props;
+
+ const {
+ hoveredHeaderStepIndex,
+ } = this.state;
+
+ const steps = React.Children.map(children, (step, index) => {
+ return React.cloneElement(step, {
+ key: index,
+ stepIndex: index,
+ isActive: activeStepIndex === index,
+ isStepHeaderHovered: hoveredHeaderStepIndex === index,
+ onStepHeaderTouch: onStepHeaderTouch,
+ onStepHeaderHover: this._handleHeaderStepHover,
+ isLastStep: index === (this.getTotalSteps() - 1),
+ isCompleted: updateCompletedStatusOfStep(index, step),
+ previousStepOptionalIndex: this.findFurthestOptionalStep(index),
+ });
+ });
+
+ return (
+
+ {steps}
+
+ );
+ },
+
+ render() {
+ const {
+ horizontal,
+ } = this.props;
+
+ if (horizontal) {
+ return this.renderHorizontalStepper();
+ }
+
+ return this.renderVerticalStepper();
+ },
+
+
+});
+
+export default Stepper;
diff --git a/src/Stepper/VerticalStep.jsx b/src/Stepper/VerticalStep.jsx
new file mode 100644
index 00000000000000..71f7a0b728a38f
--- /dev/null
+++ b/src/Stepper/VerticalStep.jsx
@@ -0,0 +1,352 @@
+import React, {PropTypes} from 'react';
+
+import TouchRipple from '../ripples/touch-ripple';
+import Avatar from '../avatar';
+
+import {getMuiTheme} from '../styles';
+
+const Step = React.createClass({
+ propTypes: {
+ children: PropTypes.node,
+
+ /**
+ * Override the inline-styles of div which contains all the children include control button groups.
+ */
+ childrenWrapperStyle: PropTypes.object,
+
+ /**
+ * Override the inline-styles of connector line.
+ */
+ connectorLineStyle: PropTypes.object,
+
+ /**
+ * An array of node for handling moving or canceling steps.
+ */
+ controlButtonsGroup: PropTypes.arrayOf(PropTypes.node),
+
+ /**
+ * Override the inline-styles of div wrapper which contains control buttons group.
+ */
+ controlButtonsGroupWrapperStyle: PropTypes.object,
+
+ /**
+ * If true, the step is active.
+ * @ignore
+ */
+ isActive: PropTypes.bool,
+
+ /**
+ * If true, the step is completed.
+ * @ignore
+ */
+ isCompleted: PropTypes.bool,
+
+ /**
+ * If true, the step is the last one.
+ * @ignore
+ */
+ isLastStep: PropTypes.bool,
+
+ /**
+ * If true, the header of step is hovered.
+ * @ignore
+ */
+ isStepHeaderHovered: PropTypes.bool,
+
+ /**
+ * Callback function that is fired when the header of step is hovered.
+ * @ignore
+ */
+ onStepHeaderHover: PropTypes.func,
+
+ /**
+ * Callback function that is fired when the header of step is touched.
+ * @ignore
+ */
+ onStepHeaderTouch: PropTypes.func,
+
+ /**
+ * The index of the furthest optional step.
+ * @ignore
+ */
+ previousStepOptionalIndex: PropTypes.number,
+
+ /**
+ * Override the inline-styles of step container which contains connector line and children.
+ */
+ stepContainerStyle: PropTypes.object,
+
+ /**
+ * Override the inline-styles of step header view (not include left avatar).
+ */
+ stepHeaderStyle: PropTypes.object,
+
+ /**
+ * Override the inline-styles of step header wrapper (include left avatar).
+ */
+ stepHeaderWrapperStyle: PropTypes.object,
+
+ /**
+ * The index of step in array of Steps.
+ * @ignore
+ */
+ stepIndex: PropTypes.number,
+
+ /**
+ * Customize the step label view.
+ */
+ stepLabel: PropTypes.node,
+ },
+
+ contextTypes: {
+ muiTheme: PropTypes.object,
+ createIcon: PropTypes.func,
+ updateAvatarBackgroundColor: PropTypes.func,
+ },
+
+ childContextTypes: {
+ muiTheme: React.PropTypes.object,
+ },
+
+
+ getInitialState() {
+ return {
+ muiTheme: this.context.muiTheme || getMuiTheme(),
+ };
+ },
+
+ getChildContext() {
+ return {
+ muiTheme: this.state.muiTheme,
+ };
+ },
+
+ componentDidMount() {
+ const {
+ isActive,
+ } = this.props;
+
+ if (isActive) {
+ const childrenWrapperNode = this.refs.childrenWrapper;
+ childrenWrapperNode.style.opacity = 1;
+
+ const containerWrapper = this.refs.containerWrapper;
+ containerWrapper.style.height = `${childrenWrapperNode.children[0].offsetHeight}px`;
+
+ setTimeout(() => {
+ containerWrapper.style.height = 'auto';
+ childrenWrapperNode.style.height = 'auto';
+ }, 300);
+ }
+ },
+
+ componentWillReceiveProps(nextProps) {
+ const {
+ isActive,
+ } = this.props;
+
+ if (!isActive && nextProps.isActive) {
+ const childrenWrapperNode = this.refs.childrenWrapper;
+ childrenWrapperNode.style.opacity = 1;
+
+ const containerWrapper = this.refs.containerWrapper;
+ containerWrapper.style.height = `${childrenWrapperNode.children[0].offsetHeight}px`;
+
+ setTimeout(() => {
+ containerWrapper.style.height = 'auto';
+ childrenWrapperNode.style.height = 'auto';
+ }, 300);
+ }
+
+ if (isActive && !nextProps.isActive) {
+ const childrenWrapperNode = this.refs.childrenWrapper;
+ childrenWrapperNode.style.opacity = '0';
+ childrenWrapperNode.style.height = '100%';
+
+ const containerWrapper = this.refs.containerWrapper;
+ containerWrapper.style.height = '32px';
+ }
+ },
+
+
+ handleStepHeaderTouch() {
+ this.props.onStepHeaderTouch(this.props.stepIndex, this);
+ },
+
+ handleStepHeaderMouseHover() {
+ this.props.onStepHeaderHover(this.props.stepIndex);
+ },
+
+ handleStepHeaderMouseLeave() {
+ this.props.onStepHeaderHover(-1);
+ },
+
+
+ getStyles() {
+ const {
+ isActive,
+ isCompleted,
+ isStepHeaderHovered,
+
+ stepHeaderStyle,
+ stepHeaderWrapperStyle,
+ connectorLineStyle,
+ stepContainerStyle,
+ controlButtonsGroupWrapperStyle,
+ childrenWrapperStyle,
+ } = this.props;
+
+ const theme = this.state.muiTheme.stepper;
+
+ const customAvatarBackgroundColor = this.context.updateAvatarBackgroundColor(this);
+
+ const avatarBackgroundColor = customAvatarBackgroundColor ||
+ ((isActive || isCompleted)
+ ? theme.activeAvatarColor
+ : isStepHeaderHovered
+ ? theme.hoveredAvatarColor
+ : theme.inactiveAvatarColor);
+
+ const stepHeaderWrapper = Object.assign({
+ cursor: 'pointer',
+ color: theme.inactiveTextColor,
+ paddingLeft: 24,
+ paddingTop: 24,
+ paddingBottom: 24,
+ marginTop: -32,
+ position: 'relative',
+
+ },
+
+ stepHeaderWrapperStyle,
+
+ isStepHeaderHovered && !isActive && {
+ backgroundColor: theme.hoveredHeaderColor,
+ color: theme.hoveredTextColor,
+
+ }, (isActive || (isActive && isStepHeaderHovered) || isCompleted) && {
+ color: theme.activeTextColor,
+
+ }, this.props.stepIndex === 0 && {
+ marginTop: 0,
+ });
+
+ const stepContainer = Object.assign({
+ paddingLeft: 36,
+ position: 'relative',
+ height: 32,
+ transition: 'height 0.2s',
+
+ },
+
+ stepContainerStyle,
+
+ isActive && {
+ paddingBottom: 36 + 24,
+ marginBottom: 8,
+ marginTop: -8,
+ });
+
+ const connectorLine = Object.assign({
+ borderLeft: '1px solid',
+ borderLeftColor: theme.connectorLineColor,
+ height: '100%',
+ position: 'absolute',
+ marginTop: -16,
+
+ },
+
+ connectorLineStyle,
+
+ isActive && {
+ marginTop: -8,
+ });
+
+ const controlButtonsGroupWrapper = Object.assign({
+ marginTop: 16,
+ }, controlButtonsGroupWrapperStyle);
+
+ const childrenWrapper = Object.assign({
+ paddingLeft: 24,
+ transition: 'height 0.05s',
+ opacity: 0,
+ overflow: 'hidden',
+ }, childrenWrapperStyle);
+
+ const stepHeader = Object.assign({
+ display: 'flex',
+ flexDirection: 'row',
+ alignItems: 'center',
+ }, stepHeaderStyle);
+
+ return {
+ avatar: {
+ backgroundColor: avatarBackgroundColor,
+ fontSize: 12,
+ marginRight: 12,
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+
+ stepHeaderWrapper: stepHeaderWrapper,
+ stepContainer: stepContainer,
+ connectorLine: connectorLine,
+ controlButtonsGroupWrapper: controlButtonsGroupWrapper,
+ childrenWrapper: childrenWrapper,
+ stepHeader: stepHeader,
+ };
+ },
+
+ render() {
+ const {
+ children,
+ stepLabel,
+ controlButtonsGroup,
+ isLastStep,
+ } = this.props;
+
+
+ const styles = this.getStyles();
+
+ const icon = this.context.createIcon(this);
+
+ const avatarView = ;
+
+ return (
+
+
+
+
+ {avatarView}
+ {stepLabel}
+
+
+
+
+ {!isLastStep &&
}
+ {
+
+
+ {children}
+
+
+ {controlButtonsGroup}
+
+
+
+ }
+
+
+ );
+ },
+
+
+});
+
+export default Step;
diff --git a/src/Stepper/index.jsx b/src/Stepper/index.jsx
new file mode 100644
index 00000000000000..deca6781701807
--- /dev/null
+++ b/src/Stepper/index.jsx
@@ -0,0 +1,13 @@
+import VerticalStep from './VerticalStep';
+import HorizontalStep from './HorizontalStep';
+import Stepper from './Stepper';
+
+export {VerticalStep};
+export {HorizontalStep};
+export {Stepper};
+
+export default {
+ VerticalStep,
+ HorizontalStep,
+ Stepper,
+};
diff --git a/src/index.js b/src/index.js
index 059c56d63724aa..916ef4d2c83b24 100644
--- a/src/index.js
+++ b/src/index.js
@@ -25,6 +25,7 @@ import FloatingActionButton from './floating-action-button';
import FontIcon from './font-icon';
import GridList from './grid-list/grid-list';
import GridTile from './grid-list/grid-tile';
+import HorizontalStep from './Stepper/HorizontalStep';
import IconButton from './icon-button';
import IconMenu from './menus/icon-menu';
import LeftNav from './left-nav';
@@ -47,6 +48,7 @@ import SelectableContainerEnhance from './hoc/selectable-enhance';
import Slider from './slider';
import Subheader from './Subheader';
import SvgIcon from './svg-icon';
+import Stepper from './Stepper/Stepper';
import Styles from './styles';
import Snackbar from './snackbar';
import Tab from './tabs/tab';
@@ -67,6 +69,7 @@ import ToolbarSeparator from './toolbar/toolbar-separator';
import ToolbarTitle from './toolbar/toolbar-title';
import Tooltip from './tooltip';
import Utils from './utils';
+import VerticalStep from './Stepper/VerticalStep';
export {AppBar};
export {AppCanvas};
@@ -95,6 +98,7 @@ export {FloatingActionButton};
export {FontIcon};
export {GridList};
export {GridTile};
+export {HorizontalStep};
export {IconButton};
export {IconMenu};
export {LeftNav};
@@ -117,6 +121,7 @@ export {SelectableContainerEnhance};
export {Slider};
export {Subheader};
export {SvgIcon};
+export {Stepper};
export {Styles};
export {Snackbar};
export {Tab};
@@ -137,6 +142,7 @@ export {ToolbarSeparator};
export {ToolbarTitle};
export {Tooltip};
export {Utils};
+export {VerticalStep};
import NavigationMenu from './svg-icons/navigation/menu';
import NavigationChevronLeft from './svg-icons/navigation/chevron-left';
@@ -176,6 +182,7 @@ export default {
FontIcon,
GridList,
GridTile,
+ HorizontalStep,
IconButton,
IconMenu,
LeftNav,
@@ -197,6 +204,7 @@ export default {
SelectableContainerEnhance,
Slider,
SvgIcon,
+ Stepper,
Styles,
Snackbar,
Tab,
@@ -217,4 +225,5 @@ export default {
ToolbarTitle,
Tooltip,
Utils,
+ VerticalStep,
};
diff --git a/src/styles/getMuiTheme.js b/src/styles/getMuiTheme.js
index ea0fb15f99d142..c29357d5b226c7 100644
--- a/src/styles/getMuiTheme.js
+++ b/src/styles/getMuiTheme.js
@@ -7,8 +7,8 @@ import compose from 'lodash.flowright';
import Typography from '../styles/typography';
import {
red500,
-grey400, grey600, grey700,
-transparent, lightWhite, white, darkWhite, lightBlack,
+grey400, grey500, grey600, grey700,
+transparent, lightWhite, white, darkWhite, lightBlack, black,
} from './colors';
/**
@@ -222,6 +222,20 @@ export default function getMuiTheme(muiTheme, ...more) {
color: ColorManipulator.fade(palette.textColor, 0.54),
fontWeight: Typography.fontWeightMedium,
},
+ stepper: {
+ activeAvatarColor: palette.primary1Color,
+ hoveredAvatarColor: grey700,
+ inactiveAvatarColor: grey500,
+
+ inactiveTextColor: ColorManipulator.fade(black, 0.26),
+ activeTextColor: ColorManipulator.fade(black, 0.87),
+ hoveredTextColor: grey600,
+
+ hoveredHeaderColor: ColorManipulator.fade(black, 0.06),
+
+ connectorLineColor: grey400,
+ avatarSize: 24,
+ },
table: {
backgroundColor: palette.canvasColor,
},