diff --git a/src/AddOkr/components/AddOkrPageLayout.tsx b/src/AddOkr/components/AddOkrPageLayout.tsx new file mode 100644 index 00000000..3db64842 --- /dev/null +++ b/src/AddOkr/components/AddOkrPageLayout.tsx @@ -0,0 +1,135 @@ +import ProgressBar from '@components/ProgressBar'; +import { css } from '@emotion/react'; +import { default as emotionStyled, default as styled } from '@emotion/styled'; +import { PropsWithChildren } from 'react'; +import { useNavigate } from 'react-router-dom'; + +import { IS_GUIDE, MAX_BASIC_STEP, MAX_GUIDE_STEP } from '../constants/ADD_OKR_METHOD_N_STEP'; +import StepBtns from './commonUse/StepBtns'; + +interface AddOkrPageLayoutProps extends PropsWithChildren { + isActiveNext: boolean; + selectedMethod: string; + step: number; + onMoveStep: (targetStep: (prev: number) => number) => void; +} + +const AddOkrPageLayout = ({ + isActiveNext, + selectedMethod, + step, + onMoveStep, + children, +}: AddOkrPageLayoutProps) => { + const navigate = useNavigate(); + + // 이전, 다음 버튼 관련 handler + const handleClickPrevBtn = () => { + // step 1 -> 0으로 이동시 정보 초기화 + if (step === 1) { + navigate('/dashboard', { state: { selectedMethod: selectedMethod } }); + } + + // default function + onMoveStep((prev) => prev - 1); + }; + + const handleClickNextBtn = () => { + // 가이드에 따라 설정하기 vs 직접 설정하기 구분 조건 : 직접 설정하기일 때는 step 4 -> step 6로 preview okr로 이동 + if (step === 4 && selectedMethod !== IS_GUIDE) { + isActiveNext && onMoveStep((prev) => prev + 2); + return; + } + + // default function + isActiveNext && onMoveStep((prev) => prev + 1); + }; + + return ( + <> + {step <= 5 ? ( +
+ {selectedMethod} + {children} + <> + + + +
+ {`${step}/${ + selectedMethod === IS_GUIDE ? MAX_GUIDE_STEP : MAX_BASIC_STEP + }`} +
+
+ +
+ ) : ( + // step > 6, 즉 preview-okr에서는 페이지 정렬 다르게 + <>{children} + )} + + ); +}; + +export default AddOkrPageLayout; + +const AddOkrContainer = css` + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + width: 100%; + height: 100%; +`; + +const StSelectedMethodTxt = emotionStyled.p` + color: ${({ theme }) => theme.colors.gray_300}; + ${({ theme }) => theme.fonts.body_12_medium}; +`; + +const marginTopState = (step: number, selectedMethod: string) => { + switch (step) { + case 1: + return selectedMethod === IS_GUIDE ? '6.8rem' : '14.8rem'; + case 2: + return '5rem'; + case 3: + return '7.3rem'; + case 4: + case 5: + return '3.4rem'; + } +}; +const StProgressBarBox = styled.div<{ $step: number; $selectedMethod: string }>` + position: relative; + display: flex; + flex-direction: column; + gap: 0.8rem; + width: 38rem; + height: 2.7rem; + margin-top: ${({ $step, $selectedMethod }) => marginTopState($step, $selectedMethod)}; + + & progress { + height: 0.8rem !important; + } +`; + +const ProgressTxtBox = css` + position: absolute; + right: 0; + bottom: 0; + width: fit-content; +`; + +const StProgressTxt = styled.span` + color: ${({ theme }) => theme.colors.gray_300}; + ${({ theme }) => theme.fonts.btn_11_medium}; +`; diff --git a/src/AddOkr/components/stepLayout/AddGuideFirstKr.tsx b/src/AddOkr/components/stepLayout/AddGuideFirstKr.tsx index f33c50d1..b60f1b13 100644 --- a/src/AddOkr/components/stepLayout/AddGuideFirstKr.tsx +++ b/src/AddOkr/components/stepLayout/AddGuideFirstKr.tsx @@ -1,6 +1,6 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import React from 'react'; +import React, { useEffect } from 'react'; import { AddOkrCardWrapper } from '../../styles/KeyResultCardStyle'; import { IAddKrFlowProps } from '../../types/KrInfoTypes'; @@ -16,11 +16,25 @@ const AddGuideFirstKr = ({ handleClickCloseBtn, krListInfo, setKrListInfo, + onValidateNextStep, }: IAddKrFlowProps) => { const { objTitle } = objInfo; const plusCardLength = Array.from({ length: MAX_KR_LENGTH - 1 }, (_, i) => i + 1); + useEffect(() => { + const isValid = + krListInfo.filter((kr) => { + return clickedCard.includes(kr.krIdx); + }).length === + krListInfo.filter((kr) => { + const { krTitle, krStartAt, krExpireAt } = kr; + return krTitle && krStartAt && krExpireAt; + }).length; + + onValidateNextStep(isValid); + }, [krListInfo, clickedCard]); + return (
diff --git a/src/AddOkr/components/stepLayout/AddGuideSecondKr.tsx b/src/AddOkr/components/stepLayout/AddGuideSecondKr.tsx index 9e45f43e..13dbade6 100644 --- a/src/AddOkr/components/stepLayout/AddGuideSecondKr.tsx +++ b/src/AddOkr/components/stepLayout/AddGuideSecondKr.tsx @@ -1,14 +1,34 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; +import { useEffect } from 'react'; import { AddOkrCardWrapper, EmptyKeyResultCard } from '../../styles/KeyResultCardStyle'; import { IAddKrFlowProps } from '../../types/KrInfoTypes'; import GuideSecondKeyResultCard from '../addKr/GuideSecondKeyResultCard'; -const AddGuideSecondKr = ({ objInfo, clickedCard, krListInfo, setKrListInfo }: IAddKrFlowProps) => { +const AddGuideSecondKr = ({ + objInfo, + clickedCard, + krListInfo, + setKrListInfo, + onValidateNextStep, +}: IAddKrFlowProps) => { const { objTitle } = objInfo; const secondKrList = [0, 1, 2]; + useEffect(() => { + const isValid = + krListInfo.filter((kr) => { + return clickedCard.includes(kr.krIdx); + }).length === + krListInfo.filter((kr) => { + const { krTarget, krMetric } = kr; + return krTarget && krMetric; + }).length; + + onValidateNextStep(isValid); + }, [krListInfo, clickedCard]); + return (
diff --git a/src/AddOkr/components/stepLayout/AddKr.tsx b/src/AddOkr/components/stepLayout/AddKr.tsx index e5ca11df..a0833562 100644 --- a/src/AddOkr/components/stepLayout/AddKr.tsx +++ b/src/AddOkr/components/stepLayout/AddKr.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import React from 'react'; +import React, { useEffect } from 'react'; import { AddOkrCardWrapper } from '../../styles/KeyResultCardStyle'; import { IAddKrFlowProps } from '../../types/KrInfoTypes'; @@ -15,9 +15,23 @@ const AddKr = ({ handleClickCloseBtn, krListInfo, setKrListInfo, + onValidateNextStep, }: IAddKrFlowProps) => { const { objTitle } = objInfo; + useEffect(() => { + const isValid = + krListInfo.filter((kr) => { + return clickedCard.includes(kr.krIdx); + }).length === + krListInfo.filter((kr) => { + const { krTitle, krTarget, krMetric, krStartAt, krExpireAt } = kr; + return krTitle && krTarget && krMetric && krStartAt && krExpireAt; + }).length; + + onValidateNextStep(isValid); + }, [krListInfo, clickedCard]); + const renderKrCards = () => { const plusCardLength = Array.from({ length: MAX_KR_LENGTH - 1 }, (_, i) => i + 1); return ( diff --git a/src/AddOkr/components/stepLayout/ObjContent.tsx b/src/AddOkr/components/stepLayout/ObjContent.tsx index 63a78f19..d7d4aac3 100644 --- a/src/AddOkr/components/stepLayout/ObjContent.tsx +++ b/src/AddOkr/components/stepLayout/ObjContent.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { limitMaxLength } from '@utils/limitMaxLength'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { MAX_OBJ_CONTENT } from '../../constants/OKR_MAX_LENGTH'; import { IAddObjFlowProps } from '../../types/ObjectInfoTypes'; @@ -10,7 +10,7 @@ import { IAddObjFlowProps } from '../../types/ObjectInfoTypes'; const OBJ_CONTENT_PLACEHOLDER = 'ex) 퇴근 후 누워있기만 하지 말고, 내가 원하는 일을 하며 시간을 알차게 쓰고 싶다.'; -const ObjContent = ({ objInfo, setObjInfo }: IAddObjFlowProps) => { +const ObjContent = ({ objInfo, setObjInfo, onValidNextStep }: IAddObjFlowProps) => { const { objContent } = objInfo; // 글자 수 저장 값 const [currContentCnt, setCurrContentCnt] = useState(objContent ? objContent.length : 0); @@ -24,6 +24,10 @@ const ObjContent = ({ objInfo, setObjInfo }: IAddObjFlowProps) => { setObjInfo({ ...objInfo, objContent: e.target.value }); }; + useEffect(() => { + onValidNextStep(!!objContent); + }, [objContent]); + return (
목표를 달성하고 싶은 이유와 다짐을 기록해주세요 diff --git a/src/AddOkr/components/stepLayout/ObjPeriod.tsx b/src/AddOkr/components/stepLayout/ObjPeriod.tsx index 93fa832a..8d2dbd58 100644 --- a/src/AddOkr/components/stepLayout/ObjPeriod.tsx +++ b/src/AddOkr/components/stepLayout/ObjPeriod.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { Dayjs } from 'dayjs'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { CALE_END_DATE, CALE_START_DATE, TODAY } from '../../constants/ADD_OKR_DATES'; import { OBJ_PERIOD_LIST } from '../../constants/OBJ_PERIOD_LIST'; @@ -17,7 +17,13 @@ interface IObjPeriodProps extends IAddObjFlowProps { const DEFAULT_SELECT_PERIOD = '1'; -const ObjPeriod = ({ objInfo, setObjInfo, selectedPeriod, setSelectedPeriod }: IObjPeriodProps) => { +const ObjPeriod = ({ + objInfo, + setObjInfo, + selectedPeriod, + setSelectedPeriod, + onValidNextStep, +}: IObjPeriodProps) => { const { objStartAt, objExpireAt } = objInfo; // dayjs 캘린더에서 사용하는 선택된 기간 값 @@ -57,6 +63,10 @@ const ObjPeriod = ({ objInfo, setObjInfo, selectedPeriod, setSelectedPeriod }: I } }; + useEffect(() => { + onValidNextStep(!!objStartAt && !!objExpireAt && !!selectedPeriod); + }, [objInfo]); + return (
앞으로 몇 개월 동안 목표에 집중해볼까요? diff --git a/src/AddOkr/components/stepLayout/ObjTitleCateg.tsx b/src/AddOkr/components/stepLayout/ObjTitleCateg.tsx index 37ba63ec..2910e55d 100644 --- a/src/AddOkr/components/stepLayout/ObjTitleCateg.tsx +++ b/src/AddOkr/components/stepLayout/ObjTitleCateg.tsx @@ -1,7 +1,7 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { limitMaxLength } from '@utils/limitMaxLength'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { GUIDE_OBJ_TITLE_PLACEHOLDER } from '../../constants/GUIDE_OBJ_TITLE_PLACEHOLDER'; import { OBJ_CATEG_LIST } from '../../constants/OBJ_CATEG_LIST'; @@ -16,7 +16,7 @@ interface IObjTitleCategProps extends IAddObjFlowProps { // 가이드에 따라 설정하기 기본 placeholder const GUIDE_DEFAULT_PLACEHOLDER = '목표를 입력하세요'; -const ObjTitleCateg = ({ isGuide, objInfo, setObjInfo }: IObjTitleCategProps) => { +const ObjTitleCateg = ({ isGuide, objInfo, setObjInfo, onValidNextStep }: IObjTitleCategProps) => { const { objCategory: selectedObjCateg, objTitle } = objInfo; //글자 수 관리 값 const [currObjCount, setCurrObjCount] = useState(objTitle ? objTitle.length : 0); @@ -77,6 +77,10 @@ const ObjTitleCateg = ({ isGuide, objInfo, setObjInfo }: IObjTitleCategProps) => setHoverObjPlaceHolder(targetPlaceholder); }; + useEffect(() => { + onValidNextStep(!!selectedObjCateg && !!objTitle); + }, [objInfo]); + return (
diff --git a/src/AddOkr/index.tsx b/src/AddOkr/index.tsx index 176d1db1..3df3ba82 100644 --- a/src/AddOkr/index.tsx +++ b/src/AddOkr/index.tsx @@ -1,12 +1,9 @@ import Error from '@components/Error'; -import ProgressBar from '@components/ProgressBar'; -import { css } from '@emotion/react'; -import styled from '@emotion/styled'; -import { useEffect, useState } from 'react'; -import { useLocation, useNavigate } from 'react-router-dom'; +import { useState } from 'react'; +import { useLocation } from 'react-router-dom'; import PreviewOkr from '../PreviewOkr/PreviewOkr'; -import StepBtns from './components/commonUse/StepBtns'; +import AddOkrPageLayout from './components/AddOkrPageLayout'; import AddGuideFirstKr from './components/stepLayout/AddGuideFirstKr'; import AddGuideSecondKr from './components/stepLayout/AddGuideSecondKr'; import AddKr from './components/stepLayout/AddKr'; @@ -14,93 +11,55 @@ import ObjContent from './components/stepLayout/ObjContent'; import ObjPeriod from './components/stepLayout/ObjPeriod'; import ObjTitleCateg from './components/stepLayout/ObjTitleCateg'; import { OBJ_START_AT } from './constants/ADD_OKR_DATES'; -import { IS_GUIDE, MAX_BASIC_STEP, MAX_GUIDE_STEP } from './constants/ADD_OKR_METHOD_N_STEP'; +import { IS_GUIDE } from './constants/ADD_OKR_METHOD_N_STEP'; import { IKrListInfoTypes } from './types/KrInfoTypes'; +const resetTaskInfo = [0, 1, 2].map((idx) => { + return { + taskTitle: '', + taskIdx: idx, + }; +}); + +const resetKrListInfo = [0, 1, 2].map((idx) => { + return { + krIdx: idx, + krTitle: '', + krStartAt: '', + krExpireAt: '', + krTarget: '', + krMetric: '', + taskList: resetTaskInfo, + }; +}); + +const resetObjInfoState = { + objTitle: '', + objCategory: '', + objContent: '', + objStartAt: OBJ_START_AT, + objExpireAt: '', +}; + const AddOkr = () => { const location = useLocation(); - const navigate = useNavigate(); - - const resetTaskInfo = [0, 1, 2].map((idx) => { - return { - taskTitle: '', - taskIdx: idx, - }; - }); - const resetKrListInfo = [0, 1, 2].map((idx) => { - return { - krIdx: idx, - krTitle: '', - krStartAt: '', - krExpireAt: '', - krTarget: '', - krMetric: '', - taskList: resetTaskInfo, - }; - }); - - const resetObjInfoState = { - objTitle: '', - objCategory: '', - objContent: '', - objStartAt: OBJ_START_AT, - objExpireAt: '', - }; + const selectedMethod = location.state.selectedMethod ?? null; // step 관리 값 - const [step, setStep] = useState(0); + const [step, setStep] = useState(1); //이전, 다음 버튼 관리 값 const [isActiveNext, setIsActiveNext] = useState(false); const [objInfo, setObjInfo] = useState(resetObjInfoState); - const { objTitle, objCategory, objContent, objStartAt, objExpireAt } = objInfo; - const [krListInfo, setKrListInfo] = useState(resetKrListInfo); - - // Step 0 - SELECT METHOD 관련 State - const [selectedMethod, setSelectedMethod] = useState(''); // Step 2 ObjPeriod- 선택된 기간 버튼 관리 값 const [selectedPeriod, setSelectedPeriod] = useState(''); - // step 4 - kr 카드 선택 여부 관리 const [clickedCard, setClickedCard] = useState([0]); - // 이전, 다음 버튼 관련 handler - const handleClickPrevBtn = () => { - // step 1 -> 0으로 이동시 정보 초기화 - if (step === 1) { - navigate('/dashboard', { state: { selectedMethod: selectedMethod } }); - } - - // default function - setStep((prev) => prev - 1); - }; - - const handleClickNextBtn = () => { - // 가이드에 따라 설정하기 vs 직접 설정하기 구분 조건 - // if ( - // (step === 4 && selectedMethod !== IS_GUIDE) || - // (step === 5 && selectedMethod === IS_GUIDE) - // ) { - // navigate('/preview-okr', { - // state: { - // selectedMethod: selectedMethod, - // step: step, - // objInfo: objInfo, - // krListInfo: krListInfo.filter((kr) => kr.title), - // }, - // }); - // } - - // 가이드에 따라 설정하기 vs 직접 설정하기 구분 조건 : 직접 설정하기일 때는 step 4 -> step 6로 preview okr로 이동 - if (step === 4 && selectedMethod !== IS_GUIDE) { - isActiveNext && setStep((prev) => prev + 2); - return; - } - - // default function - isActiveNext && setStep((prev) => prev + 1); + const handleMoveStep = (targetStep: (prev: number) => number) => { + setStep(targetStep); }; // step 4 ~ 5 - kr 카드 추가 버튼 핸들러 @@ -124,75 +83,13 @@ const AddOkr = () => { setKrListInfo([...krListInfo]); }; - // 이전, 다음 버튼 활성화 / 비활성화 관리 함수 - const validNextStep = () => { - switch (step) { - case 1: - objCategory && objTitle ? setIsActiveNext(true) : setIsActiveNext(false); - break; - case 2: - objStartAt && objExpireAt && selectedPeriod - ? setIsActiveNext(true) - : setIsActiveNext(false); - break; - case 3: - objContent ? setIsActiveNext(true) : setIsActiveNext(false); - break; - case 4: - // 가이드에 따라 설정 - 첫 번째 kr 카드일 때 - if (selectedMethod === IS_GUIDE) { - krListInfo.filter((kr) => { - return clickedCard.includes(kr.krIdx); - }).length === - krListInfo.filter((kr) => { - const { krTitle, krStartAt, krExpireAt } = kr; - return krTitle && krStartAt && krExpireAt; - }).length - ? setIsActiveNext(true) - : setIsActiveNext(false); - } - - // 직접 설정하기 플로우일 때 - if (selectedMethod !== IS_GUIDE) { - krListInfo.filter((kr) => { - return clickedCard.includes(kr.krIdx); - }).length === - krListInfo.filter((kr) => { - const { krTitle, krTarget, krMetric, krStartAt, krExpireAt } = kr; - return krTitle && krTarget && krMetric && krStartAt && krExpireAt; - }).length - ? setIsActiveNext(true) - : setIsActiveNext(false); - } - break; - case 5: - //가이드에 따라 설정 - 두번째 kr 카드일 떄 - krListInfo.filter((kr) => { - return clickedCard.includes(kr.krIdx); - }).length === - krListInfo.filter((kr) => { - const { krTarget, krMetric } = kr; - return krTarget && krMetric; - }).length - ? setIsActiveNext(true) - : setIsActiveNext(false); - break; - } + const handleValidNextStep = (isValid: boolean) => { + setIsActiveNext(isValid); }; // step에 따라 다른 layout 렌더링하는 함수 const renderStepLayout = () => { switch (step) { - case 0: - if (location.state.selectedMethod) { - setSelectedMethod(location.state.selectedMethod); - setStep((prev) => prev + 1); - return; - } - if (!location.state.selectedMethod) { - return ; - } - break; case 1: // step 1 - O 카데고리, 제목 설정 return ( @@ -200,6 +97,7 @@ const AddOkr = () => { isGuide={selectedMethod === IS_GUIDE} objInfo={objInfo} setObjInfo={setObjInfo} + onValidNextStep={handleValidNextStep} /> ); case 2: @@ -210,12 +108,19 @@ const AddOkr = () => { setObjInfo={setObjInfo} selectedPeriod={selectedPeriod} setSelectedPeriod={setSelectedPeriod} + onValidNextStep={handleValidNextStep} /> ); case 3: // step 3 - O 내용 설정 - return ; + return ( + + ); case 4: // step 4 - KR 추가 (가이드에 따라 설정 첫번째 kr 카드 or 직접 설정하기) @@ -227,6 +132,7 @@ const AddOkr = () => { handleClickCloseBtn={handleClickCloseBtn} krListInfo={krListInfo} setKrListInfo={setKrListInfo} + onValidateNextStep={handleValidNextStep} /> ) : ( { handleClickCloseBtn={handleClickCloseBtn} krListInfo={krListInfo} setKrListInfo={setKrListInfo} + onValidateNextStep={handleValidNextStep} /> ); case 5: @@ -248,6 +155,7 @@ const AddOkr = () => { handleClickCloseBtn={handleClickCloseBtn} krListInfo={krListInfo} setKrListInfo={setKrListInfo} + onValidateNextStep={handleValidNextStep} /> ); case 6: @@ -265,96 +173,20 @@ const AddOkr = () => { } }; - // 스텝에 따라 검증 - useEffect(() => { - validNextStep(); - }, [step, objInfo, krListInfo, clickedCard]); + if (!location.state.selectedMethod) { + return ; + } return ( - <> - {selectedMethod && step <= 5 ? ( -
- {selectedMethod} - {renderStepLayout()} - <> - - - -
- {`${step}/${ - selectedMethod === IS_GUIDE ? MAX_GUIDE_STEP : MAX_BASIC_STEP - }`} -
-
- -
- ) : ( - // step > 6, 즉 preview-okr에서는 페이지 정렬 다르게 - renderStepLayout() - )} - + + {renderStepLayout()} + ); }; export default AddOkr; - -const AddOkrContainer = css` - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; - height: 100%; -`; - -const StSelectedMethodTxt = styled.p` - color: ${({ theme }) => theme.colors.gray_300}; - ${({ theme }) => theme.fonts.body_12_medium}; -`; - -const marginTopState = (step: number, selectedMethod: string) => { - switch (step) { - case 1: - return selectedMethod === IS_GUIDE ? '6.8rem' : '14.8rem'; - case 2: - return '5rem'; - case 3: - return '7.3rem'; - case 4: - case 5: - return '3.4rem'; - } -}; -const StProgressBarBox = styled.div<{ $step: number; $selectedMethod: string }>` - position: relative; - display: flex; - flex-direction: column; - gap: 0.8rem; - width: 38rem; - height: 2.7rem; - margin-top: ${({ $step, $selectedMethod }) => marginTopState($step, $selectedMethod)}; - - & progress { - height: 0.8rem !important; - } -`; - -const ProgressTxtBox = css` - position: absolute; - right: 0; - bottom: 0; - width: fit-content; -`; - -const StProgressTxt = styled.span` - color: ${({ theme }) => theme.colors.gray_300}; - ${({ theme }) => theme.fonts.btn_11_medium}; -`; diff --git a/src/AddOkr/types/KrInfoTypes.ts b/src/AddOkr/types/KrInfoTypes.ts index bffd769d..03b8856d 100644 --- a/src/AddOkr/types/KrInfoTypes.ts +++ b/src/AddOkr/types/KrInfoTypes.ts @@ -18,4 +18,5 @@ export interface IAddKrFlowProps { handleClickCloseBtn: (cardIdx: number) => void; krListInfo: IKrListInfoTypes[]; setKrListInfo: React.Dispatch>; + onValidateNextStep: (isValid: boolean) => void; } diff --git a/src/AddOkr/types/ObjectInfoTypes.ts b/src/AddOkr/types/ObjectInfoTypes.ts index e4f2026a..b5321ff3 100644 --- a/src/AddOkr/types/ObjectInfoTypes.ts +++ b/src/AddOkr/types/ObjectInfoTypes.ts @@ -9,4 +9,5 @@ export interface IObjInfoTypes { export interface IAddObjFlowProps { objInfo: IObjInfoTypes; setObjInfo: React.Dispatch>; + onValidNextStep: (isValid: boolean) => void; }