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