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:
parent
e1e64b9241
commit
4f79cdea93
|
@ -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;
|
||||
|
|
|
@ -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' );
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
} );
|
||||
|
|
|
@ -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,
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue