From 71116709e7675068163efa094a765bfee5e85b04 Mon Sep 17 00:00:00 2001 From: Nathan Marks Date: Sun, 20 Mar 2016 17:55:12 -0400 Subject: [PATCH] [Stepper] Refactor Stepper to address #3725 Refactor Stepper to fix issues in the existing implementation that are detailed in #3725. Documentation is also reworked for the component and a new transition group added to the internal folder. --- docs/src/app/components/CodeExample/index.js | 10 +- .../pages/components/Stepper/Advanced.md | 8 + .../pages/components/Stepper/CustomIcon.js | 74 ++++ .../Stepper/GranularControlStepper.js | 141 ++++++++ .../Stepper/HorizontalLinearStepper.js | 203 +++++------ .../Stepper/HorizontalNonLinearStepper.js | 96 ++++++ .../Stepper/HorizontalTransition.js | 151 ++++++++ .../pages/components/Stepper/Page.js | 94 +++-- .../pages/components/Stepper/README.md | 13 +- .../Stepper/VerticalLinearStepper.js | 240 ++++++------- .../VerticalLinearStepperWithOptionalStep.js | 190 ----------- .../Stepper/VerticalNonLinearStepper.js | 219 +++++------- src/Stepper/HorizontalStep.js | 211 ------------ src/Stepper/Step.js | 95 ++++++ src/Stepper/Step.spec.js | 80 +++++ src/Stepper/StepButton.js | 147 ++++++++ src/Stepper/StepButton.spec.js | 96 ++++++ src/Stepper/StepConnector.js | 56 +++ src/Stepper/StepConnector.spec.js | 39 +++ src/Stepper/StepContent.js | 75 ++++ src/Stepper/StepContent.spec.js | 60 ++++ src/Stepper/StepLabel.js | 152 +++++++++ src/Stepper/StepLabel.spec.js | 204 +++++++++++ src/Stepper/Stepper.js | 305 ++++------------- src/Stepper/Stepper.spec.js | 92 +++++ src/Stepper/VerticalStep.js | 323 ------------------ src/Stepper/index.js | 8 +- src/SvgIcon/SvgIcon.js | 2 + src/index.js | 8 +- src/internal/ExpandTransition.js | 72 ++++ src/internal/ExpandTransitionChild.js | 90 +++++ src/styles/getMuiTheme.js | 22 +- 32 files changed, 2160 insertions(+), 1416 deletions(-) create mode 100644 docs/src/app/components/pages/components/Stepper/Advanced.md create mode 100644 docs/src/app/components/pages/components/Stepper/CustomIcon.js create mode 100644 docs/src/app/components/pages/components/Stepper/GranularControlStepper.js create mode 100644 docs/src/app/components/pages/components/Stepper/HorizontalNonLinearStepper.js create mode 100644 docs/src/app/components/pages/components/Stepper/HorizontalTransition.js delete mode 100644 docs/src/app/components/pages/components/Stepper/VerticalLinearStepperWithOptionalStep.js delete mode 100644 src/Stepper/HorizontalStep.js create mode 100644 src/Stepper/Step.js create mode 100644 src/Stepper/Step.spec.js create mode 100644 src/Stepper/StepButton.js create mode 100644 src/Stepper/StepButton.spec.js create mode 100644 src/Stepper/StepConnector.js create mode 100644 src/Stepper/StepConnector.spec.js create mode 100644 src/Stepper/StepContent.js create mode 100644 src/Stepper/StepContent.spec.js create mode 100644 src/Stepper/StepLabel.js create mode 100644 src/Stepper/StepLabel.spec.js create mode 100644 src/Stepper/Stepper.spec.js delete mode 100644 src/Stepper/VerticalStep.js create mode 100644 src/internal/ExpandTransition.js create mode 100644 src/internal/ExpandTransitionChild.js diff --git a/docs/src/app/components/CodeExample/index.js b/docs/src/app/components/CodeExample/index.js index 277ec489b2024f..73760b0f778b67 100644 --- a/docs/src/app/components/CodeExample/index.js +++ b/docs/src/app/components/CodeExample/index.js @@ -1,4 +1,5 @@ import React from 'react'; +import {parse} from 'react-docgen'; import CodeBlock from './CodeBlock'; import ClearFix from 'material-ui/internal/ClearFix'; import Paper from 'material-ui/Paper'; @@ -40,9 +41,16 @@ class CodeExample extends React.Component { }, }; + const docs = parse(code); + return ( - {code} + + {code} + {children} ); diff --git a/docs/src/app/components/pages/components/Stepper/Advanced.md b/docs/src/app/components/pages/components/Stepper/Advanced.md new file mode 100644 index 00000000000000..a27131f0328972 --- /dev/null +++ b/docs/src/app/components/pages/components/Stepper/Advanced.md @@ -0,0 +1,8 @@ +## Advanced Usage + +The `` can also be controlled by interfacing directly with the `` components placed inside ``. These individual props are also compatible with the `activeStep` prop, and will take precedence if found on the component. + +You can also place completely custom components inside `` if required and they will be passed the same props as the other `` children. + +These features allows for all sorts of usage scenarios -- the world is your oyster. + diff --git a/docs/src/app/components/pages/components/Stepper/CustomIcon.js b/docs/src/app/components/pages/components/Stepper/CustomIcon.js new file mode 100644 index 00000000000000..a5969dd1014f80 --- /dev/null +++ b/docs/src/app/components/pages/components/Stepper/CustomIcon.js @@ -0,0 +1,74 @@ +import React from 'react'; +import { + Step, + Stepper, + StepLabel, +} from 'material-ui/Stepper'; +import WarningIcon from 'material-ui/svg-icons/alert/warning'; +import {red500} from 'material-ui/styles/colors'; + +/** + * Custom icons can be used to create different visual states. + */ +class CustomIcon extends React.Component { + + state = { + stepIndex: 0, + }; + + handleNext = () => { + const {stepIndex} = this.state; + if (stepIndex < 2) { + this.setState({stepIndex: stepIndex + 1}); + } + }; + + handlePrev = () => { + const {stepIndex} = this.state; + if (stepIndex > 0) { + this.setState({stepIndex: stepIndex - 1}); + } + }; + + getStepContent(stepIndex) { + switch (stepIndex) { + case 0: + return 'Select campaign settings...'; + case 1: + return 'What is an ad group anyways?'; + case 2: + return 'This is the bit I really care about!'; + default: + return 'You\'re a long way from home sonny jim!'; + } + } + + render() { + return ( +
+ + + + Select campaign settings + + + + } + style={{color: red500}} + > + Create an ad group + + + + + Create an ad + + + +
+ ); + } +} + +export default CustomIcon; diff --git a/docs/src/app/components/pages/components/Stepper/GranularControlStepper.js b/docs/src/app/components/pages/components/Stepper/GranularControlStepper.js new file mode 100644 index 00000000000000..5fbaea4f158d22 --- /dev/null +++ b/docs/src/app/components/pages/components/Stepper/GranularControlStepper.js @@ -0,0 +1,141 @@ +import React from 'react'; +import { + Step, + Stepper, + StepButton, +} from 'material-ui/Stepper'; +import RaisedButton from 'material-ui/RaisedButton'; +import FlatButton from 'material-ui/FlatButton'; + +const getStyles = () => { + return { + root: { + width: '100%', + maxWidth: 700, + margin: 'auto', + }, + content: { + margin: '0 16px', + }, + actions: { + marginTop: 12, + }, + backButton: { + marginRight: 12, + }, + }; +}; + +/** + * This is similiar to the horizontal non-linear example, except the + * `` components are being controlled manually via individual props. + * + * An enhancement made possible by this functionality (shown below), + * is to permanently mark steps as complete once the user has satisfied the + * application's required conditions (in this case, once it has visited the step). + * + */ +class GranularControlStepper extends React.Component { + + state = { + stepIndex: null, + visited: [], + }; + + componentWillMount() { + const {stepIndex, visited} = this.state; + this.setState({visited: visited.concat(stepIndex)}); + } + + componentWillUpdate(nextProps, nextState) { + const {stepIndex, visited} = nextState; + if (visited.indexOf(stepIndex) === -1) { + this.setState({visited: visited.concat(stepIndex)}); + } + } + + handleNext = () => { + const {stepIndex} = this.state; + if (stepIndex < 2) { + this.setState({stepIndex: stepIndex + 1}); + } + }; + + handlePrev = () => { + const {stepIndex} = this.state; + if (stepIndex > 0) { + this.setState({stepIndex: stepIndex - 1}); + } + }; + + getStepContent(stepIndex) { + switch (stepIndex) { + case 0: + return 'Select campaign settings...'; + case 1: + return 'What is an ad group anyways?'; + case 2: + return 'This is the bit I really care about!'; + default: + return 'Click a step to get started.'; + } + } + + render() { + const {stepIndex, visited} = this.state; + const styles = getStyles(); + + return ( +
+

+ { + event.preventDefault(); + this.setState({stepIndex: null, visited: []}); + }} + > + Click here + to reset the example. +

+ + + this.setState({stepIndex: 0})}> + Select campaign settings + + + + this.setState({stepIndex: 1})}> + Create an ad group + + + + this.setState({stepIndex: 2})}> + Create an ad + + + +
+

{this.getStepContent(stepIndex)}

+ {stepIndex !== null && ( +
+ + +
+ )} +
+
+ ); + } +} + +export default GranularControlStepper; diff --git a/docs/src/app/components/pages/components/Stepper/HorizontalLinearStepper.js b/docs/src/app/components/pages/components/Stepper/HorizontalLinearStepper.js index 6ac7366bdf6661..ebbee89bec1050 100644 --- a/docs/src/app/components/pages/components/Stepper/HorizontalLinearStepper.js +++ b/docs/src/app/components/pages/components/Stepper/HorizontalLinearStepper.js @@ -1,138 +1,105 @@ import React from 'react'; -import Stepper from 'material-ui/Stepper'; -import Step from 'material-ui/Stepper/HorizontalStep'; -import Paper from 'material-ui/Paper'; -import FontIcon from 'material-ui/FontIcon'; +import { + Step, + Stepper, + StepLabel, +} from 'material-ui/Stepper'; import RaisedButton from 'material-ui/RaisedButton'; import FlatButton from 'material-ui/FlatButton'; -const HorizontalStepper = React.createClass({ - getInitialState() { - return { - activeStep: -1, - lastActiveStep: 0, - }; - }, +/** + * Horizontal steppers are ideal when the contents of one step depend on an earlier step. + * Avoid using long step names in horizontal steppers. + * + * Linear steppers require users to complete one step in order to move on to the next. + */ +class HorizontalLinearStepper extends React.Component { - handleStepHeaderTouch(currentStep) { - const { - lastActiveStep, - activeStep, - - } = this.state; - - if (currentStep > lastActiveStep) { - return; - } + state = { + finished: false, + stepIndex: 0, + }; + handleNext = () => { + const {stepIndex} = this.state; this.setState({ - activeStep: currentStep, - lastActiveStep: Math.max(lastActiveStep, activeStep), + stepIndex: stepIndex + 1, + finished: stepIndex >= 2, }); - }, + }; - updateCompletedSteps(currentStep) { - return currentStep < this.state.lastActiveStep; - }, - - createIcon(step) { - if (step.props.isCompleted) { - return ( - - done - - ); + handlePrev = () => { + const {stepIndex} = this.state; + if (stepIndex > 0) { + this.setState({stepIndex: stepIndex - 1}); } + }; - return {step.props.orderStepLabel}; - }, - - handleTouchTap() { - const { - activeStep, - lastActiveStep, - } = this.state; - - this.setState({ - activeStep: activeStep + 1, - lastActiveStep: Math.max(lastActiveStep, activeStep + 1), - }); - }, + getStepContent(stepIndex) { + switch (stepIndex) { + case 0: + return 'Select campaign settings...'; + case 1: + return 'What is an ad group anyways?'; + case 2: + return 'This is the bit I really care about!'; + default: + return 'You\'re a long way from home sonny jim!'; + } + } render() { + const {finished, stepIndex} = this.state; + const contentStyle = {margin: '0 16px'}; + return ( - -
- Material-UI User Group Registration -
- - , - , - ]} - > -
- Please create an account, or login with your account details. -
+
+ + + Select campaign settings - , - , - ]} - > -
- Please sign up for the event you wish to attend. -
+ + Create an ad group - - , - , - ]} - > -
- Please provide your credit card details. -
+ + Create an ad
- +
+ {finished ? ( +

+ { + event.preventDefault(); + this.setState({stepIndex: 0, finished: false}); + }} + > + Click here + to reset the example. +

+ ) : ( +
+

{this.getStepContent(stepIndex)}

+
+ + +
+
+ )} +
+
); - }, -}); + } +} -export default HorizontalStepper; +export default HorizontalLinearStepper; diff --git a/docs/src/app/components/pages/components/Stepper/HorizontalNonLinearStepper.js b/docs/src/app/components/pages/components/Stepper/HorizontalNonLinearStepper.js new file mode 100644 index 00000000000000..8e6e9d4dab7bbe --- /dev/null +++ b/docs/src/app/components/pages/components/Stepper/HorizontalNonLinearStepper.js @@ -0,0 +1,96 @@ +import React from 'react'; +import { + Step, + Stepper, + StepButton, +} from 'material-ui/Stepper'; +import RaisedButton from 'material-ui/RaisedButton'; +import FlatButton from 'material-ui/FlatButton'; + +/** + * Non-linear steppers allow users to enter a multi-step flow at any point. + * + * This example is similiar to the regular horizontal stepper, except steps are no longer + * automatically set to `disabled={true}` based on the `activeStep` prop. + * + * We've used the `` here to demonstrate clickable step labels. + */ +class HorizontalNonLinearStepper extends React.Component { + + state = { + stepIndex: 0, + }; + + handleNext = () => { + const {stepIndex} = this.state; + if (stepIndex < 2) { + this.setState({stepIndex: stepIndex + 1}); + } + }; + + handlePrev = () => { + const {stepIndex} = this.state; + if (stepIndex > 0) { + this.setState({stepIndex: stepIndex - 1}); + } + }; + + getStepContent(stepIndex) { + switch (stepIndex) { + case 0: + return 'Select campaign settings...'; + case 1: + return 'What is an ad group anyways?'; + case 2: + return 'This is the bit I really care about!'; + default: + return 'You\'re a long way from home sonny jim!'; + } + } + + render() { + const {stepIndex} = this.state; + const contentStyle = {margin: '0 16px'}; + + return ( +
+ + + this.setState({stepIndex: 0})}> + Select campaign settings + + + + this.setState({stepIndex: 1})}> + Create an ad group + + + + this.setState({stepIndex: 2})}> + Create an ad + + + +
+

{this.getStepContent(stepIndex)}

+
+ + +
+
+
+ ); + } +} + +export default HorizontalNonLinearStepper; diff --git a/docs/src/app/components/pages/components/Stepper/HorizontalTransition.js b/docs/src/app/components/pages/components/Stepper/HorizontalTransition.js new file mode 100644 index 00000000000000..247d55293603d6 --- /dev/null +++ b/docs/src/app/components/pages/components/Stepper/HorizontalTransition.js @@ -0,0 +1,151 @@ +import React from 'react'; +import { + Step, + Stepper, + StepLabel, +} from 'material-ui/Stepper'; +import RaisedButton from 'material-ui/RaisedButton'; +import FlatButton from 'material-ui/FlatButton'; +import ExpandTransition from 'material-ui/internal/ExpandTransition'; +import TextField from 'material-ui/TextField'; + +/** + * A contrived example using a transition between steps + */ +class HorizontalTransition extends React.Component { + + state = { + loading: false, + finished: false, + stepIndex: 0, + }; + + dummyAsync = (cb) => { + this.setState({loading: true}, () => { + this.asyncTimer = setTimeout(cb, 500); + }); + }; + + handleNext = () => { + const {stepIndex} = this.state; + if (!this.state.loading) { + this.dummyAsync(() => this.setState({ + loading: false, + stepIndex: stepIndex + 1, + finished: stepIndex >= 2, + })); + } + }; + + handlePrev = () => { + const {stepIndex} = this.state; + if (!this.state.loading) { + this.dummyAsync(() => this.setState({ + loading: false, + stepIndex: stepIndex - 1, + })); + } + }; + + getStepContent(stepIndex) { + switch (stepIndex) { + case 0: + return ( +

+ Select campaign settings. Campaign settings can include your budget, network, bidding + options and adjustments, location targeting, campaign end date, and other settings that + affect an entire campaign. +

+ ); + case 1: + return ( +
+ +

+ Ad group status is different than the statuses for campaigns, ads, and keywords, though the + statuses can affect each other. Ad groups are contained within a campaign, and each campaign can + have one or more ad groups. Within each ad group are ads, keywords, and bids. +

+

Something something whatever cool

+
+ ); + case 2: + return ( +

+ Try out different ad text to see what brings in the most customers, and learn how to + enhance your ads using features like ad extensions. If you run into any problems with your + ads, find out how to tell if they're running and how to resolve approval issues. +

+ ); + default: + return 'You\'re a long way from home sonny jim!'; + } + } + + renderContent() { + const {finished, stepIndex} = this.state; + const contentStyle = {margin: '0 16px', overflow: 'hidden'}; + + if (finished) { + return ( + + ); + } + + return ( +
+
{this.getStepContent(stepIndex)}
+
+ + +
+
+ ); + } + + render() { + const {loading, stepIndex} = this.state; + + return ( +
+ + + Select campaign settings + + + Create an ad group + + + Create an ad + + + + {this.renderContent()} + +
+ ); + } +} + +export default HorizontalTransition; diff --git a/docs/src/app/components/pages/components/Stepper/Page.js b/docs/src/app/components/pages/components/Stepper/Page.js index a120dbfdfd4cac..1188a558b357c6 100644 --- a/docs/src/app/components/pages/components/Stepper/Page.js +++ b/docs/src/app/components/pages/components/Stepper/Page.js @@ -4,30 +4,28 @@ import CodeExample from '../../../CodeExample'; import PropTypeDescription from '../../../PropTypeDescription'; import MarkdownElement from '../../../MarkdownElement'; -import stepperReadmeText from './README'; +import stepperReadmeText from './README.md'; +import advancedReadmeText from './Advanced.md'; +import HorizontalLinearStepper from './HorizontalLinearStepper'; +import HorizontalLinearStepperCode from '!raw!./HorizontalLinearStepper'; +import HorizontalNonLinearStepper from './HorizontalNonLinearStepper'; +import HorizontalNonLinearStepperCode from '!raw!./HorizontalNonLinearStepper'; import VerticalLinearStepper from './VerticalLinearStepper'; -import VerticalNonLinearStepper from './VerticalNonLinearStepper'; -import VerticalLinearStepperWithOptionalStep from './VerticalLinearStepperWithOptionalStep'; import VerticalLinearStepperCode from '!raw!./VerticalLinearStepper'; -import VerticalLinearStepperWithOptionalStepCode from '!raw!./VerticalLinearStepperWithOptionalStep'; +import VerticalNonLinearStepper from './VerticalNonLinearStepper'; 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: 'The vertical linear stepper requires steps be completed in a specific order.', - verticalLinearStepperWithOptionalStep: 'Set the `optional` property to `true` for optional steps.' + - 'Pass a custom label view through `stepLabel` property to indicate an optional step.', - verticalNonLinearStepper: 'For the vertical non-linear stepper, steps can be completed in any order.', - horizontalLinearStepper: 'The horizontal linear stepper acts the same as the vertical linear stepper. ' + - 'The horizontal stepper does not support optional or non-linear steps at this time.', -}; +import GranularControlStepper from './GranularControlStepper'; +import GranularControlStepperCode from '!raw!./GranularControlStepper'; +import CustomIcon from './CustomIcon'; +import CustomIconCode from '!raw!./CustomIcon'; +import HorizontalTransition from './HorizontalTransition'; +import HorizontalTransitionCode from '!raw!./HorizontalTransition'; +import stepCode from '!raw!material-ui/Stepper/Step'; +import stepperCode from '!raw!material-ui/Stepper/Stepper'; +import stepButtonCode from '!raw!material-ui/Stepper/StepButton'; +import stepLabelCode from '!raw!material-ui/Stepper/StepLabel'; +import stepContentCode from '!raw!material-ui/Stepper/StepContent'; const styles = { stepperWrapper: { @@ -38,9 +36,18 @@ const styles = { const StepperPage = () => (
+ + +
+ +
+
+
@@ -49,18 +56,16 @@ const StepperPage = () => (
- +
@@ -68,19 +73,40 @@ const StepperPage = () => (
+ +
- + +
+
+ + +
+ +
+
+ + +
+
- - + + + +
); diff --git a/docs/src/app/components/pages/components/Stepper/README.md b/docs/src/app/components/pages/components/Stepper/README.md index 24bbae339d0749..91368fbfb4a095 100644 --- a/docs/src/app/components/pages/components/Stepper/README.md +++ b/docs/src/app/components/pages/components/Stepper/README.md @@ -1,7 +1,12 @@ -## Stepper +# 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 +to the next step). + +## Basic Usage + +The `` can be controlled by passing the current step index (zero based) as the `activeStep` prop. `` orientation is set using the `orientation` prop. Below are basic implementations of both the horizontal and vertical stepper. + +**Note:** In the linear examples we're using `` to display the icon and heading. But in other types of Steppers (or other situations), you may want to use the `` component to make your step clickable. + diff --git a/docs/src/app/components/pages/components/Stepper/VerticalLinearStepper.js b/docs/src/app/components/pages/components/Stepper/VerticalLinearStepper.js index 3ad2e40d3bca13..0b6ff60ade7c4c 100644 --- a/docs/src/app/components/pages/components/Stepper/VerticalLinearStepper.js +++ b/docs/src/app/components/pages/components/Stepper/VerticalLinearStepper.js @@ -1,160 +1,122 @@ import React from 'react'; -import Stepper from 'material-ui/Stepper/Stepper'; -import Step from 'material-ui/Stepper/VerticalStep'; -import Paper from 'material-ui/Paper'; -import FontIcon from 'material-ui/FontIcon'; +import { + Step, + Stepper, + StepLabel, + StepContent, +} from 'material-ui/Stepper'; import RaisedButton from 'material-ui/RaisedButton'; import FlatButton from 'material-ui/FlatButton'; -const styles = { - paper: { - width: 500, - margin: 'auto', - }, - header: { - textAlign: 'center', - padding: 10, - fontSize: 20, - }, - actionButton: { - marginRight: 8, - }, -}; +/** + * Vertical steppers are designed for narrow screen sizes. They are ideal for mobile. + * + * To use the vertical stepper with the contained content as seen in spec examples, + * you must use the `` component inside the ``. + * + * (The vertical stepper can also be used without `` to display a basic stepper.) + */ +class VerticalLinearStepper extends React.Component { -const VerticalLinearStepper = React.createClass({ - getInitialState() { - return { - activeStep: -1, - lastActiveStep: 0, - }; - }, - - handleStepHeaderTouch(currentStep) { - const { - lastActiveStep, - activeStep, - - } = this.state; - - if (currentStep > lastActiveStep) { - return; - } - - this.setState({ - activeStep: currentStep, - lastActiveStep: Math.max(lastActiveStep, activeStep), - }); - }, - - updateCompletedSteps(currentStep) { - return currentStep < this.state.lastActiveStep; - }, - - handleTouchTap() { - const { - activeStep, - lastActiveStep, - } = this.state; + state = { + finished: false, + stepIndex: 0, + }; + handleNext = () => { + const {stepIndex} = this.state; this.setState({ - activeStep: activeStep + 1, - lastActiveStep: Math.max(lastActiveStep, activeStep + 1), + stepIndex: stepIndex + 1, + finished: stepIndex >= 2, }); - }, + }; - createIcon(step) { - if (step.props.isCompleted) { - return ( - - done - - ); + handlePrev = () => { + const {stepIndex} = this.state; + if (stepIndex > 0) { + this.setState({stepIndex: stepIndex - 1}); } + }; - return {step.props.orderStepLabel}; - }, + renderStepActions(step) { + const {stepIndex} = this.state; + + return ( +
+ + {step > 0 && ( + + )} +
+ ); + } render() { + const {finished, stepIndex} = this.state; + return ( - -
- Create an Ad Campaign -
- - , - , - ]} - > -
- Please select the type of campaign you wish to create. -
+
+ + + Select campaign settings + +

+ For each ad campaign that you create, you can control how much + you're willing to spend on clicks and conversions, which networks + and geographical locations you want your ads to show on, and more. +

+ {this.renderStepActions(0)} +
- , - , - ]} - > -
- Please create an ad group for this campaign.

- Your campaign may contain multiple ad groups. -
+ + Create an ad group + +

An ad group contains one or more ads which target a shared set of keywords.

+ {this.renderStepActions(1)} +
- - , - , - ]} - > -
- Please create one or more adverts for this ad group. -
+ + Create an ad + +

+ Try out different ad text to see what brings in the most customers, + and learn how to enhance your ads using features like ad extensions. + If you run into any problems with your ads, find out how to tell if + they're running and how to resolve approval issues. +

+ {this.renderStepActions(2)} +
- + {finished && ( +

+ { + event.preventDefault(); + this.setState({stepIndex: 0, finished: false}); + }} + > + Click here + to reset the example. +

+ )} +
); - }, -}); + } +} export default VerticalLinearStepper; diff --git a/docs/src/app/components/pages/components/Stepper/VerticalLinearStepperWithOptionalStep.js b/docs/src/app/components/pages/components/Stepper/VerticalLinearStepperWithOptionalStep.js deleted file mode 100644 index df71f060429fa6..00000000000000 --- a/docs/src/app/components/pages/components/Stepper/VerticalLinearStepperWithOptionalStep.js +++ /dev/null @@ -1,190 +0,0 @@ -import React from 'react'; -import Stepper from 'material-ui/Stepper/Stepper'; -import Step from 'material-ui/Stepper/VerticalStep'; -import Paper from 'material-ui/Paper'; -import FontIcon from 'material-ui/FontIcon'; -import RaisedButton from 'material-ui/RaisedButton'; -import FlatButton from 'material-ui/FlatButton'; -import SeatIcon from 'material-ui/svg-icons/action/event-seat'; -import PrintIcon from 'material-ui/svg-icons/action/print'; - -const styles = { - icon: { - width: 15, - height: 15, - }, - paper: { - width: 500, - margin: 'auto', - }, - header: { - textAlign: 'center', - padding: 10, - fontSize: 20, - }, - actionButton: { - marginRight: 8, - }, - stepLabelSecondary: { - fontSize: 10, - lineHeight: '5px', - }, -}; - -const VerticalLinearStepper = React.createClass({ - getInitialState() { - return { - activeStep: -1, - lastActiveStep: 0, - statusSteps: [], - }; - }, - - handleStepHeaderTouch(currentStep, step) { - const { - lastActiveStep, - activeStep, - - } = this.state; - - if (currentStep > lastActiveStep && lastActiveStep < step.props.previousStepOptionalIndex) { - return; - } - - this.setState({ - activeStep: currentStep, - lastActiveStep: Math.max(lastActiveStep, activeStep), - }); - }, - - updateCompletedSteps(currentStep) { - return this.state.statusSteps[currentStep]; - }, - - createIcon(step) { - if (step.props.isCompleted) { - return ( - - flight_takeoff - - ); - } - - return {step.props.orderStepLabel}; - }, - - handleTouchTap() { - const { - activeStep, - lastActiveStep, - statusSteps, - } = this.state; - - statusSteps[activeStep] = true; - - this.setState({ - activeStep: activeStep + 1, - statusSteps: statusSteps, - lastActiveStep: Math.max(lastActiveStep, activeStep + 1), - }); - }, - - render() { - return ( - -
- Online check-in -
- - - flight - - } - stepLabel="Flight details" - actions={[ - , - , - ]} - > -
- Please enter your booking reference, and flight number. -
-
- - } - isCompleted={false} - optional={true} - stepLabel={ -
-
Seat selection
-
optional
-
- } - stepHeaderStyle={{ - alignItems: 'center', - }} - actions={[ - , - , - ]} - > -
- If you wish to change your assigned seat, please select - an alternative seat, or click Finish below to skip this step. -
-
- - } - stepLabel="Boarding pass" - actions={[ - , - , - ]} - > -
- Please print your boarding pass. -
-
-
-
- ); - }, -}); - -export default VerticalLinearStepper; diff --git a/docs/src/app/components/pages/components/Stepper/VerticalNonLinearStepper.js b/docs/src/app/components/pages/components/Stepper/VerticalNonLinearStepper.js index 212f15ac6d9a2f..d9ac1c14cc167f 100644 --- a/docs/src/app/components/pages/components/Stepper/VerticalNonLinearStepper.js +++ b/docs/src/app/components/pages/components/Stepper/VerticalNonLinearStepper.js @@ -1,150 +1,109 @@ import React from 'react'; -import Stepper from 'material-ui/Stepper/Stepper'; -import Step from 'material-ui/Stepper/VerticalStep'; -import Paper from 'material-ui/Paper'; -import FontIcon from 'material-ui/FontIcon'; +import { + Step, + Stepper, + StepButton, + StepContent, +} from 'material-ui/Stepper'; import RaisedButton from 'material-ui/RaisedButton'; import FlatButton from 'material-ui/FlatButton'; -const styles = { - paper: { - width: 500, - margin: 'auto', - }, - header: { - textAlign: 'center', - padding: 10, - fontSize: 20, - }, - actionButton: { - marginRight: 8, - }, -}; +/** + * A basic vertical non-linear implementation + */ +class VerticalNonLinear extends React.Component { -const VerticalNonLinearStepper = React.createClass({ - getInitialState() { - return { - activeStep: -1, - statusSteps: [], - }; - }, + state = { + stepIndex: 0, + }; - handleStepHeaderTouch(CurrentStep) { - this.setState({ - activeStep: CurrentStep, - }); - }, - - updateCompletedSteps(CurrentStep) { - return this.state.statusSteps[CurrentStep]; - }, - - createIcon(step) { - if (step.props.isCompleted) { - return ( - - done - - ); + handleNext = () => { + const {stepIndex} = this.state; + if (stepIndex < 2) { + this.setState({stepIndex: stepIndex + 1}); } + }; - return {step.props.orderStepLabel}; - }, - - handleTouchTap() { - const { - activeStep, - statusSteps, - } = this.state; - - statusSteps[activeStep] = true; + handlePrev = () => { + const {stepIndex} = this.state; + if (stepIndex > 0) { + this.setState({stepIndex: stepIndex - 1}); + } + }; - this.setState({ - activeStep: activeStep + 1, - statusSteps: statusSteps, - }); - }, + renderStepActions(step) { + return ( +
+ + {step > 0 && ( + + )} +
+ ); + } render() { + const {stepIndex} = this.state; + return ( - -
- Your interests -
+
- , - , - ]} - > -
- Please list your favorite reads. -
+ + this.setState({stepIndex: 0})}> + Select campaign settings + + +

+ For each ad campaign that you create, you can control how much + you're willing to spend on clicks and conversions, which networks + and geographical locations you want your ads to show on, and more. +

+ {this.renderStepActions(0)} +
- , - , - ]} - > -
- Please list your favorite flicks. -
+ + this.setState({stepIndex: 1})}> + Create an ad group + + +

An ad group contains one or more ads which target a shared set of keywords.

+ {this.renderStepActions(1)} +
- - , - , - ]} - > -
- Please list your favorite tunes. -
+ + this.setState({stepIndex: 2})}> + Create an ad + + +

+ Try out different ad text to see what brings in the most customers, + and learn how to enhance your ads using features like ad extensions. + If you run into any problems with your ads, find out how to tell if + they're running and how to resolve approval issues. +

+ {this.renderStepActions(2)} +
- +
); - }, -}); + } +} -export default VerticalNonLinearStepper; +export default VerticalNonLinear; diff --git a/src/Stepper/HorizontalStep.js b/src/Stepper/HorizontalStep.js deleted file mode 100644 index 2b55f3747631ca..00000000000000 --- a/src/Stepper/HorizontalStep.js +++ /dev/null @@ -1,211 +0,0 @@ -import React from 'react'; -import TouchRipple from '../internal/TouchRipple'; -import Avatar from '../Avatar'; - -class HorizontalStep extends React.Component { - static propTypes = { - - /** - * @ignore - * The width of step header, unit is % which passed from Stepper. - */ - headerWidth: React.PropTypes.string, - - /** - * @ignore - * If true, the step is active. - */ - isActive: React.PropTypes.bool, - - /** - * @ignore - * If true, the step is completed. - */ - isCompleted: React.PropTypes.bool, - - /** - * @ignore - * If true, the step is the first step. - */ - isFirstStep: React.PropTypes.bool, - - /** - * @ignore - * If true, the step is the last step. - */ - isLastStep: React.PropTypes.bool, - - /** - * @ignore - * If true, the step header is hovered. - */ - isStepHeaderHovered: React.PropTypes.bool, - - /** - * @ignore - * Callback function will be called when step header is hovered. - */ - onStepHeaderHover: React.PropTypes.func, - - /** - * @ignore - * Call back function will be called when step header is touched. - */ - onStepHeaderTouch: React.PropTypes.func, - - /** - * Override inline-style of step header wrapper. - */ - stepHeaderWrapperStyle: React.PropTypes.object, - - /** - * @ignore - * The index of step in array of Steps. - */ - stepIndex: React.PropTypes.number, - - /** - * The label of step which be shown in step header. - */ - stepLabel: React.PropTypes.node, - }; - - static contextTypes = { - muiTheme: React.PropTypes.object.isRequired, - createIcon: React.PropTypes.func, - updateAvatarBackgroundColor: React.PropTypes.func, - }; - - getStyles() { - const { - headerWidth, - isActive, - isCompleted, - isStepHeaderHovered, - stepHeaderWrapperStyle, - } = this.props; - - const theme = this.context.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/Step.js b/src/Stepper/Step.js new file mode 100644 index 00000000000000..0b062e368e7336 --- /dev/null +++ b/src/Stepper/Step.js @@ -0,0 +1,95 @@ +import React, {PropTypes} from 'react'; + +const getStyles = ({index}, {stepper}) => { + const {orientation} = stepper; + const styles = { + root: { + flex: '0 0 auto', + }, + }; + + if (index > 0) { + if (orientation === 'horizontal') { + styles.root.marginLeft = -6; + } else if (orientation === 'vertical') { + styles.root.marginTop = -14; + } + } + + return styles; +}; + +export default class Step extends React.Component { + + static propTypes = { + /** + * Sets the step as active. Is passed to child components. + */ + active: PropTypes.bool, + /** + * Should be `Step` sub-components such as `StepLabel`. + */ + children: PropTypes.node, + /** + * Mark the step as completed. Is passed to child components. + */ + completed: PropTypes.bool, + /** + * Mark the step as disabled, will also disable the button if + * `StepButton` is a child of `Step`. Is passed to child components. + */ + disabled: PropTypes.bool, + /** + * @ignore + * Used internally for numbering. + */ + index: PropTypes.number, + /** + * @ignore + */ + last: PropTypes.bool, + /** + * Override the inline-style of the root element. + */ + style: PropTypes.object, + }; + + static contextTypes = { + muiTheme: PropTypes.object.isRequired, + stepper: PropTypes.object, + }; + + renderChild = (child) => { + const { + active, + completed, + disabled, + index, + last, + } = this.props; + + const icon = index + 1; + + return React.cloneElement(child, Object.assign( + {active, completed, disabled, icon, last}, + child.props, + )); + } + + render() { + const { + children, + style, + ...other, + } = this.props; + + const {prepareStyles} = this.context.muiTheme; + const styles = getStyles(this.props, this.context); + + return ( +
+ {React.Children.map(children, this.renderChild)} +
+ ); + } +} diff --git a/src/Stepper/Step.spec.js b/src/Stepper/Step.spec.js new file mode 100644 index 00000000000000..c1a55a2a0e6232 --- /dev/null +++ b/src/Stepper/Step.spec.js @@ -0,0 +1,80 @@ +/* eslint-env mocha */ +import React from 'react'; +import {shallow} from 'enzyme'; +import {assert} from 'chai'; +import Step from './Step'; +import getMuiTheme from '../styles/getMuiTheme'; + +describe('', () => { + const muiTheme = getMuiTheme(); + const shallowWithContext = (node, context = {}) => { + return shallow(node, { + context: { + muiTheme, + stepper: {orientation: 'horizontal'}, + ...context, + }, + }); + }; + + it('merges styles and other props into the root node', () => { + const wrapper = shallowWithContext( + + ); + const {style, myProp} = wrapper.props(); + assert.strictEqual(style.paddingRight, 200); + assert.strictEqual(style.color, 'purple'); + assert.strictEqual(style.border, '1px solid tomato'); + assert.strictEqual(myProp, 'hello'); + }); + + describe('rendering children', () => { + it('renders children', () => { + const children =

Hello World

; + const wrapper = shallowWithContext( + {children} + ); + assert.strictEqual(wrapper.find('.hello-world').length, 1); + }); + + it('renders children with all props passed through', () => { + const children = [ +

Hello World

, +

How are you?

, + ]; + const wrapper = shallowWithContext( + + {children} + + ); + const child1 = wrapper.find('.hello-world'); + const child2 = wrapper.find('.hay'); + [child1, child2].forEach((child) => { + assert.strictEqual(child.length, 1); + assert.strictEqual(child.prop('active'), false); + assert.strictEqual(child.prop('completed'), true); + assert.strictEqual(child.prop('disabled'), true); + assert.strictEqual(child.prop('icon'), 1); + }); + }); + + it('honours children overriding props passed through', () => { + const children = ( +

Hello World

+ ); + const wrapper = shallowWithContext( + {children} + ); + const childWrapper = wrapper.find('.hello-world'); + assert.strictEqual(childWrapper.prop('active'), false); + }); + }); +}); diff --git a/src/Stepper/StepButton.js b/src/Stepper/StepButton.js new file mode 100644 index 00000000000000..0c50b3d6baeafa --- /dev/null +++ b/src/Stepper/StepButton.js @@ -0,0 +1,147 @@ +import React, {PropTypes} from 'react'; +import transitions from '../styles/transitions'; +import EnhancedButton from '../internal/EnhancedButton'; +import StepLabel from './StepLabel'; + +const isLabel = (child) => { + return child && child.type && child.type.muiName === 'StepLabel'; +}; + +const getStyles = (props, context, state) => { + const {hovered} = state; + const {backgroundColor, hoverBackgroundColor} = context.muiTheme.stepper; + + const styles = { + root: { + padding: 0, + backgroundColor: hovered ? hoverBackgroundColor : backgroundColor, + transition: transitions.easeOut(), + }, + }; + + if (context.stepper.orientation === 'vertical') { + styles.root.width = '100%'; + } + + return styles; +}; + +class StepButton extends React.Component { + + static propTypes = { + /** + * Passed from `Step` Is passed to StepLabel. + */ + active: PropTypes.bool, + /** + * Can be a `StepLabel` or a node to place inside `StepLabel` as children. + */ + children: PropTypes.node, + /** + * Sets completed styling. Is passed to StepLabel. + */ + completed: PropTypes.bool, + /** + * Disables the button and sets disabled styling. Is passed to StepLabel. + */ + disabled: PropTypes.bool, + /** + * The icon displayed by the step label. + */ + icon: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.string, + PropTypes.number, + ]), + /** + * Callback function fired when the mouse enters the element. + * + * @param {object} event `mouseenter` event targeting the element. + */ + onMouseEnter: PropTypes.func, + /** + * Callback function fired when the mouse leaves the element. + * + * @param {object} event `mouseleave` event targeting the element. + */ + onMouseLeave: PropTypes.func, + /** + * Callback function fired when the element is touched. + * + * @param {object} event `touchstart` event targeting the element. + */ + onTouchStart: PropTypes.func, + /** + * Override the inline-style of the root element. + */ + style: PropTypes.object, + }; + + static contextTypes = { + muiTheme: PropTypes.object.isRequired, + stepper: PropTypes.object, + }; + + state = { + hovered: false, + touch: false, + }; + + handleMouseEnter = (event) => { + const {onMouseEnter} = this.props; + // Cancel hover styles for touch devices + if (!this.state.touch) { + this.setState({hovered: true}); + } + if (typeof onMouseEnter === 'function') { + onMouseEnter(event); + } + }; + + handleMouseLeave = (event) => { + const {onMouseLeave} = this.props; + this.setState({hovered: false}); + if (typeof onMouseLeave === 'function') { + onMouseLeave(event); + } + }; + + handleTouchStart = (event) => { + const {onTouchStart} = this.props; + this.setState({touch: true}); + if (typeof onTouchStart === 'function') { + onTouchStart(event); + } + }; + + render() { + const { + active, + children, + completed, + disabled, + icon, + style, + ...other, + } = this.props; + + const styles = getStyles(this.props, this.context, this.state); + + const child = isLabel(children) ? children : {children}; + + return ( + + {React.cloneElement(child, {active, completed, disabled, icon})} + + ); + } +} + +export default StepButton; diff --git a/src/Stepper/StepButton.spec.js b/src/Stepper/StepButton.spec.js new file mode 100644 index 00000000000000..2d381b32fbe894 --- /dev/null +++ b/src/Stepper/StepButton.spec.js @@ -0,0 +1,96 @@ +/* eslint-env mocha */ +import React from 'react'; +import {shallow} from 'enzyme'; +import {assert} from 'chai'; +import sinon from 'sinon'; +import StepButton from './StepButton'; +import getMuiTheme from '../styles/getMuiTheme'; + +describe('', () => { + const muiTheme = getMuiTheme(); + const themedShallow = (node) => { + const context = {muiTheme, stepper: {orientation: 'horizontal'}}; + return shallow(node, {context}); + }; + + it('merges user styles in', () => { + const wrapper = themedShallow( + Step One + ); + + assert.strictEqual(wrapper.props().style.backgroundColor, 'purple'); + }); + + it('renders an EnhancedButton with a StepLabel', () => { + const wrapper = themedShallow( + Step One + ); + assert.ok(wrapper.is('EnhancedButton')); + const stepLabel = wrapper.find('StepLabel'); + assert.strictEqual(stepLabel.length, 1); + assert.strictEqual(stepLabel.props().children, 'Step One'); + }); + + it('passes props to StepLabel', () => { + const wrapper = themedShallow( + + Step One + + ); + const stepLabel = wrapper.find('StepLabel'); + assert.strictEqual(stepLabel.prop('active'), true); + assert.strictEqual(stepLabel.prop('completed'), true); + assert.strictEqual(stepLabel.prop('disabled'), true); + }); + + it('passes props to EnhancedButton', () => { + const wrapper = themedShallow( + Step One + ); + const stepLabel = wrapper.find('EnhancedButton'); + assert.strictEqual(stepLabel.prop('disabled'), true); + }); + + it('bubbles callbacks used internally', () => { + const handleMouseEnter = sinon.spy(); + const handleMouseLeave = sinon.spy(); + const handleTouchStart = sinon.spy(); + const wrapper = themedShallow( + + Step One + + ); + wrapper.simulate('mouseEnter'); + assert.ok(handleMouseEnter.calledOnce); + wrapper.simulate('mouseLeave'); + assert.ok(handleMouseEnter.calledOnce); + assert.ok(handleMouseLeave.calledOnce); + wrapper.simulate('touchStart'); + assert.ok(handleMouseEnter.calledOnce); + assert.ok(handleMouseLeave.calledOnce); + assert.ok(handleTouchStart.calledOnce); + wrapper.simulate('mouseEnter'); + wrapper.simulate('touchStart'); + assert.ok(handleMouseEnter.calledTwice); + assert.ok(handleMouseLeave.calledOnce); + assert.ok(handleTouchStart.calledTwice); + }); + + it('sets the EnhancedButton backgroundColor on hover', () => { + const wrapper = themedShallow( + Step One + ); + assert.strictEqual(wrapper.prop('style').backgroundColor, muiTheme.stepper.backgroundColor); + wrapper.setState({hovered: true}); + assert.strictEqual(wrapper.prop('style').backgroundColor, muiTheme.stepper.hoverBackgroundColor); + }); +}); diff --git a/src/Stepper/StepConnector.js b/src/Stepper/StepConnector.js new file mode 100644 index 00000000000000..cbe2a98da4a5fa --- /dev/null +++ b/src/Stepper/StepConnector.js @@ -0,0 +1,56 @@ +import React, {PropTypes} from 'react'; +import pure from 'recompose/pure'; + +const propTypes = { + /** + * Override the inline-style of the root element. + */ + style: PropTypes.object, +}; + +const contextTypes = { + muiTheme: PropTypes.object.isRequired, + stepper: PropTypes.object, +}; + +const StepConnector = (props, context) => { + const {muiTheme, stepper} = context; + + const styles = { + wrapper: { + flex: '1 1 auto', + }, + line: { + display: 'block', + borderColor: muiTheme.stepper.connectorLineColor, + }, + }; + + /** + * Clean up once we can use CSS pseudo elements + */ + if (stepper.orientation === 'horizontal') { + styles.line.marginLeft = -6; + styles.line.borderTopStyle = 'solid'; + styles.line.borderTopWidth = 1; + } else if (stepper.orientation === 'vertical') { + styles.wrapper.marginLeft = 14 + 11; // padding + 1/2 icon + styles.line.borderLeftStyle = 'solid'; + styles.line.borderLeftWidth = 1; + styles.line.minHeight = 28; + } + + const {prepareStyles} = muiTheme; + + return ( +
+ +
+ ); +}; + +StepConnector.propTypes = propTypes; +StepConnector.contextTypes = contextTypes; + +export {StepConnector as PlainStepConnector}; +export default pure(StepConnector); diff --git a/src/Stepper/StepConnector.spec.js b/src/Stepper/StepConnector.spec.js new file mode 100644 index 00000000000000..d7572820901dce --- /dev/null +++ b/src/Stepper/StepConnector.spec.js @@ -0,0 +1,39 @@ +/* eslint-env mocha */ +import React from 'react'; +import {shallow} from 'enzyme'; +import {assert} from 'chai'; +import {PlainStepConnector as StepConnector} from './StepConnector'; +import getMuiTheme from '../styles/getMuiTheme'; + +describe('', () => { + const muiTheme = getMuiTheme(); + const themedShallow = (node) => { + const context = {muiTheme, stepper: {orientation: 'horizontal'}}; + return shallow(node, {context}); + }; + + describe('rendering', () => { + const wrapper = themedShallow( + + ); + + it('renders a div containing a span', () => { + assert.ok(wrapper.is('div')); + const line = wrapper.find('span'); + assert.ok(line.length); + }); + + it('has a top border when horizontal', () => { + const line = wrapper.find('span'); + assert.strictEqual(line.prop('style').borderTopWidth, 1); + assert.notOk(line.prop('style').borderLeftWidth); + }); + + it('has a left border when vertical', () => { + wrapper.setContext({muiTheme, stepper: {orientation: 'vertical'}}); + const line = wrapper.find('span'); + assert.strictEqual(line.prop('style').borderLeftWidth, 1); + assert.notOk(line.prop('style').borderTopWidth); + }); + }); +}); diff --git a/src/Stepper/StepContent.js b/src/Stepper/StepContent.js new file mode 100644 index 00000000000000..fce58b1d043d48 --- /dev/null +++ b/src/Stepper/StepContent.js @@ -0,0 +1,75 @@ +import React, {PropTypes} from 'react'; +import ExpandTransition from '../internal/ExpandTransition'; +import warning from 'warning'; + +const getStyles = (props, context) => { + const styles = { + root: { + marginTop: -14, + marginLeft: 14 + 11, // padding + 1/2 icon + paddingLeft: 24 - 11 + 8, + paddingRight: 16, + overflow: 'hidden', + }, + }; + + if (!props.last) { + styles.root.borderLeft = `1px solid ${context.muiTheme.stepper.connectorLineColor}`; + } + + return styles; +}; + +class StepContent extends React.Component { + static propTypes = { + /** + * Expands the content + */ + active: PropTypes.bool, + /** + * Step content + */ + children: PropTypes.node, + /** + * @ignore + */ + last: PropTypes.bool, + /** + * Override the inline-style of the root element. + */ + style: PropTypes.object, + }; + + static contextTypes = { + muiTheme: PropTypes.object.isRequired, + stepper: PropTypes.object, + }; + + render() { + const { + active, + children, + last, // eslint-disable-line no-unused-vars + style, + ...other, + } = this.props; + const {stepper, muiTheme: {prepareStyles}} = this.context; + + if (stepper.orientation !== 'vertical') { + warning(false, ' is only designed for use with the vertical stepper.'); + return null; + } + + const styles = getStyles(this.props, this.context); + + return ( +
+ +
{children}
+
+
+ ); + } +} + +export default StepContent; diff --git a/src/Stepper/StepContent.spec.js b/src/Stepper/StepContent.spec.js new file mode 100644 index 00000000000000..b52cfa53491ca2 --- /dev/null +++ b/src/Stepper/StepContent.spec.js @@ -0,0 +1,60 @@ +/* eslint-env mocha */ +import React from 'react'; +import {shallow} from 'enzyme'; +import {assert} from 'chai'; +import StepContent from './StepContent'; +import getMuiTheme from '../styles/getMuiTheme'; + +describe('', () => { + const muiTheme = getMuiTheme(); + const shallowWithContext = (node, context = {}) => { + return shallow(node, { + context: { + muiTheme, + stepper: {orientation: 'vertical'}, + ...context, + }, + }); + }; + + it('renders a div', () => { + const wrapper = shallowWithContext( + + ); + assert.ok(wrapper.is('div')); + }); + + it('renders null when used in a horizontal stepper', () => { + const wrapper = shallowWithContext( + + , {stepper: {orientation: 'horizontal'}}); + assert.strictEqual(wrapper.node, null); + }); + + it('merges styles and other props into the root node', () => { + const wrapper = shallowWithContext( + + ); + const {style, myProp} = wrapper.props(); + assert.strictEqual(style.paddingRight, 200); + assert.strictEqual(style.color, 'purple'); + assert.strictEqual(style.border, '1px solid tomato'); + assert.strictEqual(myProp, 'hello'); + }); + + it('renders children inside an ExpandTransition group', () => { + const wrapper = shallowWithContext( + +
This is my content!
+
+ ); + const transitionGroup = wrapper.find('ExpandTransition'); + assert.ok(transitionGroup.length); + const content = transitionGroup.find('.test-content'); + assert.ok(content.length); + assert.strictEqual(content.props().children, 'This is my content!'); + }); +}); diff --git a/src/Stepper/StepLabel.js b/src/Stepper/StepLabel.js new file mode 100644 index 00000000000000..566c471f39cdd8 --- /dev/null +++ b/src/Stepper/StepLabel.js @@ -0,0 +1,152 @@ +import React, {PropTypes} from 'react'; +import CheckCircle from '../svg-icons/action/check-circle'; +import SvgIcon from '../SvgIcon'; + +const getStyles = ({active, completed, disabled}, {muiTheme, stepper}) => { + const { + textColor, + disabledTextColor, + iconColor, + inactiveIconColor, + } = muiTheme.stepper; + const {orientation} = stepper; + + const styles = { + root: { + height: orientation === 'horizontal' ? 72 : 64, + color: textColor, + display: 'flex', + alignItems: 'center', + fontSize: 14, + paddingLeft: 14, + paddingRight: 14, + }, + icon: { + color: iconColor, + display: 'block', + fontSize: 24, + width: 24, + height: 24, + }, + iconContainer: { + display: 'flex', + alignItems: 'center', + paddingRight: 8, + width: 24, + }, + }; + + if (active) { + styles.root.fontWeight = 500; + } + + if (!completed && !active) { + styles.icon.color = inactiveIconColor; + } + + if (disabled) { + styles.icon.color = inactiveIconColor; + styles.root.color = disabledTextColor; + } + + return styles; +}; + +class StepLabel extends React.Component { + static muiName = 'StepLabel'; + + static propTypes = { + /** + * Sets active styling. Overrides disabled coloring. + */ + active: PropTypes.bool, + /** + * The label text node + */ + children: PropTypes.node, + /** + * Sets completed styling. Overrides disabled coloring. + */ + completed: PropTypes.bool, + /** + * Sets disabled styling. + */ + disabled: PropTypes.bool, + /** + * The icon displayed by the step label. + */ + icon: PropTypes.oneOfType([ + PropTypes.element, + PropTypes.string, + PropTypes.number, + ]), + /** + * Override the inline-style of the root element. + */ + style: PropTypes.object, + }; + + static contextTypes = { + muiTheme: PropTypes.object.isRequired, + stepper: PropTypes.object, + }; + + renderIcon(completed, icon, styles) { + const iconType = typeof icon; + + if (iconType === 'number' || iconType === 'string') { + if (completed) { + return ( + + ); + } + + return ( + + + + {icon} + + + ); + } + + return icon; + } + + render() { + const { + children, + completed, + icon: userIcon, + style, + ...other, + } = this.props; + + const {prepareStyles} = this.context.muiTheme; + const styles = getStyles(this.props, this.context); + const icon = this.renderIcon(completed, userIcon, styles); + + return ( + + {icon && ( + + {icon} + + )} + {children} + + ); + } +} + +export default StepLabel; diff --git a/src/Stepper/StepLabel.spec.js b/src/Stepper/StepLabel.spec.js new file mode 100644 index 00000000000000..05301df2cec675 --- /dev/null +++ b/src/Stepper/StepLabel.spec.js @@ -0,0 +1,204 @@ +/* eslint-env mocha */ +import React from 'react'; +import {shallow} from 'enzyme'; +import {assert} from 'chai'; +import StepLabel from './StepLabel'; +import getMuiTheme from '../styles/getMuiTheme'; + +describe('', () => { + const muiTheme = getMuiTheme(); + const shallowWithContext = (node, context = {}) => { + return shallow(node, { + context: { + muiTheme, + stepper: {orientation: 'horizontal'}, + ...context, + }, + }); + }; + + it('merges styles and other props into the root node', () => { + const wrapper = shallowWithContext( + + ); + const {style, myProp} = wrapper.props(); + assert.strictEqual(style.paddingRight, 200); + assert.strictEqual(style.color, 'purple'); + assert.strictEqual(style.border, '1px solid tomato'); + assert.strictEqual(myProp, 'hello'); + }); + + describe('label content', () => { + it('renders the label from children', () => { + const childWrapper = shallowWithContext( + Step One + ); + assert.ok(childWrapper.contains('Step One')); + }); + + it('renders the icon from a number with the disabled color', () => { + const wrapper = shallowWithContext( + Step One + ); + const icon = wrapper.find('SvgIcon'); + assert.strictEqual(icon.length, 1, 'should have an '); + assert.strictEqual( + icon.props().color, + muiTheme.stepper.inactiveIconColor, + 'should pass the inactive icon color' + ); + }); + + it('renders the custom icon', () => { + const wrapper = shallowWithContext( + }>Step One + ); + assert.strictEqual(wrapper.find('.my-icon').length, 1, 'should have the custom icon'); + }); + }); + + describe('prop: active = false', () => { + it('renders text with no specific font weight', () => { + const wrapper = shallowWithContext( + Step One + ); + assert.strictEqual(typeof wrapper.props().style.fontWeight, 'undefined'); + }); + }); + + describe('prop: active = true', () => { + it('renders the label text bold', () => { + const wrapper = shallowWithContext( + Step One + ); + assert.strictEqual(wrapper.props().style.fontWeight, 500); + }); + + it('renders with the standard coloring', () => { + const wrapper = shallowWithContext( + Step One + ); + assert.strictEqual( + wrapper.props().style.color, + muiTheme.stepper.textColor, + 'should have the standard text color' + ); + const icon = wrapper.find('SvgIcon'); + assert.strictEqual( + icon.props().color, + muiTheme.stepper.iconColor, + 'should pass the standard icon color' + ); + }); + }); + + describe('prop: completed = true', () => { + it('renders the label text with no specific font weight', () => { + const wrapper = shallowWithContext( + Step One + ); + assert.strictEqual(typeof wrapper.props().style.fontWeight, 'undefined'); + }); + + it('renders a check circle with the standard coloring', () => { + const wrapper = shallowWithContext( + Step One + ); + assert.strictEqual( + wrapper.props().style.color, + muiTheme.stepper.textColor, + 'should have the standard text color' + ); + }); + }); + + describe('prop combinations', () => { + it('renders with active styling when active', () => { + const wrapper = shallowWithContext( + Step One + ); + assert.strictEqual( + wrapper.props().style.color, + muiTheme.stepper.textColor, + 'should have the standard text color' + ); + const icon = wrapper.find('SvgIcon'); + assert.strictEqual( + icon.props().color, + muiTheme.stepper.iconColor, + 'should pass the standard icon color' + ); + }); + + it('renders with inactive styling when inactive and not complete', () => { + const wrapper = shallowWithContext( + Step One + ); + assert.strictEqual( + wrapper.props().style.color, + muiTheme.stepper.textColor, + 'should have the standard text color' + ); + const icon = wrapper.find('SvgIcon'); + assert.strictEqual( + icon.props().color, + muiTheme.stepper.inactiveIconColor, + 'should pass the inactive icon color' + ); + }); + + it('renders with disabled styling when disabled', () => { + const wrapper = shallowWithContext( + Step One + ); + assert.strictEqual( + wrapper.props().style.color, + muiTheme.stepper.disabledTextColor, + 'should have the disabled text color' + ); + const icon = wrapper.find('SvgIcon'); + assert.strictEqual( + icon.props().color, + muiTheme.stepper.inactiveIconColor, + 'should pass the inactive icon color' + ); + }); + + it('renders with a check icon and active styling when completed', () => { + const wrapper = shallowWithContext( + Step One + ); + assert.strictEqual( + wrapper.props().style.color, + muiTheme.stepper.textColor, + 'should have the standard text color' + ); + const icon = wrapper.find('ActionCheckCircle'); + assert.strictEqual( + icon.props().color, + muiTheme.stepper.iconColor, + 'should pass the standard icon color' + ); + }); + + it('renders with a check icon and disabled when disabled and completed', () => { + const wrapper = shallowWithContext( + Step One + ); + assert.strictEqual( + wrapper.props().style.color, + muiTheme.stepper.disabledTextColor, + 'should have the disabled text color' + ); + const icon = wrapper.find('ActionCheckCircle'); + assert.strictEqual( + icon.props().color, + muiTheme.stepper.inactiveIconColor, + 'should pass the inactive icon color' + ); + }); + }); +}); diff --git a/src/Stepper/Stepper.js b/src/Stepper/Stepper.js index f8dc2a61593cb8..2462331fd91047 100644 --- a/src/Stepper/Stepper.js +++ b/src/Stepper/Stepper.js @@ -1,294 +1,103 @@ import React, {PropTypes} from 'react'; -import Paper from '../Paper'; +import StepConnector from './StepConnector'; + +const getStyles = (props) => { + const {orientation} = props; + return { + root: { + display: 'flex', + flexDirection: orientation === 'horizontal' ? 'row' : 'column', + alignContent: 'center', + alignItems: orientation === 'horizontal' ? 'center' : 'stretch', + justifyContent: 'space-between', + }, + }; +}; class Stepper extends React.Component { + static propTypes = { /** - * Set the active step. + * Set the active step (zero based index). This will enable `Step` control helpers. */ activeStep: PropTypes.number, - - /** - * Should be two or more `HorizontalStep` or `VerticalStep`. - */ - children: PropTypes.node, - - /** - * Override the inline-style of the content container. - */ - containerStyle: PropTypes.object, - - /** - * Function used to set a suitable icon for the step, based on the current state of the step. - * - * @param {node} Step Component that is being updated. - * @returns {node} - Icon that will be shown for the step. - */ - createIcon: PropTypes.func, - /** - * If true, it will be horizontal stepper. Should match the step type used for `children`. + * Should be two or more `` components */ - horizontal: PropTypes.bool, - + children: PropTypes.arrayOf(PropTypes.element), /** - * Callback function fired when the step header is touched. - * - * @param {number} stepIndex - The index of step is being touched. - * @param {node} Step component that is being touched. + * If set to `true`, the `Stepper` will assist in controlling steps for linear flow */ - onStepHeaderTouch: PropTypes.func, - + linear: PropTypes.bool, /** - * Override the inline-style of the step header wrapper. + * The stepper orientation (layout flow direction) */ - stepHeadersWrapperStyle: PropTypes.object, - + orientation: PropTypes.oneOf(['horizontal', 'vertical']), /** * Override the inline-style of the root element. */ style: PropTypes.object, - - /** - * Callback function fired on re-render to set the background color of the icon. - * If not passed, it will use the default theme. - * - * @param {node} Step Component which is being updated. - * @returns {string} The background color of the icon. - */ - updateAvatarBackgroundColor: PropTypes.func, - - /** - * Callback function fired on re-render to update the completed status of the step. - * - * @param {number} stepIndex - The step that is being updated. - * @param {node} Step Component that is being updated. - * @returns {boolean} `true` if the step is completed. - */ - updateCompletedStatus: PropTypes.func, }; static defaultProps = { - activeStep: -1, - onStepHeaderTouch: () => {}, - updateAvatarBackgroundColor: () => null, - style: {}, - horizontal: false, - }; - - static contextTypes = { - muiTheme: PropTypes.object.isRequired, + orientation: 'horizontal', + activeStep: 0, + linear: true, }; - static childContextTypes = { - createIcon: PropTypes.func, - updateAvatarBackgroundColor: PropTypes.func, - }; + static contextTypes = {muiTheme: PropTypes.object.isRequired}; - state = { - hoveredHeaderStepIndex: -1, - itemWidth: 0, - }; + static childContextTypes = {stepper: PropTypes.object}; getChildContext() { - return { - createIcon: this.props.createIcon, - updateAvatarBackgroundColor: this.props.updateAvatarBackgroundColor, - }; + const {orientation} = this.props; + return {stepper: {orientation}}; } - componentWillReceiveProps(nextProps) { - if (!this.props.horizontal) { - return; - } - - const childrenWrapperNode = this.refs.childrenWrapper; - const containerWrapperNode = this.refs.containerWrapper; - const actionsNode = this.refs.actions; - - if (containerWrapperNode.style.height === '0px' && - nextProps.activeStep > -1) { - containerWrapperNode.style.height = `${(childrenWrapperNode.offsetHeight + - actionsNode.offsetHeight + 40)}px`; - childrenWrapperNode.style.transition = 'none'; - } else if (nextProps.activeStep > this.getTotalSteps() - 1) { - containerWrapperNode.style.height = '0px'; - } else { - childrenWrapperNode.style.transition = 'all 1s'; - } - } - - getTotalSteps() { - return React.Children.count(this.props.children); - } - - getStylesForHorizontalStepper() { + render() { const { - stepHeadersWrapperStyle, - containerStyle, - style, activeStep, + children, + linear, + style, } = this.props; - const itemWidth = this.state.itemWidth; - const translateX = -activeStep * 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', - }, - activeStep > -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, - activeStep, - updateCompletedStatus, - } = this.props; - - const {hoveredHeaderStepIndex} = this.state; - - const setOfChildren = []; - const setOfActions = []; + const {prepareStyles} = this.context.muiTheme; + const styles = getStyles(this.props, this.context); + /** + * One day, we may be able to use real CSS tools + * For now, we need to create our own "pseudo" elements + * and nth child selectors, etc + * That's what some of this garbage is for :) + */ const steps = React.Children.map(children, (step, index) => { - setOfChildren.push(step.props.children); - setOfActions.push(step.props.actions); - - return React.cloneElement(step, { - headerWidth: `${100 / this.getTotalSteps()}%`, - key: index, - stepIndex: index, - isActive: activeStep === index, - isStepHeaderHovered: hoveredHeaderStepIndex === index, - onStepHeaderTouch: onStepHeaderTouch, - onStepHeaderHover: this.handleHeaderStepHover, - isLastStep: index === (this.getTotalSteps() - 1), - isFirstStep: index === 0, - isCompleted: updateCompletedStatus(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, - }); - } - } + const controlProps = {index}; + + if (activeStep === index) { + controlProps.active = true; + } else if (linear && activeStep > index) { + controlProps.completed = true; + } else if (linear && activeStep < index) { + controlProps.disabled = true; } - > - - {steps} - - -
-
-
- {setOfChildren.map((children, index) => -
- {children} -
)} -
-
-
- {setOfActions[activeStep]} -
-
-
- ); - } - renderVerticalStepper() { - const { - style, - children, - onStepHeaderTouch, - activeStep, - updateCompletedStatus, - } = this.props; - - const {hoveredHeaderStepIndex} = this.state; + if (index + 1 === children.length) { + controlProps.last = true; + } - const steps = React.Children.map(children, (step, index) => { - return React.cloneElement(step, { - key: index, - stepIndex: index, - isActive: activeStep === index, - isStepHeaderHovered: hoveredHeaderStepIndex === index, - onStepHeaderTouch: onStepHeaderTouch, - onStepHeaderHover: this.handleHeaderStepHover, - isLastStep: index === (this.getTotalSteps() - 1), - isCompleted: updateCompletedStatus(index, step), - previousStepOptionalIndex: this.findFurthestOptionalStep(index), - }); + return [ + index > 0 && , + React.cloneElement(step, Object.assign(controlProps, step.props)), + ]; }); return ( -
+
{steps}
); } - - render() { - const {horizontal} = this.props; - - if (horizontal) { - return this.renderHorizontalStepper(); - } - - return this.renderVerticalStepper(); - } } export default Stepper; diff --git a/src/Stepper/Stepper.spec.js b/src/Stepper/Stepper.spec.js new file mode 100644 index 00000000000000..107cb3dd4c3ea3 --- /dev/null +++ b/src/Stepper/Stepper.spec.js @@ -0,0 +1,92 @@ +/* eslint-env mocha */ +import React from 'react'; +import {shallow} from 'enzyme'; +import {assert} from 'chai'; +import Stepper from './Stepper'; +import getMuiTheme from '../styles/getMuiTheme'; + +describe('', () => { + const muiTheme = getMuiTheme(); + const shallowWithContext = (node, context = {}) => { + return shallow(node, { + context: { + muiTheme, + ...context, + }, + }); + }; + + it('merges user styles into the root node', () => { + const wrapper = shallowWithContext( + + ); + + assert.strictEqual(wrapper.props().style.backgroundColor, 'purple', ); + }); + + describe('rendering children', () => { + const wrapper = shallowWithContext( + +
+
+
+ + ); + + const children = wrapper.children(); + + it('renders 3 children with connectors as separators', () => { + assert.strictEqual(children.length, 5); + assert.ok(wrapper.childAt(1).is('pure(StepConnector)')); + assert.ok(wrapper.childAt(3).is('pure(StepConnector)')); + }); + + assert.ok(true); + }); + + describe('controlling child props', () => { + it('controls children linearly based on the activeStep prop', () => { + const wrapper = shallowWithContext( + +
+
+
+ + ); + assert.ok(wrapper.find('.child-0').prop('active')); + assert.notOk(wrapper.find('.child-1').prop('active')); + assert.notOk(wrapper.find('.child-2').prop('active')); + assert.ok(wrapper.find('.child-1').prop('disabled')); + assert.ok(wrapper.find('.child-2').prop('disabled')); + wrapper.setProps({activeStep: 1}); + assert.ok(wrapper.find('.child-0').prop('completed')); + assert.notOk(wrapper.find('.child-0').prop('active')); + assert.ok(wrapper.find('.child-1').prop('active')); + assert.notOk(wrapper.find('.child-2').prop('active')); + assert.ok(wrapper.find('.child-2').prop('disabled')); + }); + + it('controls children non-linearly based on the activeStep prop', () => { + const wrapper = shallowWithContext( + +
+
+
+ + ); + assert.ok(wrapper.find('.child-0').prop('active')); + assert.notOk(wrapper.find('.child-1').prop('active')); + assert.notOk(wrapper.find('.child-2').prop('active')); + wrapper.setProps({activeStep: 1}); + assert.notOk(wrapper.find('.child-0').prop('active')); + assert.ok(wrapper.find('.child-1').prop('active')); + assert.notOk(wrapper.find('.child-2').prop('active')); + wrapper.setProps({activeStep: 2}); + assert.notOk(wrapper.find('.child-0').prop('active')); + assert.notOk(wrapper.find('.child-1').prop('active')); + assert.ok(wrapper.find('.child-2').prop('active')); + }); + }); +}); diff --git a/src/Stepper/VerticalStep.js b/src/Stepper/VerticalStep.js deleted file mode 100644 index b98864a016112e..00000000000000 --- a/src/Stepper/VerticalStep.js +++ /dev/null @@ -1,323 +0,0 @@ -import React, {PropTypes} from 'react'; -import TouchRipple from '../internal/TouchRipple'; -import Avatar from '../Avatar'; - -class Step extends React.Component { - static propTypes = { - /** - * An array of nodes for handling moving or canceling steps. - */ - actions: PropTypes.arrayOf(PropTypes.node), - - /** - * Override the inline-style of the div which contains the actions. - */ - actionsWrapperStyle: PropTypes.object, - - children: PropTypes.node, - - /** - * Override the inline-style of the div which contains all the children, including control button groups. - */ - childrenWrapperStyle: PropTypes.object, - - /** - * Override the inline-style of the connector line. - */ - connectorLineStyle: PropTypes.object, - - /** - * @ignore - * If true, the step is active. - */ - isActive: PropTypes.bool, - - /** - * @ignore - * If true, the step is completed. - */ - isCompleted: PropTypes.bool, - - /** - * @ignore - * If true, the step is the last one. - */ - isLastStep: PropTypes.bool, - - /** - * @ignore - * If true, the header of step is hovered. - */ - isStepHeaderHovered: PropTypes.bool, - - /** - * @ignore - * Callback function fired when the header of step is hovered. - */ - onStepHeaderHover: PropTypes.func, - - /** - * @ignore - * Callback function fired when the header of step is touched. - */ - onStepHeaderTouch: PropTypes.func, - - /** - * @ignore - * The index of the furthest optional step. - */ - previousStepOptionalIndex: PropTypes.number, - - /** - * Override the inline-style of step container, which contains connector line and children. - */ - stepContainerStyle: PropTypes.object, - - /** - * Override the inline-style of step header (not including left avatar). - */ - stepHeaderStyle: PropTypes.object, - - /** - * Override the inline-style of step header wrapper, including left avatar. - */ - stepHeaderWrapperStyle: PropTypes.object, - - /** - * @ignore - * The index of step in array of Steps. - */ - stepIndex: PropTypes.number, - - /** - * Customize the step label. - */ - stepLabel: PropTypes.node, - }; - - static contextTypes = { - muiTheme: PropTypes.object.isRequired, - createIcon: PropTypes.func, - updateAvatarBackgroundColor: PropTypes.func, - }; - - 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, - actionsWrapperStyle, - childrenWrapperStyle, - } = this.props; - - const theme = this.context.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 actionsWrapper = Object.assign({ - marginTop: 16, - }, actionsWrapperStyle); - - 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, - actionsWrapper: actionsWrapper, - childrenWrapper: childrenWrapper, - stepHeader: stepHeader, - }; - } - - render() { - const { - children, - stepLabel, - actions, - isLastStep, - } = this.props; - - const styles = this.getStyles(); - - const icon = this.context.createIcon(this); - - const avatarView = ; - - return ( -
-
- -
- {avatarView} - {stepLabel} -
-
-
-
- {!isLastStep &&
} - {
-
-
- {children} -
-
- {actions} -
-
-
- } -
-
- ); - } -} - -export default Step; diff --git a/src/Stepper/index.js b/src/Stepper/index.js index ad35c4b1c056f7..f8a5ce9e2db2c0 100644 --- a/src/Stepper/index.js +++ b/src/Stepper/index.js @@ -1,5 +1,5 @@ +export Step from './Step'; +export StepButton from './StepButton'; +export StepContent from './StepContent'; +export StepLabel from './StepLabel'; export Stepper from './Stepper'; -export VerticalStep from './VerticalStep'; -export HorizontalStep from './HorizontalStep'; - -export default from './Stepper'; diff --git a/src/SvgIcon/SvgIcon.js b/src/SvgIcon/SvgIcon.js index ecb4f65ae08936..da33cd1c744187 100644 --- a/src/SvgIcon/SvgIcon.js +++ b/src/SvgIcon/SvgIcon.js @@ -2,6 +2,8 @@ import React from 'react'; import transitions from '../styles/transitions'; class SvgIcon extends React.Component { + static muiName = 'SvgIcon'; + static propTypes = { /** * Elements passed into the SVG Icon. diff --git a/src/index.js b/src/index.js index baf7631bf963a8..97ac71054d73d6 100644 --- a/src/index.js +++ b/src/index.js @@ -20,7 +20,6 @@ export FloatingActionButton from './FloatingActionButton'; export FontIcon from './FontIcon'; export GridList from './GridList'; export GridTile from './GridList/GridTile'; -export HorizontalStep from './Stepper/HorizontalStep'; export IconButton from './IconButton'; export IconMenu from './IconMenu'; export LinearProgress from './LinearProgress'; @@ -39,7 +38,11 @@ export SelectField from './SelectField'; export Slider from './Slider'; export Subheader from './Subheader'; export SvgIcon from './SvgIcon'; -export Stepper from './Stepper'; +export Step from './Stepper/Step'; +export StepButton from './Stepper/StepButton'; +export StepContent from './Stepper/StepContent'; +export StepLabel from './Stepper/StepLabel'; +export Stepper from './Stepper/Stepper'; export Snackbar from './Snackbar'; export Tabs from './Tabs'; export Tab from './Tabs/Tab'; @@ -57,4 +60,3 @@ export Toolbar from './Toolbar'; export ToolbarGroup from './Toolbar/ToolbarGroup'; export ToolbarSeparator from './Toolbar/ToolbarSeparator'; export ToolbarTitle from './Toolbar/ToolbarTitle'; -export VerticalStep from './Stepper/VerticalStep'; diff --git a/src/internal/ExpandTransition.js b/src/internal/ExpandTransition.js new file mode 100644 index 00000000000000..99a3a4185a062b --- /dev/null +++ b/src/internal/ExpandTransition.js @@ -0,0 +1,72 @@ +import React, {PropTypes} from 'react'; +import ReactTransitionGroup from 'react-addons-transition-group'; +import ExpandTransitionChild from './ExpandTransitionChild'; + +class ExpandTransition extends React.Component { + static propTypes = { + children: PropTypes.node, + enterDelay: PropTypes.number, + loading: PropTypes.bool, + open: PropTypes.bool, + style: PropTypes.object, + }; + + static defaultProps = { + enterDelay: 0, + loading: false, + open: false, + }; + + static contextTypes = { + muiTheme: React.PropTypes.object.isRequired, + }; + + renderChildren(children, loading) { + if (loading) { + return ([]); + } + + return React.Children.map(children, (child) => { + return ( + + {child} + + ); + }, this); + } + + render() { + const { + children, + loading, + open, + style, + ...other, + } = this.props; + + const {prepareStyles} = this.context.muiTheme; + + const mergedRootStyles = Object.assign({}, { + position: 'relative', + overflow: 'hidden', + height: '100%', + }, style); + + const newChildren = this.renderChildren(children, loading); + + return ( + + {open && newChildren} + + ); + } +} + +export default ExpandTransition; diff --git a/src/internal/ExpandTransitionChild.js b/src/internal/ExpandTransitionChild.js new file mode 100644 index 00000000000000..4ca669cf285746 --- /dev/null +++ b/src/internal/ExpandTransitionChild.js @@ -0,0 +1,90 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import transitions from '../styles/transitions'; + +class ExpandTransitionChild extends React.Component { + static propTypes = { + children: React.PropTypes.node, + enterDelay: React.PropTypes.number, + style: React.PropTypes.object, + }; + + static defaultProps = { + enterDelay: 0, + }; + + static contextTypes = { + muiTheme: React.PropTypes.object.isRequired, + }; + + componentDidUpdate() { + this.open(); + } + + componentWillUnmount() { + clearTimeout(this.enterTimer); + clearTimeout(this.leaveTimer); + } + + componentWillAppear(callback) { + this.open(); + callback(); + } + + componentWillEnter(callback) { + const {enterDelay} = this.props; + const {style} = ReactDOM.findDOMNode(this); + style.height = 0; + + if (enterDelay) { + this.enterTimer = setTimeout(() => callback(), 450); + return; + } + + callback(); + } + + componentDidEnter() { + this.open(); + } + + componentWillLeave(callback) { + const style = ReactDOM.findDOMNode(this).style; + style.height = this.refs.wrapper.clientHeight; + style.height = 0; + this.leaveTimer = setTimeout(() => callback(), 450); + } + + open() { + const style = ReactDOM.findDOMNode(this).style; + style.height = `${this.refs.wrapper.clientHeight}px`; + } + + render() { + const { + children, + style, + ...other, + } = this.props; + + const {prepareStyles} = this.context.muiTheme; + + const mergedRootStyles = Object.assign({ + position: 'relative', + height: 0, + width: '100%', + top: 0, + left: 0, + overflow: 'hidden', + transition: transitions.easeOut(null, ['height', 'opacity']), + }, style); + + return ( +
+
{children}
+
+ ); + } +} + +export default ExpandTransitionChild; diff --git a/src/styles/getMuiTheme.js b/src/styles/getMuiTheme.js index 1dfc2d6d8fe597..f017a9cca0cf31 100644 --- a/src/styles/getMuiTheme.js +++ b/src/styles/getMuiTheme.js @@ -127,6 +127,10 @@ export default function getMuiTheme(muiTheme, ...more) { gridTile: { textColor: white, }, + icon: { + color: palette.canvasColor, + backgroundColor: palette.primary1Color, + }, inkBar: { backgroundColor: palette.accent1Color, }, @@ -224,18 +228,14 @@ export default function getMuiTheme(muiTheme, ...more) { 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), - + backgroundColor: 'transparent', + hoverBackgroundColor: ColorManipulator.fade(black, 0.06), + iconColor: palette.primary1Color, + hoveredIconColor: grey700, + inactiveIconColor: grey500, + textColor: ColorManipulator.fade(black, 0.87), + disabledTextColor: ColorManipulator.fade(black, 0.26), connectorLineColor: grey400, - avatarSize: 24, }, table: { backgroundColor: palette.canvasColor,