From 4f79cdea930a78fc7ecbe77d68a071a2e1e5a9df Mon Sep 17 00:00:00 2001 From: Joshua T Flowers Date: Wed, 6 Nov 2019 08:26:08 +0800 Subject: [PATCH] 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 --- .../client/dashboard/profile-wizard/header.js | 26 ++++- .../client/dashboard/profile-wizard/index.js | 106 ++++++++++-------- .../components/src/stepper/docs/example.js | 90 +++++++-------- .../packages/components/src/stepper/index.js | 80 +++++++------ .../components/src/stepper/style.scss | 25 ++++- 5 files changed, 193 insertions(+), 134 deletions(-) diff --git a/plugins/woocommerce-admin/client/dashboard/profile-wizard/header.js b/plugins/woocommerce-admin/client/dashboard/profile-wizard/header.js index 001f778f96f..5423044b327 100644 --- a/plugins/woocommerce-admin/client/dashboard/profile-wizard/header.js +++ b/plugins/woocommerce-admin/client/dashboard/profile-wizard/header.js @@ -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 ; + 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 ; } + render() { const currentStep = this.props.steps.find( s => s.key === this.props.currentStep ); const showStepper = ! currentStep || ! currentStep.label ? false : true; diff --git a/plugins/woocommerce-admin/client/dashboard/profile-wizard/index.js b/plugins/woocommerce-admin/client/dashboard/profile-wizard/index.js index c53db166098..6b0c3e198cd 100644 --- a/plugins/woocommerce-admin/client/dashboard/profile-wizard/index.js +++ b/plugins/woocommerce-admin/client/dashboard/profile-wizard/index.js @@ -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 ( @@ -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' ); diff --git a/plugins/woocommerce-admin/packages/components/src/stepper/docs/example.js b/plugins/woocommerce-admin/packages/components/src/stepper/docs/example.js index 8101fa9244e..83931eb6429 100644 --- a/plugins/woocommerce-admin/packages/components/src/stepper/docs/example.js +++ b/plugins/woocommerce-admin/packages/components/src/stepper/docs/example.js @@ -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:
First step content.
, + onClick: goToStep, }, { key: 'second', label: 'Second', description: 'Step item description', content:
Second step content.
, + onClick: goToStep, }, { label: 'Third', key: 'third', description: 'Step item description', content:
Third step content.
, + onClick: goToStep, }, { label: 'Fourth', key: 'fourth', description: 'Step item description', content:
Fourth step content.
, + 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 ( -
- { isComplete ? ( - - ) : ( -
- - - - -
- ) } + ) : ( +
+ + + + +
+ ) } - + -
+
- -
+ + ); } ); diff --git a/plugins/woocommerce-admin/packages/components/src/stepper/index.js b/plugins/woocommerce-admin/packages/components/src/stepper/index.js index 689a44d2307..7b0249abf5b 100644 --- a/plugins/woocommerce-admin/packages/components/src/stepper/index.js +++ b/plugins/woocommerce-admin/packages/components/src/stepper/index.js @@ -24,11 +24,7 @@ class Stepper extends Component { return null; } - return ( -
- { step.content } -
- ); + return
{ step.content }
; } render() { @@ -42,39 +38,45 @@ class Stepper extends Component {
{ 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 ? : ( -
- { i + 1 } - -
- ); + const icon = + isCurrentStep && isPending ? ( + + ) : ( +
+ { i + 1 } + +
+ ); + + const LabelWrapper = 'function' === typeof onClick ? 'button' : 'div'; return ( - -
- { icon } -
- - { label } - - { description && - - { description } - - } - { isCurrentStep && isVertical && this.renderCurrentStepContent() } -
+ +
+ onClick( key ) : null } + > + { icon } +
+ { label } + { description && ( + + { description } + + ) } +
+
+ { isCurrentStep && isVertical && this.renderCurrentStepContent() }
- { ! isVertical &&
} + { ! isVertical &&
} ); } ) } @@ -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, diff --git a/plugins/woocommerce-admin/packages/components/src/stepper/style.scss b/plugins/woocommerce-admin/packages/components/src/stepper/style.scss index 178c4ef9bde..971e9cc5e4e 100644 --- a/plugins/woocommerce-admin/packages/components/src/stepper/style.scss +++ b/plugins/woocommerce-admin/packages/components/src/stepper/style.scss @@ -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; } } }