/**
 * Copyright (c) 2018 Dell Inc., or its subsidiaries. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 */ import * as React from "react" ; import * as ReactDOM from "react-dom" ; import { Icon , IconProps } from "../icon" ; import { classNames , allTrue } from "../utils" ; import { ClassNames , Styles } from "./ClassNames" ; import { Button , ButtonState } from "../forms/button" ; import { VerticalNav } from "../layout/vertical-nav" ; import { ReactNode } from "react" ; /**
 * General component description :
 * Wizard :
 * Wizards are used to help users walk through a defined step-by-step process.
 * Each step is in the sidebar on the left. As users complete steps,
 * the steps are marked with a green bar to the left.
 */ param { stepName } Name param { stepComponent } React param { stepId } unique param { showStepTitle } if param { stepCompleted } flag param { customStepNav } custom param { disableNav } if param { stepFooter } custom param { isStepValid } function param { onStepSubmit } function type WizardStep = { stepName : string ; stepComponent : React . ReactNode ; stepId : number ; showStepTitle ? : boolean ; stepCompleted ? : boolean ; customStepNav ? : WizardStepNavDetails ; disableNav ? : boolean ; stepFooter ? : React . ReactElement ; isStepValid ? : Function ; onStepSubmit ? : ( ) => Promise < any > ; } ; type WizardStepNavDetails = { stepNavIcon ? : string ; stepNavTitle ? : string ; stepNavChildren ? : React . ReactNode ; onNavClick ? : ( ) => Promise < any > ; } ; param { isInline } if param { style } CSS param { className } CSS param { show } if param { size } enum param { title } wizard param { steps } wizard param { defaultStepId } on param { showNav } if param { closable } if param { onClose } callback param { previousButtonText } custom param { showPreviousButton } if param { onPrevious } callback param { previousButtonClassName } extranla param { nextButtonText } custom param { onNext } callback param { nextButtonClassName } extranla param { finishButtonText } custom param { onFinish } callback param { finishButtonClassName } extranla param { cancelButtonText } custom param { showCancelButton } if param { cancelButtonClassName } extranla param { customFooter } custom param { navLinkClasses } extranal param { validationType } validation param { dataqa } Quality type WizardProps = { isInline ? : boolean ; show ? : boolean ; size ? : WizardSize ; title ? : any ; steps : WizardStep [ ] ; defaultStepId ? : number ; showNav ? : boolean ; closable ? : boolean ; previousButtonText ? : string ; showPreviousButton ? : boolean ; onPrevious ? : ( ) => Promise < any > ; previousButtonClassName ? : string ; nextButtonText ? : string ; onNext ? : ( ) => Promise < any > ; nextButtonClassName ? : string ; finishButtonText ? : string ; onFinish ? : Function ; finishButtonClassName ? : string ; cancelButtonText ? : string ; showCancelButton ? : boolean ; cancelButtonClassName ? : string ; onClose ? : Function ; customFooter ? : React . ReactElement ; navLinkClasses ? : string ; validationType ? : WizardValidationType ; style ? : any ; className ? : string ; dataqa ? : string ; } ; param { show } if param { currentStepId } ID param { showFinishButton } if param { showPreviousButton } if param { showNextButton } if param { showCancelButton } if param { disableFinishButton } label param { allSteps } wizard type WizardState = { show : boolean ; currentStepId : number ; showFinishButton : boolean ; disableFinishButton : boolean ; showPreviousButton : boolean ; showNextButton : boolean ; showCancelButton : boolean ; disableNextButton : boolean ; allSteps : WizardStep [ ] ; } ; param { MEDIUM } midum param { LARGE } large param { XLARGE } xtra-large export enum WizardSize { MEDIUM = "md" , LARGE = "lg" , XLARGE = "xl" } param { ASYNC } Asynchronous param { SYNC } Synchronous param { NONE } no export enum WizardValidationType { ASYNC = "Asynchronous" , SYNC = "Synchronous" , NONE = "None" } let dataqa_wizard = "prefix" ; let dataqa_wizard_btn_cancel = "prefix_btn_cancel" ; let dataqa_wizard_btn_previous = "prefix_btn_back" ; let dataqa_wizard_btn_next = "prefix_btn_next" ; let dataqa_wizard_btn_finish = "prefix_btn_finish" ; export class Wizard extends React . PureComponent < WizardProps > { private divRef : HTMLDivElement | null = null ; static defaultProps = { isInline : false , previousButtonText : "BACK" , nextButtonText : "NEXT" , cancelButtonText : "CANCEL" , finishButtonText : "FINISH" , size : WizardSize . MEDIUM , showNav : true , defaultStepId : 0 , validationType : WizardValidationType . NONE , } ; private initialState : WizardState = { show : this . props . show !== undefined ? this . props . show : false , currentStepId : this . props . defaultStepId ! , showFinishButton : this . props . defaultStepId === this . props . steps . length - 1 ? true : false , disableFinishButton : false , showPreviousButton : this . props . showPreviousButton ? this . props . showPreviousButton : false , showCancelButton : this . props . showCancelButton ? this . props . showCancelButton : true , showNextButton : this . props . defaultStepId === this . props . steps . length - 1 ? false : true , disableNextButton : false , allSteps : this . props . steps , } ; state : WizardState = this . initialState ; componentWillMount ( ) { this . createRefDiv ( ) ; this . initStepsState ( ) ; } private createRefDiv ( ) { const { isInline } = this . props ; if ( this . divRef === null ) { const el = document . createElement ( "div" ) ; document . body . appendChild ( el ) ; this . divRef = el ; } if ( isInline ) { document . body . classList . add ( ClassNames . NO_SCROLLING ) ; } } private cleanup ( ) { const { isInline } = this . props ; if ( this . divRef !== null ) { document . body . removeChild ( this . divRef ) ; this . divRef = null ; } if ( isInline ) { document . body . classList . remove ( ClassNames . NO_SCROLLING ) ; } } private initStepsState ( ) { const { validationType , steps } = this . props ; const disableNext = validationType === WizardValidationType . SYNC ? true : false ; let disableFinish = this . state . disableFinishButton ; if ( steps . length === 1 ) { disableFinish = disableNext ; } steps . map ( ( step , key ) => { const disableNav = ( step . stepId === 0 && validationType === WizardValidationType . SYNC ) || [ WizardValidationType . ASYNC , WizardValidationType . NONE ] . includes ( validationType ! ) ? false : true ; this . setState ( { disableNextButton : this . state . disableNextButton !== disableNext ? disableNext : undefined , disableFinishButton : this . state . disableFinishButton !== disableFinish ? disableFinish : undefined , allSteps : [ ... this . state . allSteps , ( ( this . state . allSteps [ step . stepId ] . stepCompleted = false ) , ( this . state . allSteps [ step . stepId ] . disableNav = disableNav ) ) , ] , } ) ; } ) ; } componentWillUnmount ( ) { this . cleanup ( ) ; } private getStepObj ( stepId ) { return this . state . allSteps [ stepId ] ; } getCurrentStepDetails ( ) { return this . getStepObj ( this . state . currentStepId ) ; } close ( ) { const { onClose } = this . props ; this . setState ( { show : false } ) ; onClose && onClose ( ) ; } show ( ) { this . setState ( { show : true } ) ; } resetWizard ( ) { const { steps } = this . props ; steps . map ( ( step , key ) => { this . setState ( { allSteps : [ ... this . state . allSteps , ( this . state . allSteps [ step . stepId ] . stepCompleted = false ) ] , } ) ; } ) ; this . setState ( this . initialState ) ; } nextButtonClick ( ) { const { onNext , steps } = this . props ; const nextStepId = this . state . currentStepId + 1 ; const currenstStep = this . getStepObj ( this . state . currentStepId ) ; const { validState , disableNextStep } = this . checkStepValidity ( this . state . currentStepId ) ; if ( ! disableNextStep && nextStepId <= steps . length - 1 ) { if ( currenstStep . onStepSubmit ) { currenstStep . onStepSubmit ( ) . then ( ( ) => { onNext && onNext ( ) ; } ) . then ( ( ) => { this . modifyButtonStates ( nextStepId ) ; } ) ; } else if ( onNext ) { onNext ( ) . then ( ( ) => { this . modifyButtonStates ( nextStepId ) ; } ) ; } else { this . modifyButtonStates ( nextStepId ) ; } } } previousButtonClick ( ) { const { onPrevious } = this . props ; const previousStepId = this . state . currentStepId - 1 ; if ( onPrevious ) { onPrevious ( ) . then ( ( ) => { if ( previousStepId >= 0 ) { this . modifyButtonStates ( previousStepId ) ; } } ) ; } else { this . modifyButtonStates ( previousStepId ) ; } } finishButtonClick ( ) { const { onFinish , validationType , isInline } = this . props ; let finishWizard ; const currenstStep = this . getStepObj ( this . state . currentStepId ) ; const { validState , disableNextStep } = this . checkStepValidity ( this . state . currentStepId ) ; if ( validationType === WizardValidationType . ASYNC ) { var { validationData , allStepsValid } = this . checkValidityOfAllSteps ( ) ; finishWizard = allStepsValid ; } else { finishWizard = ! disableNextStep ; } if ( finishWizard ) { if ( currenstStep . onStepSubmit ) { currenstStep . onStepSubmit ( ) . then ( ( ) => { onFinish && onFinish ( ) ; } ) ; } else { onFinish && onFinish ( ) ; } if ( ! isInline ) { this . close ( ) ; } } } private modifyButtonStates ( stepId ) { const { steps , validationType , showPreviousButton , showCancelButton } = this . props ; const step = this . getStepObj ( stepId ) ; this . scrollToTop ( ) ; if ( stepId === 0 ) { this . setState ( { showPreviousButton : false , showNextButton : true , disableNextButton : ! step . stepCompleted && validationType == WizardValidationType . SYNC ? true : false , showFinishButton : false , showCancelButton : showCancelButton !== undefined ? showCancelButton : true , currentStepId : step . stepId , } ) ; } else if ( stepId === steps . length - 1 ) { this . setState ( { showPreviousButton : showPreviousButton !== undefined ? showPreviousButton : true , showFinishButton : true , disableFinishButton : ! step . stepCompleted && validationType == WizardValidationType . SYNC ? true : false , showNextButton : false , showCancelButton : showCancelButton !== undefined ? showCancelButton : true , currentStepId : step . stepId , } ) ; } else { this . setState ( { showPreviousButton : showPreviousButton !== undefined ? showPreviousButton : true , showNextButton : true , disableNextButton : ! step . stepCompleted && validationType == WizardValidationType . SYNC ? true : false , showFinishButton : false , showCancelButton : showCancelButton !== undefined ? showCancelButton : true , currentStepId : step . stepId , } ) ; } } navigationClick ( stepId ) { const step = this . getStepObj ( stepId ) ; if ( step && step . customStepNav && step . customStepNav . onNavClick ) { step . customStepNav . onNavClick ( ) . then ( ( ) => { this . modifyButtonStates ( stepId ) ; } ) ; } else { this . modifyButtonStates ( stepId ) ; } } private getStepNavClasses ( stepId ) { let classNames = [ ClassNames . WIZARD_STEPNAV_LINK ] ; if ( this . state . currentStepId === stepId ) classNames . push ( ClassNames . ACTIVE ) ; if ( this . state . allSteps [ stepId ] . stepCompleted ) classNames . push ( ClassNames . COMPLETE ) ; if ( this . props . navLinkClasses ) classNames . push ( this . props . navLinkClasses ) ; return classNames ; } checkStepValidity ( stepId ) { const { validationType , steps } = this . props ; let validationState = true ; let disableNext = false ; let currenstStep = this . getStepObj ( stepId ) ; let nextStep = stepId == steps . length - 1 ? currenstStep : this . state . allSteps [ stepId + 1 ] ; if ( currenstStep . isStepValid !== undefined ) validationState = currenstStep . isStepValid ! ( ) ; if ( ! validationState && validationType === WizardValidationType . SYNC ) disableNext = true ; if ( currenstStep . stepCompleted !== validationState ) { this . setState ( { disableNextButton : this . state . disableNextButton !== disableNext ? disableNext : undefined , disableFinishButton : this . state . disableFinishButton !== disableNext ? disableNext : undefined , allSteps : [ ... this . state . allSteps , ( ( currenstStep . stepCompleted = validationState ) , ( nextStep . disableNav = disableNext ) ) , ] , } ) ; } return { validState : validationState , disableNextStep : disableNext , } ; } checkValidityOfAllSteps ( ) { const { steps } = this . props ; let validationData = { } ; steps . map ( ( step , key ) => { const { validState , disableNextStep } = this . checkStepValidity ( step . stepId ) ; validationData [ step . stepId ] = validState ; } ) ; const allStepsValid = allTrue ( validationData ) ; return { validationData : validationData , allStepsValid : allStepsValid , } ; } private buildWizardFooter ( ) { const { cancelButtonText , nextButtonText , previousButtonText , finishButtonText , nextButtonClassName , previousButtonClassName , finishButtonClassName , cancelButtonClassName , customFooter , } = this . props ; const { showPreviousButton , showNextButton , showFinishButton , showCancelButton , currentStepId } = this . state ; const stepObj = this . getStepObj ( currentStepId ) ; return ( < div className = { ClassNames . WIZARD_FOOTER } > < div className = { ClassNames . WIZARD_FOOTER_BUTTON } > { customFooter } { stepObj . stepFooter } { showCancelButton && ( < Button key = { cancelButtonText } className = { cancelButtonClassName } link dataqa = { dataqa_wizard_btn_cancel } onClick = { this . close . bind ( this ) } > { cancelButtonText } { " " } < /Button>)} { showPreviousButton && ( < Button key = { previousButtonText } className = { previousButtonClassName } dataqa = { dataqa_wizard_btn_previous } onClick = { this . previousButtonClick . bind ( this ) } > { previousButtonText } { " " } < /Button>)} { showNextButton && ( < Button key = { nextButtonText } className = { nextButtonClassName } primary disabled = { this . state . disableNextButton } dataqa = { dataqa_wizard_btn_next } onClick = { this . nextButtonClick . bind ( this ) } > { nextButtonText } { " " } < /Button>)} { showFinishButton && ( < Button key = { finishButtonText } className = { finishButtonClassName } state = { ButtonState . SUCCESS } disabled = { this . state . disableFinishButton } dataqa = { dataqa_wizard_btn_finish } onClick = { this . finishButtonClick . bind ( this ) } > { finishButtonText } < /Button>)} < /div>{" "} { } { } < /div>); } private buildStepIcon ( step ) { if ( step . customStepNav !== undefined && step . customStepNav . stepNavIcon ) return { shape : step . customStepNav . stepNavIcon } ; return undefined ; } private buildWizardNav ( ) { const { steps , title , showNav } = this . props ; return ( < VerticalNav className = { classNames ( [ ClassNames . WIZARD_STEPNAV_WRAPPER , ClassNames . NG_TNS ] ) } style = { Styles . WIZARD_STEPNAV_WRAPPER_STYLE } > < h2 className = { ClassNames . WIZARD_TITLE } > < span style = { Styles . WIZARD_TITLE_STYLE } > { title } < /span> < /h2> { showNav && ( < div className = { ClassNames . WIZARD_STEPNAV } > { steps . map ( ( step , key ) => { return ( < div className = { classNames ( this . getStepNavClasses ( step . stepId ) ) } > < Button disabled = { this . state . allSteps [ step . stepId ] . disableNav } link = { true } className = "clr-wizard-stepnav-link" onClick = { this . navigationClick . bind ( this , step . stepId ) } icon = { this . buildStepIcon ( step ) } > & nbsp ; { step . customStepNav !== undefined && step . customStepNav . stepNavTitle ? step . customStepNav . stepNavTitle : step . stepName } & nbsp ; { step . customStepNav !== undefined && step . customStepNav ! . stepNavChildren } < /Button> < /div>); } ) } < /div>)} < /VerticalNav>); } private buildWizardSteps ( ) { const { steps } = this . props ; return ( < div className = { ClassNames . WIZARD_CONTENT } > { steps . map ( ( step , key ) => { const hideStep = step . stepId === this . state . currentStepId ? false : true ; return ( < div role = "tabpanel" id = { step . stepId . toString ( ) } aria - hidden = { hideStep } aria - labelledby = "stepId" className = { `${ ClassNames . ACTIVE } ${ ClassNames . WIZARD_PAGE }` } > { step . stepComponent } < /div>); } ) } < /div>); } private buildWizard ( ) { const { size , closable , steps , isInline , style , className , dataqa , } = this . props ; if ( dataqa ) this . updateDataQAStrings ( dataqa ) ; const wizardSize = "wizard-" + size ; const modalSize = "modal-" + size ; const buttonStyle = ( ) => { if ( isInline ) { return { display : "inline-block !important" } ; } } ; return ( < React . Fragment > < div data - qa = { dataqa_wizard } className = { classNames ( [ isInline && `${ ClassNames . WIZARD_INLINE } ${ ClassNames . WIZARD_NO_SHADOW }` , ClassNames . WIZARD , wizardSize , ClassNames . NG_TNS , ClassNames . WIZARD_OPEN , className , ] ) } style = { style } > < span className = "offscreen-focus-rebounder ng-tns-c167-4 ng-star-inserted" / > < div className = { ClassNames . WIZARD_MODAL } > < div className = { classNames ( [ ClassNames . WIZARD_MODAL_DIALOG , modalSize ] ) } role = "dialog" aria - hidden = "false" aria - labelledby = "clr-id-3" > < div _ngcontent - clarity - c167 = "" className = "clr-sr-only ng-tns-c167-4" / > < div className = { ClassNames . WIZARD_OUTER_WRAPPER } style = { Styles . WIZARD_OUTER_WRAPPER } > < div className = { classNames ( [ ClassNames . MODAL_CONTENT_WRAPPER , ClassNames . NG_TNS ] ) } > { this . buildWizardNav ( ) } < div className = { classNames ( [ ClassNames . MODAL_CONTENT , ClassNames . NG_TNS ] ) } > < div className = { classNames ( [ ClassNames . MODAL_HEADER , ClassNames . NG_TNS ] ) } > { closable && ( < button aria - label = "Close" className = { classNames ( [ ClassNames . CLOSE , ClassNames . NG_TNS , ClassNames . NG_STAR_INSERTED , ] ) } type = "button" onClick = { this . close . bind ( this ) } > < Icon aria - hidden = { true } shape = "close" / > < /button>)} < div className = { classNames ( [ ClassNames . MODAL_TITLE_WRAPPER , ClassNames . NG_TNS , ] ) } > < h3 className = { classNames ( [ ClassNames . MODAL_TITLE , ClassNames . NG_TNS ] ) } style = { Styles . MODAL_TITLE_STYLE } > < span className = { ClassNames . MODAL_TITLE_TEXT } > { steps [ this . state . currentStepId ] . showStepTitle !== false && steps [ this . state . currentStepId ] . stepName } < /span> < /h3> < /div> < /div> { } < div className = { ClassNames . MODAL_BODY } > { this . buildWizardSteps ( ) } < /div>{" "} { } { this . buildWizardFooter ( ) } < /div> { } < /div> { } < div className = { ClassNames . MODAL_GHOST_WRAPPER } > < div className = { ClassNames . MODAL_GHOST_1 } style = { Styles . MODAL_GHOST_1 } / > < div className = { ClassNames . MODAL_GHOST_2 } style = { Styles . MODAL_GHOST_2 } / > < /div> < /div>{" "} { } < /div>{" "} { } < div className = { ClassNames . MODAL_BACKDROP } aria - hidden = "true" / > < /div>{" "} { } < /div>{" "} { } < /React.Fragment>); } updateDataQAStrings ( dataqa ) { let prefix = "prefix" ; dataqa_wizard_btn_cancel = dataqa_wizard_btn_cancel . replace ( new RegExp ( "^" + prefix ) , dataqa ) ; dataqa_wizard_btn_previous = dataqa_wizard_btn_previous . replace ( new RegExp ( "^" + prefix ) , dataqa ) ; dataqa_wizard_btn_next = dataqa_wizard_btn_next . replace ( new RegExp ( "^" + prefix ) , dataqa ) ; dataqa_wizard_btn_finish = dataqa_wizard_btn_finish . replace ( new RegExp ( "^" + prefix ) , dataqa ) ; dataqa_wizard = dataqa_wizard . replace ( new RegExp ( "^" + prefix ) , dataqa ) ; } scrollToTop ( ) { document . getElementsByClassName ( "modal-body" ) [ 0 ] . scrollTo ( 0 , 0 ) ; } render ( ) { return this . props . show ? ReactDOM . createPortal ( this . buildWizard ( ) , this . divRef ! ) : null ; } }