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 classnames from 'classnames';
/**
* WooCommerce dependencies
*/
import { updateQueryString } from '@woocommerce/navigation';
/**
* Internal dependencies
*/
@ -14,9 +19,26 @@ import HeaderLogo from './header-logo';
export default class ProfileWizardHeader extends Component {
renderStepper() {
const steps = filter( this.props.steps, step => !! step.label );
return <Stepper steps={ steps } currentStep={ this.props.currentStep } />;
const { currentStep, steps } = this.props;
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() {
const currentStep = this.props.steps.find( s => s.key === this.props.currentStep );
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 './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 {
constructor() {
super( ...arguments );
@ -94,12 +56,61 @@ class ProfileWizard extends Component {
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() {
const { step } = this.props.query;
const currentStep = getSteps().find( s => s.key === step );
const currentStep = this.getSteps().find( s => s.key === step );
if ( ! currentStep ) {
return getSteps()[ 0 ];
return this.getSteps()[ 0 ];
}
return currentStep;
@ -108,8 +119,8 @@ class ProfileWizard extends Component {
async goToNextStep() {
const { createNotice, isError, updateProfileItems } = this.props;
const currentStep = this.getCurrentStep();
const currentStepIndex = getSteps().findIndex( s => s.key === currentStep.key );
const nextStep = getSteps()[ currentStepIndex + 1 ];
const currentStepIndex = this.getSteps().findIndex( s => s.key === currentStep.key );
const nextStep = this.getSteps()[ currentStepIndex + 1 ];
if ( 'undefined' === typeof nextStep ) {
await updateProfileItems( { completed: true } );
@ -135,7 +146,7 @@ class ProfileWizard extends Component {
step,
goToNextStep: this.goToNextStep,
} );
const steps = getSteps().map( _step => pick( _step, [ 'key', 'label' ] ) );
const steps = this.getSteps().map( _step => pick( _step, [ 'key', 'label', 'isComplete' ] ) );
return (
<Fragment>
@ -148,11 +159,12 @@ class ProfileWizard extends Component {
export default compose(
withSelect( select => {
const { getProfileItemsError } = select( 'wc-api' );
const { getProfileItems, getProfileItemsError } = select( 'wc-api' );
const isError = Boolean( getProfileItemsError() );
return { isError };
return {
isError: Boolean( getProfileItemsError() ),
profileItems: getProfileItems(),
};
} ),
withDispatch( dispatch => {
const { updateProfileItems } = dispatch( 'wc-api' );

View File

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

View File

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

View File

@ -1,3 +1,5 @@
/** @format */
.woocommerce-stepper {
$step-icon-size: 24px;
@ -8,11 +10,27 @@
}
.woocommerce-stepper__step {
display: inline-flex;
padding: $gap-smaller;
font-weight: 400;
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 {
width: 100%;
}
@ -39,7 +57,7 @@
display: block;
margin-right: $gap-small;
max-height: $step-icon-size;
min-width: auto;
min-width: 24px;
width: 24px;
border-radius: 50%;
background: $studio-blue-50;
@ -127,7 +145,7 @@
content: '';
position: absolute;
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 });
border-left: 1px solid $studio-gray-5;
}
@ -153,6 +171,7 @@
.woocommerce-stepper_content {
margin-top: $gap-smaller;
margin-left: $gap-small + $step-icon-size;
}
}
}