Onboarding: Allow users to navigate backwards in steps (https://github.com/woocommerce/woocommerce-admin/pull/3154)

* Add label wrapper to stepper and style for both orientations

* Add onClick event to steps

* Add onClick to stepper examples

* Allow returning to previous steps on step click

* Allow navigating to previously completed steps

* Mark previous steps complete
This commit is contained in:
Joshua T Flowers 2019-11-06 08:26:08 +08:00 committed by GitHub
parent e1e64b9241
commit 4f79cdea93
5 changed files with 193 additions and 134 deletions

View File

@ -6,6 +6,11 @@ import { Component } from '@wordpress/element';
import { filter } from 'lodash'; import { filter } from 'lodash';
import classnames from 'classnames'; import classnames from 'classnames';
/**
* WooCommerce dependencies
*/
import { updateQueryString } from '@woocommerce/navigation';
/** /**
* Internal dependencies * Internal dependencies
*/ */
@ -14,9 +19,26 @@ import HeaderLogo from './header-logo';
export default class ProfileWizardHeader extends Component { export default class ProfileWizardHeader extends Component {
renderStepper() { renderStepper() {
const steps = filter( this.props.steps, step => !! step.label ); const { currentStep, steps } = this.props;
return <Stepper steps={ steps } currentStep={ this.props.currentStep } />; const visibleSteps = filter( steps, step => !! step.label );
const currentStepIndex = visibleSteps.findIndex( step => step.key === currentStep );
visibleSteps.map( ( step, index ) => {
const previousStep = visibleSteps[ index - 1 ];
if ( index < currentStepIndex ) {
step.isComplete = true;
}
if ( ! previousStep || previousStep.isComplete ) {
step.onClick = key => updateQueryString( { step: key } );
}
return step;
} );
return <Stepper steps={ visibleSteps } currentStep={ currentStep } />;
} }
render() { render() {
const currentStep = this.props.steps.find( s => s.key === this.props.currentStep ); const currentStep = this.props.steps.find( s => s.key === this.props.currentStep );
const showStepper = ! currentStep || ! currentStep.label ? false : true; const showStepper = ! currentStep || ! currentStep.label ? false : true;

View File

@ -27,44 +27,6 @@ import Theme from './steps/theme';
import withSelect from 'wc-api/with-select'; import withSelect from 'wc-api/with-select';
import './style.scss'; import './style.scss';
const getSteps = () => {
const steps = [];
steps.push( {
key: 'start',
container: Start,
} );
steps.push( {
key: 'plugins',
container: Plugins,
} );
steps.push( {
key: 'store-details',
container: StoreDetails,
label: __( 'Store Details', 'woocommerce-admin' ),
} );
steps.push( {
key: 'industry',
container: Industry,
label: __( 'Industry', 'woocommerce-admin' ),
} );
steps.push( {
key: 'product-types',
container: ProductTypes,
label: __( 'Product Types', 'woocommerce-admin' ),
} );
steps.push( {
key: 'business-details',
container: BusinessDetails,
label: __( 'Business Details', 'woocommerce-admin' ),
} );
steps.push( {
key: 'theme',
container: Theme,
label: __( 'Theme', 'woocommerce-admin' ),
} );
return steps;
};
class ProfileWizard extends Component { class ProfileWizard extends Component {
constructor() { constructor() {
super( ...arguments ); super( ...arguments );
@ -94,12 +56,61 @@ class ProfileWizard extends Component {
document.body.classList.remove( 'woocommerce-admin-full-screen' ); document.body.classList.remove( 'woocommerce-admin-full-screen' );
} }
getSteps() {
const { profileItems } = this.props;
const steps = [];
steps.push( {
key: 'start',
container: Start,
} );
steps.push( {
key: 'plugins',
container: Plugins,
isComplete: profileItems.hasOwnProperty( 'plugins' ) && null !== profileItems.plugins,
} );
steps.push( {
key: 'store-details',
container: StoreDetails,
label: __( 'Store Details', 'woocommerce-admin' ),
isComplete:
profileItems.hasOwnProperty( 'setup_client' ) && null !== profileItems.setup_client,
} );
steps.push( {
key: 'industry',
container: Industry,
label: __( 'Industry', 'woocommerce-admin' ),
isComplete: profileItems.hasOwnProperty( 'industry' ) && null !== profileItems.industry,
} );
steps.push( {
key: 'product-types',
container: ProductTypes,
label: __( 'Product Types', 'woocommerce-admin' ),
isComplete:
profileItems.hasOwnProperty( 'product_types' ) && null !== profileItems.product_types,
} );
steps.push( {
key: 'business-details',
container: BusinessDetails,
label: __( 'Business Details', 'woocommerce-admin' ),
isComplete:
profileItems.hasOwnProperty( 'product_count' ) && null !== profileItems.product_count,
} );
steps.push( {
key: 'theme',
container: Theme,
label: __( 'Theme', 'woocommerce-admin' ),
isComplete: profileItems.hasOwnProperty( 'theme' ) && null !== profileItems.theme,
} );
return steps;
}
getCurrentStep() { getCurrentStep() {
const { step } = this.props.query; const { step } = this.props.query;
const currentStep = getSteps().find( s => s.key === step ); const currentStep = this.getSteps().find( s => s.key === step );
if ( ! currentStep ) { if ( ! currentStep ) {
return getSteps()[ 0 ]; return this.getSteps()[ 0 ];
} }
return currentStep; return currentStep;
@ -108,8 +119,8 @@ class ProfileWizard extends Component {
async goToNextStep() { async goToNextStep() {
const { createNotice, isError, updateProfileItems } = this.props; const { createNotice, isError, updateProfileItems } = this.props;
const currentStep = this.getCurrentStep(); const currentStep = this.getCurrentStep();
const currentStepIndex = getSteps().findIndex( s => s.key === currentStep.key ); const currentStepIndex = this.getSteps().findIndex( s => s.key === currentStep.key );
const nextStep = getSteps()[ currentStepIndex + 1 ]; const nextStep = this.getSteps()[ currentStepIndex + 1 ];
if ( 'undefined' === typeof nextStep ) { if ( 'undefined' === typeof nextStep ) {
await updateProfileItems( { completed: true } ); await updateProfileItems( { completed: true } );
@ -135,7 +146,7 @@ class ProfileWizard extends Component {
step, step,
goToNextStep: this.goToNextStep, goToNextStep: this.goToNextStep,
} ); } );
const steps = getSteps().map( _step => pick( _step, [ 'key', 'label' ] ) ); const steps = this.getSteps().map( _step => pick( _step, [ 'key', 'label', 'isComplete' ] ) );
return ( return (
<Fragment> <Fragment>
@ -148,11 +159,12 @@ class ProfileWizard extends Component {
export default compose( export default compose(
withSelect( select => { withSelect( select => {
const { getProfileItemsError } = select( 'wc-api' ); const { getProfileItems, getProfileItemsError } = select( 'wc-api' );
const isError = Boolean( getProfileItemsError() ); return {
isError: Boolean( getProfileItemsError() ),
return { isError }; profileItems: getProfileItems(),
};
} ), } ),
withDispatch( dispatch => { withDispatch( dispatch => {
const { updateProfileItems } = dispatch( 'wc-api' ); const { updateProfileItems } = dispatch( 'wc-api' );

View File

@ -14,87 +14,87 @@ export default withState( {
isComplete: false, isComplete: false,
isPending: false, isPending: false,
} )( ( { currentStep, isComplete, isPending, setState } ) => { } )( ( { currentStep, isComplete, isPending, setState } ) => {
const goToStep = key => {
setState( { currentStep: key } );
};
const steps = [ const steps = [
{ {
key: 'first', key: 'first',
label: 'First', label: 'First',
description: 'Step item description', description: 'Step item description',
content: <div>First step content.</div>, content: <div>First step content.</div>,
onClick: goToStep,
}, },
{ {
key: 'second', key: 'second',
label: 'Second', label: 'Second',
description: 'Step item description', description: 'Step item description',
content: <div>Second step content.</div>, content: <div>Second step content.</div>,
onClick: goToStep,
}, },
{ {
label: 'Third', label: 'Third',
key: 'third', key: 'third',
description: 'Step item description', description: 'Step item description',
content: <div>Third step content.</div>, content: <div>Third step content.</div>,
onClick: goToStep,
}, },
{ {
label: 'Fourth', label: 'Fourth',
key: 'fourth', key: 'fourth',
description: 'Step item description', description: 'Step item description',
content: <div>Fourth step content.</div>, content: <div>Fourth step content.</div>,
onClick: goToStep,
}, },
]; ];
const currentIndex = steps.findIndex( s => currentStep === s.key ); const currentIndex = steps.findIndex( s => currentStep === s.key );
if ( isComplete ) { if ( isComplete ) {
steps.forEach( s => s.isComplete = true ); steps.forEach( s => ( s.isComplete = true ) );
} }
return ( return (
<div> <div>
{ isComplete ? ( { isComplete ? (
<button onClick={ () => setState( { currentStep: 'first', isComplete: false } ) } > <button onClick={ () => setState( { currentStep: 'first', isComplete: false } ) }>
Reset Reset
</button>
) : (
<div>
<button
onClick={ () => setState( { currentStep: steps[ currentIndex - 1 ].key } ) }
disabled={ currentIndex < 1 }
>
Previous step
</button> </button>
<button ) : (
onClick={ () => setState( { currentStep: steps[ currentIndex + 1 ].key } ) } <div>
disabled={ currentIndex >= steps.length - 1 } <button
> onClick={ () => setState( { currentStep: steps[ currentIndex - 1 ].key } ) }
Next step disabled={ currentIndex < 1 }
</button> >
<button Previous step
onClick={ () => setState( { isComplete: true } ) } </button>
disabled={ currentIndex !== steps.length - 1 } <button
> onClick={ () => setState( { currentStep: steps[ currentIndex + 1 ].key } ) }
Complete disabled={ currentIndex >= steps.length - 1 }
</button> >
<button Next step
onClick={ () => setState( { isPending: ! isPending } ) } </button>
> <button
Toggle Spinner onClick={ () => setState( { isComplete: true } ) }
</button> disabled={ currentIndex !== steps.length - 1 }
</div> >
) } Complete
</button>
<button onClick={ () => setState( { isPending: ! isPending } ) }>Toggle Spinner</button>
</div>
) }
<Stepper <Stepper steps={ steps } currentStep={ currentStep } isPending={ isPending } />
steps={ steps }
currentStep={ currentStep }
isPending={ isPending }
/>
<br /> <br />
<Stepper <Stepper
isPending={ isPending } isPending={ isPending }
isVertical={ true } isVertical={ true }
steps={ steps } steps={ steps }
currentStep={ currentStep } currentStep={ currentStep }
/> />
</div> </div>
); );
} ); } );

View File

@ -24,11 +24,7 @@ class Stepper extends Component {
return null; return null;
} }
return ( return <div className="woocommerce-stepper_content">{ step.content }</div>;
<div className="woocommerce-stepper_content">
{ step.content }
</div>
);
} }
render() { render() {
@ -42,39 +38,45 @@ class Stepper extends Component {
<div className={ stepperClassName }> <div className={ stepperClassName }>
<div className="woocommerce-stepper__steps"> <div className="woocommerce-stepper__steps">
{ steps.map( ( step, i ) => { { steps.map( ( step, i ) => {
const { key, label, description, isComplete } = step; const { key, label, description, isComplete, onClick } = step;
const isCurrentStep = key === currentStep; const isCurrentStep = key === currentStep;
const stepClassName = classnames( 'woocommerce-stepper__step', { const stepClassName = classnames( 'woocommerce-stepper__step', {
'is-active': isCurrentStep, 'is-active': isCurrentStep,
'is-complete': 'undefined' !== typeof isComplete ? isComplete : currentIndex > i, 'is-complete': 'undefined' !== typeof isComplete ? isComplete : currentIndex > i,
} ); } );
const icon = isCurrentStep && isPending ? <Spinner /> : ( const icon =
<div className="woocommerce-stepper__step-icon"> isCurrentStep && isPending ? (
<span className="woocommerce-stepper__step-number">{ i + 1 }</span> <Spinner />
<CheckIcon /> ) : (
</div> <div className="woocommerce-stepper__step-icon">
); <span className="woocommerce-stepper__step-number">{ i + 1 }</span>
<CheckIcon />
</div>
);
const LabelWrapper = 'function' === typeof onClick ? 'button' : 'div';
return ( return (
<Fragment key={ key } > <Fragment key={ key }>
<div <div className={ stepClassName }>
className={ stepClassName } <LabelWrapper
> className="woocommerce-stepper__step-label-wrapper"
{ icon } onClick={ 'function' === typeof onClick ? () => onClick( key ) : null }
<div className="woocommerce-stepper__step-text"> >
<span className="woocommerce-stepper__step-label"> { icon }
{ label } <div className="woocommerce-stepper__step-text">
</span> <span className="woocommerce-stepper__step-label">{ label }</span>
{ description && { description && (
<span className="woocommerce-stepper__step-description"> <span className="woocommerce-stepper__step-description">
{ description } { description }
</span> </span>
} ) }
{ isCurrentStep && isVertical && this.renderCurrentStepContent() } </div>
</div> </LabelWrapper>
{ isCurrentStep && isVertical && this.renderCurrentStepContent() }
</div> </div>
{ ! isVertical && <div className="woocommerce-stepper__step-divider" /> } { ! isVertical && <div className="woocommerce-stepper__step-divider" /> }
</Fragment> </Fragment>
); );
} ) } } ) }
@ -101,13 +103,9 @@ Stepper.propTypes = {
steps: PropTypes.arrayOf( steps: PropTypes.arrayOf(
PropTypes.shape( { PropTypes.shape( {
/** /**
* Key used to identify step. * Content displayed when the step is active.
*/ */
key: PropTypes.string.isRequired, content: PropTypes.node,
/**
* Label displayed in stepper.
*/
label: PropTypes.string.isRequired,
/** /**
* Description displayed beneath the label. * Description displayed beneath the label.
*/ */
@ -117,9 +115,17 @@ Stepper.propTypes = {
*/ */
isComplete: PropTypes.bool, isComplete: PropTypes.bool,
/** /**
* Content displayed when the step is active. * Key used to identify step.
*/ */
content: PropTypes.node, key: PropTypes.string.isRequired,
/**
* Label displayed in stepper.
*/
label: PropTypes.string.isRequired,
/**
* A function to be called when the step label is clicked.
*/
onClick: PropTypes.func,
} ) } )
).isRequired, ).isRequired,

View File

@ -1,3 +1,5 @@
/** @format */
.woocommerce-stepper { .woocommerce-stepper {
$step-icon-size: 24px; $step-icon-size: 24px;
@ -8,11 +10,27 @@
} }
.woocommerce-stepper__step { .woocommerce-stepper__step {
display: inline-flex;
padding: $gap-smaller; padding: $gap-smaller;
font-weight: 400; font-weight: 400;
position: relative; position: relative;
.woocommerce-stepper__step-label-wrapper {
display: flex;
text-align: left;
border: 0;
background-color: transparent;
padding: 0;
&:focus {
outline: none;
box-shadow: none;
}
}
button.woocommerce-stepper__step-label-wrapper {
cursor: pointer;
}
.woocommerce-stepper__step-text { .woocommerce-stepper__step-text {
width: 100%; width: 100%;
} }
@ -39,7 +57,7 @@
display: block; display: block;
margin-right: $gap-small; margin-right: $gap-small;
max-height: $step-icon-size; max-height: $step-icon-size;
min-width: auto; min-width: 24px;
width: 24px; width: 24px;
border-radius: 50%; border-radius: 50%;
background: $studio-blue-50; background: $studio-blue-50;
@ -127,7 +145,7 @@
content: ''; content: '';
position: absolute; position: absolute;
left: $step-icon-size / 2 + $gap-smaller; left: $step-icon-size / 2 + $gap-smaller;
top: $step-icon-size + ( $gap-smaller * 2 ); top: $step-icon-size + ($gap-smaller * 2);
height: calc(100% - #{$step-icon-size} - #{ $gap-smaller * 2 }); height: calc(100% - #{$step-icon-size} - #{ $gap-smaller * 2 });
border-left: 1px solid $studio-gray-5; border-left: 1px solid $studio-gray-5;
} }
@ -153,6 +171,7 @@
.woocommerce-stepper_content { .woocommerce-stepper_content {
margin-top: $gap-smaller; margin-top: $gap-smaller;
margin-left: $gap-small + $step-icon-size;
} }
} }
} }