Browse Source

feat(guided-steps): Auto-advance GuidedSteps when the current step has been completed (#71265)

Malachi Willey 9 months ago
parent
commit
558587cc44

+ 49 - 0
static/app/components/guidedSteps/guidedSteps.spec.tsx

@@ -1,5 +1,8 @@
+import {useState} from 'react';
+
 import {render, screen, userEvent, within} from 'sentry-test/reactTestingLibrary';
 
+import {Button} from 'sentry/components/button';
 import {GuidedSteps} from 'sentry/components/guidedSteps/guidedSteps';
 
 describe('GuidedSteps', function () {
@@ -65,4 +68,50 @@ describe('GuidedSteps', function () {
     expect(screen.getByText('This is the second step.')).toBeInTheDocument();
     expect(screen.queryByText('This is the third step.')).not.toBeInTheDocument();
   });
+
+  it('advances to the next step when the current one is completed', async function () {
+    function Comp() {
+      const [isCompleted, setIsCompleted] = useState(false);
+
+      return (
+        <div>
+          <Button onClick={() => setIsCompleted(true)}>Complete Step</Button>
+          <GuidedSteps>
+            <GuidedSteps.Step
+              stepKey="step-1"
+              title="Step 1 Title"
+              isCompleted={isCompleted}
+            >
+              This is the first step.
+              <GuidedSteps.StepButtons />
+            </GuidedSteps.Step>
+            <GuidedSteps.Step stepKey="step-2" title="Step 2 Title" isCompleted={false}>
+              This is the second step.
+              <GuidedSteps.StepButtons />
+            </GuidedSteps.Step>
+            <GuidedSteps.Step stepKey="step-3" title="Step 3 Title" isCompleted={false}>
+              This is the third step.
+              <GuidedSteps.StepButtons />
+            </GuidedSteps.Step>
+          </GuidedSteps>
+        </div>
+      );
+    }
+
+    render(<Comp />);
+
+    // First step is shown as active and not completed
+    expect(screen.getByText('This is the first step.')).toBeInTheDocument();
+    expect(
+      within(screen.getByTestId('guided-step-1')).queryByTestId('icon-check-mark')
+    ).not.toBeInTheDocument();
+    await userEvent.click(screen.getByRole('button', {name: 'Complete Step'}));
+
+    // Now the first step is completed and the second step is active
+    expect(screen.queryByText('This is the first step.')).not.toBeInTheDocument();
+    expect(screen.getByText('This is the second step.')).toBeInTheDocument();
+    expect(
+      within(screen.getByTestId('guided-step-1')).getByTestId('icon-check-mark')
+    ).toBeInTheDocument();
+  });
 });

+ 37 - 8
static/app/components/guidedSteps/guidedSteps.tsx

@@ -14,6 +14,7 @@ import {type BaseButtonProps, Button} from 'sentry/components/button';
 import {IconCheckmark} from 'sentry/icons';
 import {t} from 'sentry/locale';
 import {space} from 'sentry/styles/space';
+import usePrevious from 'sentry/utils/usePrevious';
 
 type GuidedStepsProps = {
   children: React.ReactElement<StepProps> | React.ReactElement<StepProps>[];
@@ -22,6 +23,7 @@ type GuidedStepsProps = {
 };
 
 interface GuidedStepsContextState {
+  advanceToNextIncompleteStep: () => void;
   currentStep: number;
   getStepNumber: (stepKey: string) => number;
   registerStep: (step: RegisterStepInfo) => void;
@@ -40,6 +42,7 @@ type RegisterStepInfo = Pick<StepProps, 'stepKey' | 'isCompleted'>;
 type RegisteredSteps = {[key: string]: {stepNumber: number; isCompleted?: boolean}};
 
 const GuidedStepsContext = createContext<GuidedStepsContextState>({
+  advanceToNextIncompleteStep: () => {},
   currentStep: 0,
   setCurrentStep: () => {},
   totalSteps: 0,
@@ -63,6 +66,7 @@ function useGuidedStepsContentValue({
   // render and that step order does not change.
   const registerStep = useCallback((props: RegisterStepInfo) => {
     if (registeredStepsRef.current[props.stepKey]) {
+      registeredStepsRef.current[props.stepKey].isCompleted = props.isCompleted;
       return;
     }
     const numRegisteredSteps = Object.keys(registeredStepsRef.current).length + 1;
@@ -77,15 +81,24 @@ function useGuidedStepsContentValue({
     return registeredStepsRef.current[stepKey]?.stepNumber ?? 1;
   }, []);
 
+  const getFirstIncompleteStep = useCallback(() => {
+    return orderBy(Object.values(registeredStepsRef.current), 'stepNumber').find(
+      step => step.isCompleted !== true
+    );
+  }, []);
+
+  const advanceToNextIncompleteStep = useCallback(() => {
+    const firstIncompleteStep = getFirstIncompleteStep();
+    if (firstIncompleteStep) {
+      setCurrentStep(firstIncompleteStep.stepNumber);
+    }
+  }, [getFirstIncompleteStep]);
+
   // On initial load, set the current step to the first incomplete step
   useEffect(() => {
-    const firstIncompleteStep = orderBy(
-      Object.values(registeredStepsRef.current),
-      'stepNumber'
-    ).find(step => step.isCompleted !== true);
-
+    const firstIncompleteStep = getFirstIncompleteStep();
     setCurrentStep(firstIncompleteStep?.stepNumber ?? 1);
-  }, []);
+  }, [getFirstIncompleteStep]);
 
   const handleSetCurrentStep = useCallback(
     (step: number) => {
@@ -102,21 +115,37 @@ function useGuidedStepsContentValue({
       totalSteps,
       registerStep,
       getStepNumber,
+      advanceToNextIncompleteStep,
     }),
-    [currentStep, getStepNumber, handleSetCurrentStep, registerStep, totalSteps]
+    [
+      advanceToNextIncompleteStep,
+      currentStep,
+      getStepNumber,
+      handleSetCurrentStep,
+      registerStep,
+      totalSteps,
+    ]
   );
 }
 
 function Step(props: StepProps) {
-  const {currentStep, registerStep, getStepNumber} = useGuidedStepsContext();
+  const {advanceToNextIncompleteStep, currentStep, registerStep, getStepNumber} =
+    useGuidedStepsContext();
   const stepNumber = getStepNumber(props.stepKey);
   const isActive = currentStep === stepNumber;
   const isCompleted = props.isCompleted ?? currentStep > stepNumber;
+  const previousIsCompleted = usePrevious(isCompleted);
 
   useEffect(() => {
     registerStep({isCompleted: props.isCompleted, stepKey: props.stepKey});
   }, [props.isCompleted, props.stepKey, registerStep]);
 
+  useEffect(() => {
+    if (!previousIsCompleted && isCompleted && isActive) {
+      advanceToNextIncompleteStep();
+    }
+  }, [advanceToNextIncompleteStep, isActive, isCompleted, previousIsCompleted]);
+
   return (
     <StepWrapper data-test-id={`guided-step-${stepNumber}`}>
       <StepNumber isActive={isActive}>{stepNumber}</StepNumber>