Skip to content

Commit

Permalink
feat(FormRenderer-Wizard): Support onNavigating event (#967)
Browse files Browse the repository at this point in the history
  • Loading branch information
jessieweiyi authored Sep 13, 2023
1 parent 702a216 commit 5fe37b5
Show file tree
Hide file tree
Showing 3 changed files with 133 additions and 13 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
******************************************************************************************************************** */
import { ComponentMeta } from '@storybook/react';
import Box from '@cloudscape-design/components/box';
import { WizardProps as WizardComponentProps } from '@cloudscape-design/components/wizard';
import { NonCancelableCustomEvent } from '@cloudscape-design/components/internal/events';
import FormRenderer, { componentTypes, validatorTypes } from '../..';
import { Template, DEFAULT_ARGS } from '../../index.stories';

Expand Down Expand Up @@ -158,3 +160,32 @@ Disabled.args = {
})),
},
};

export const NavigatingEvent = Template.bind({});
NavigatingEvent.args = {
...Default.args,
schema: {
...Default.args.schema,
fields: [
{
...Default.args.schema!.fields[0],
onNavigating: (event: NonCancelableCustomEvent<WizardComponentProps.NavigateDetail>) => {
return new Promise((resolve) => {
window.setTimeout(() => {
if (event.detail.requestedStepIndex >= 2) {
resolve({
continued: false,
errorText: 'Unable to proceed',
});
} else {
resolve({
continued: true,
});
}
}, 2000);
});
},
},
],
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import wrapper from '@cloudscape-design/components/test-utils/dom';
import { composeStories } from '@storybook/testing-react';
import * as stories from './index.stories';

const { Default, WithInitialValue, Submittting } = composeStories(stories);
const { Default, WithInitialValue, Submittting, NavigatingEvent } = composeStories(stories);

const handleCancel = jest.fn();
const handleSubmit = jest.fn();
Expand Down Expand Up @@ -145,4 +145,38 @@ describe('Wizard', () => {
expect(screen.getByText('Submit').parentElement).toHaveAttribute('aria-disabled', 'true');
expect(screen.getByText('Previous').parentElement).toBeDisabled();
});

it('should render wizard with onNavigating event handler', async () => {
render(<NavigatingEvent onSubmit={handleSubmit} onCancel={handleCancel} />);

const element = await screen.findByTestId('form-renderer');
const wizard = wrapper(element).findWizard();

expect(wizard?.findHeader()?.getElement()).toHaveTextContent('Step 1');

await act(async () => {
await userEvent.click(wizard?.findPrimaryButton().getElement()!);
});
expect(screen.getByText('Required')).toBeVisible();

await act(async () => {
await userEvent.type(screen.getByLabelText('Config 1'), 'Config 1 Content');
await userEvent.click(wizard?.findPrimaryButton().getElement()!);
});

expect(wizard?.findPrimaryButton().findLoadingIndicator()).not.toBeNull();

await waitFor(() => expect(wizard?.findHeader()?.getElement()).toHaveTextContent('Step 2'), {
timeout: 3000,
});

await act(async () => {
await userEvent.type(screen.getByLabelText('Config 2'), 'Config 2 Content');
await userEvent.click(wizard?.findPrimaryButton().getElement()!);
});

await waitFor(() => expect(screen.getByText('Unable to proceed')).toBeVisible(), {
timeout: 3000,
});
});
});
79 changes: 67 additions & 12 deletions packages/ui/src/components/FormRenderer/components/Wizard/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,27 @@
import { FC, memo, useCallback, useEffect, useMemo, useState } from 'react';
import useFormApi from '@data-driven-forms/react-form-renderer/use-form-api';
import WizardComponent, { WizardProps as WizardComponentProps } from '@cloudscape-design/components/wizard';
import { NonCancelableCustomEvent, NonCancelableEventHandler } from '@cloudscape-design/components/internal/events';
import { Field } from '../../types';
import WizardSteps from './components/WizardSteps';
import { useFormRendererContext } from '../../formRendererContext';

export interface NavigatingEventResponse {
continued: boolean;
errorText?: string;
}

export interface WizardProps {
fields: Field[];
allowSkipTo: WizardComponentProps['allowSkipTo'];
i18nStrings: WizardComponentProps['i18nStrings'];
activeStepIndex: WizardComponentProps['activeStepIndex'];
isReadOnly?: boolean;
isDisabled?: boolean;
onNavigating?: (
event: NonCancelableCustomEvent<WizardComponentProps.NavigateDetail>
) => Promise<NavigatingEventResponse>;
isLoadingNextStep?: WizardComponentProps['isLoadingNextStep'];
}

const DEFAULT_RESOURCE_STRINGS: WizardComponentProps.I18nStrings = {
Expand All @@ -41,11 +51,25 @@ const DEFAULT_RESOURCE_STRINGS: WizardComponentProps.I18nStrings = {
optional: 'optional',
};

const Wizard: FC<WizardProps> = ({ i18nStrings, allowSkipTo, fields, isReadOnly, isDisabled, ...props }) => {
const Wizard: FC<WizardProps> = ({
i18nStrings,
allowSkipTo,
fields,
isReadOnly,
isDisabled,
onNavigating,
...props
}) => {
const [showError, setShowError] = useState(false);
const formOptions = useFormApi();
const { isSubmitting } = useFormRendererContext();
const [activeStepIndex, setActiveStepIndex] = useState(props.activeStepIndex || 0);
const [isLoadingNextStep, setIsLoadingNextStep] = useState(props.isLoadingNextStep || isSubmitting);
const [errorText, setErrorText] = useState<string>();

useEffect(() => {
setIsLoadingNextStep(props.isLoadingNextStep || isSubmitting);
}, [props.isLoadingNextStep, isSubmitting]);

const resourceStrings = useMemo(() => {
return {
Expand All @@ -56,13 +80,13 @@ const Wizard: FC<WizardProps> = ({ i18nStrings, allowSkipTo, fields, isReadOnly,

const steps = useMemo(() => {
return fields.map((field) => {
const { title, info, description, isOptional, errorText, fields: fieldFields, ...rest } = field;
const { title, info, description, isOptional, fields: fieldFields, ...rest } = field;
return {
title: title,
info: info,
description: field.description,
isOptional: field.isOptional,
errorText: field.errorText,
errorText: field.errorText || errorText,
content: (
<WizardSteps
isReadOnly={isReadOnly}
Expand All @@ -74,26 +98,57 @@ const Wizard: FC<WizardProps> = ({ i18nStrings, allowSkipTo, fields, isReadOnly,
),
};
});
}, [fields, showError, isReadOnly, isDisabled]);
}, [fields, showError, isReadOnly, isDisabled, errorText]);

useEffect(() => {
setShowError(false); // When steps change
}, [activeStepIndex]);

const handleNavigation = useCallback(
({ detail }: { detail: WizardComponentProps.NavigateDetail }) => {
const requestedStepIndex = detail.requestedStepIndex;
if (activeStepIndex < detail.requestedStepIndex) {
const handleNavigating = useCallback(
async (event: NonCancelableCustomEvent<WizardComponentProps.NavigateDetail>) => {
if (onNavigating) {
setIsLoadingNextStep(true);
const response = await onNavigating(event);
setIsLoadingNextStep(false);
return response;
}

return {
continued: true,
errorText: undefined,
};
},
[onNavigating]
);

const processNavigate = useCallback(
async (requestedStepIndex: number, event: NonCancelableCustomEvent<WizardComponentProps.NavigateDetail>) => {
const response = await handleNavigating(event);
if (response.continued) {
setActiveStepIndex(requestedStepIndex);
} else {
setErrorText(response.errorText);
}
},
[handleNavigating]
);

const handleNavigation: NonCancelableEventHandler<WizardComponentProps.NavigateDetail> = useCallback(
async (event) => {
setErrorText(undefined);
const requestedStepIndex = event.detail.requestedStepIndex;

if (activeStepIndex < event.detail.requestedStepIndex) {
const state = formOptions.getState();
setShowError(true);
if (!(state.invalid || state.validating || state.submitting)) {
setActiveStepIndex(requestedStepIndex);
processNavigate(requestedStepIndex, event);
}
} else {
setActiveStepIndex(requestedStepIndex);
processNavigate(requestedStepIndex, event);
}
},
[activeStepIndex, formOptions]
[activeStepIndex, formOptions, processNavigate]
);

return (
Expand All @@ -103,7 +158,7 @@ const Wizard: FC<WizardProps> = ({ i18nStrings, allowSkipTo, fields, isReadOnly,
onNavigate={handleNavigation}
activeStepIndex={activeStepIndex}
allowSkipTo={allowSkipTo}
isLoadingNextStep={isSubmitting}
isLoadingNextStep={isLoadingNextStep}
steps={steps}
onSubmit={formOptions.handleSubmit}
onCancel={formOptions.onCancel}
Expand Down

0 comments on commit 5fe37b5

Please sign in to comment.