Skip to content

Commit

Permalink
Merge pull request #589 from open-formulieren/task/36-refactor-cleanu…
Browse files Browse the repository at this point in the history
…p-progress-indicator

[#36] Refactor Progress Indicator
  • Loading branch information
sergei-maertens authored Nov 20, 2023
2 parents e8cb7f5 + 647f2c7 commit 7aee061
Show file tree
Hide file tree
Showing 32 changed files with 1,073 additions and 1,074 deletions.
27 changes: 27 additions & 0 deletions .storybook/test-runner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
const {getStoryContext} = require('@storybook/test-runner');
const {MINIMAL_VIEWPORTS} = require('@storybook/addon-viewport');

const DEFAULT_VIEWPORT_SIZE = {width: 1280, height: 720};

module.exports = {
async preRender(page, story) {
const context = await getStoryContext(page, story);
const viewportName = context.parameters?.viewport?.defaultViewport;
const viewportParameter = MINIMAL_VIEWPORTS[viewportName];

if (viewportParameter) {
const viewportSize = Object.entries(viewportParameter.styles).reduce(
(acc, [screen, size]) => ({
...acc,
// make sure your viewport config in Storybook only uses numbers, not percentages
[screen]: parseInt(size),
}),
{}
);

page.setViewportSize(viewportSize);
} else {
page.setViewportSize(DEFAULT_VIEWPORT_SIZE);
}
},
};
42 changes: 42 additions & 0 deletions src/api-mocks/submissions.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,29 @@ const SUBMISSION_DETAILS = {
},
};

// mock for /api/v2/submissions/{submission_uuid}/steps/{step_uuid}
const SUBMISSION_STEP_DETAILS = {
id: '58aad9c3-29c7-4568-9047-3ac7ceb0f0ff',
slug: 'step-1',
formStep: {
index: 0,
configuration: {
components: [
{
id: 'asdiwj',
type: 'textfield',
key: 'component1',
label: 'Component 1',
},
],
},
},
data: null,
isApplicable: true,
completed: false,
canSubmit: true,
};

/**
* Return a submission object as if it would be returned from the backend API.
* @param {Object} overrides Key-value mapping with overrides from the defaults. These
Expand All @@ -41,6 +64,25 @@ export const mockSubmissionPost = (submission = buildSubmission()) =>
return res(ctx.status(201), ctx.json(submission));
});

export const mockSubmissionGet = () =>
rest.get(`${BASE_URL}submissions/:uuid`, (req, res, ctx) => {
return res(ctx.status(200), ctx.json(SUBMISSION_DETAILS));
});

export const mockSubmissionStepGet = () =>
rest.get(`${BASE_URL}submissions/:uuid/steps/:uuid`, (req, res, ctx) => {
return res(ctx.status(200), ctx.json(SUBMISSION_STEP_DETAILS));
});

export const mockSubmissionCheckLogicPost = () =>
rest.post(`${BASE_URL}submissions/:uuid/steps/:uuid/_check_logic`, (req, res, ctx) => {
const responseData = {
submission: SUBMISSION_DETAILS,
step: SUBMISSION_STEP_DETAILS,
};
return res(ctx.status(200), ctx.json(responseData));
});

/**
* Simulate a succesful backend processing status without payment.
*/
Expand Down
1 change: 1 addition & 0 deletions src/components/Anchor/Anchor.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ The standard look of the anchor component without modifiers.
<Story of={AnchorStories.Muted} />
<Story of={AnchorStories.Indent} />
<Story of={AnchorStories.Inherit} />
<Story of={AnchorStories.Placeholder} />
</Canvas>

## Props
Expand Down
8 changes: 8 additions & 0 deletions src/components/Anchor/Anchor.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,11 @@ export const Inherit = {
label: 'Inherit',
},
};

export const Placeholder = {
render,
args: {
label: 'placeholder',
placeholder: true,
},
};
111 changes: 110 additions & 1 deletion src/components/App.stories.js
Original file line number Diff line number Diff line change
@@ -1,24 +1,72 @@
import {expect} from '@storybook/jest';
import {waitForElementToBeRemoved, within} from '@storybook/testing-library';
import {userEvent, waitForElementToBeRemoved, within} from '@storybook/testing-library';
import {RouterProvider, createMemoryRouter} from 'react-router-dom';

import {FormContext} from 'Context';
import {BASE_URL} from 'api-mocks';
import {buildForm} from 'api-mocks';
import {
mockSubmissionCheckLogicPost,
mockSubmissionGet,
mockSubmissionPost,
mockSubmissionStepGet,
} from 'api-mocks/submissions';
import {mockLanguageChoicePut, mockLanguageInfoGet} from 'components/LanguageSelection/mocks';
import {ConfigDecorator, LayoutDecorator} from 'story-utils/decorators';

import App, {routes as nestedRoutes} from './App';
import {SUBMISSION_ALLOWED} from './constants';

export default {
title: 'Private API / App',
component: App,
decorators: [LayoutDecorator, ConfigDecorator],
args: {
'form.translationEnabled': true,
submissionAllowed: SUBMISSION_ALLOWED.yes,
hideNonApplicableSteps: false,
steps: [
{
uuid: '9e6eb3c5-e5a4-4abf-b64a-73d3243f2bf5',
slug: 'step-1',
formDefinition: 'Step 1',
index: 0,
literals: {
previousText: {resolved: 'Previous', value: ''},
saveText: {resolved: 'Save', value: ''},
nextText: {resolved: 'Next', value: ''},
},
url: `${BASE_URL}forms/mock/steps/9e6eb3c5-e5a4-4abf-b64a-73d3243f2bf5`,
isApplicable: true,
completed: false,
},
{
uuid: '98980oi8-e5a4-4abf-b64a-76j3j3ki897',
slug: 'step-2',
formDefinition: 'Step 2',
index: 0,
literals: {
previousText: {resolved: 'Previous', value: ''},
saveText: {resolved: 'Save', value: ''},
nextText: {resolved: 'Next', value: ''},
},
url: `${BASE_URL}forms/mock/steps/98980oi8-e5a4-4abf-b64a-76j3j3ki897`,
isApplicable: false,
completed: false,
},
],
},
argTypes: {
form: {table: {disable: true}},
noDebug: {table: {disable: true}},
submissionAllowed: {
options: Object.values(SUBMISSION_ALLOWED),
control: {type: 'radio'},
'submission.submissionAllowed': {
options: Object.values(SUBMISSION_ALLOWED),
control: {type: 'radio'},
},
},
},
parameters: {
msw: {
Expand Down Expand Up @@ -56,6 +104,9 @@ const render = args => {
const form = buildForm({
translationEnabled: args['form.translationEnabled'],
explanationTemplate: '<p>Toelichtingssjabloon...</p>',
submissionAllowed: args['submissionAllowed'],
hideNonApplicableSteps: args['hideNonApplicableSteps'],
steps: args['steps'],
});
return <Wrapper form={form} />;
};
Expand Down Expand Up @@ -92,3 +143,61 @@ export const TranslationDisabled = {
await expect(langSelector).toBeNull();
},
};

export const ActiveSubmission = {
name: 'Active submission',
render,
decorators: [
// remove the window.localStorage entry, UUID value is from `api-mocks/forms.js`.
// it gets set because of the play function which starts a submission.
Story => {
const key = 'e450890a-4166-410e-8d64-0a54ad30ba01';
window.localStorage.removeItem(key);
return <Story />;
},
],
args: {
steps: [
{
uuid: '9e6eb3c5-e5a4-4abf-b64a-73d3243f2bf5',
slug: 'step-1',
formDefinition: 'Step 1',
index: 0,
literals: {
previousText: {resolved: 'Previous', value: ''},
saveText: {resolved: 'Save', value: ''},
nextText: {resolved: 'Next', value: ''},
},
url: `${BASE_URL}forms/mock/steps/9e6eb3c5-e5a4-4abf-b64a-73d3243f2bf5`,
isApplicable: true,
completed: false,
},
],
},
argTypes: {
hideNonApplicableSteps: {table: {disable: true}},
submissionAllowed: {table: {disable: true}},
},
parameters: {
msw: {
handlers: [
mockSubmissionPost(),
mockSubmissionGet(),
mockSubmissionStepGet(),
mockSubmissionCheckLogicPost(),
mockLanguageInfoGet([
{code: 'nl', name: 'Nederlands'},
{code: 'en', name: 'English'},
]),
mockLanguageChoicePut,
],
},
},

play: async ({canvasElement}) => {
const canvas = within(canvasElement);

const beginButton = await canvas.findByRole('button', {name: 'Begin'});
await userEvent.click(beginButton);
},
};
81 changes: 74 additions & 7 deletions src/components/Form.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, {useContext, useEffect} from 'react';
import {useIntl} from 'react-intl';
import {Navigate, Route, Routes, useMatch, useNavigate} from 'react-router-dom';
import {Navigate, Route, Routes, useLocation, useMatch, useNavigate} from 'react-router-dom';
import {usePrevious} from 'react-use';
import {useImmerReducer} from 'use-immer';

Expand All @@ -27,6 +27,9 @@ import useQuery from 'hooks/useQuery';
import useRecycleSubmission from 'hooks/useRecycleSubmission';
import useSessionTimeout from 'hooks/useSessionTimeout';

import {addFixedSteps, getStepsInfo} from './ProgressIndicator/utils';
import {PI_TITLE, STEP_LABELS, SUBMISSION_ALLOWED} from './constants';

const initialState = {
submission: null,
submittedSubmission: null,
Expand Down Expand Up @@ -97,6 +100,12 @@ const Form = () => {
usePageViews();
const intl = useIntl();
const prevLocale = usePrevious(intl.locale);
const {pathname: currentPathname} = useLocation();

// TODO replace absolute path check with relative
const stepMatch = useMatch('/stap/:step');
const summaryMatch = useMatch('/overzicht');
const confirmationMatch = useMatch('/bevestiging');

// extract the declared properties and configuration
const {steps} = form;
Expand Down Expand Up @@ -260,14 +269,72 @@ const Form = () => {
return <Loader modifiers={['centered']} />;
}

// Progress Indicator

const isStartPage = !summaryMatch && stepMatch == null && !confirmationMatch;
const submissionAllowedSpec = state.submission?.submissionAllowed ?? form.submissionAllowed;
const showOverview = submissionAllowedSpec !== SUBMISSION_ALLOWED.noWithoutOverview;
const showConfirmation = submissionAllowedSpec === SUBMISSION_ALLOWED.yes;
const submission = state.submission || state.submittedSubmission;
const isCompleted = state.completed;
const formName = form.name;

// Figure out the slug from the currently active step IF we're looking at a step
const stepSlug = stepMatch ? stepMatch.params.step : '';

// figure out the title for the mobile menu based on the state
let activeStepTitle;
if (isStartPage) {
activeStepTitle = intl.formatMessage(STEP_LABELS.login);
} else if (summaryMatch) {
activeStepTitle = intl.formatMessage(STEP_LABELS.overview);
} else if (confirmationMatch) {
activeStepTitle = intl.formatMessage(STEP_LABELS.confirmation);
} else {
const step = steps.find(step => step.slug === stepSlug);
activeStepTitle = step.formDefinition;
}

const ariaMobileIconLabel = intl.formatMessage({
description: 'Progress step indicator toggle icon (mobile)',
defaultMessage: 'Toggle the progress status display',
});

const accessibleToggleStepsLabel = intl.formatMessage(
{
description: 'Active step accessible label in mobile progress indicator',
defaultMessage: 'Current step in form {formName}: {activeStepTitle}',
},
{formName, activeStepTitle}
);

let applicableSteps = [];
if (form.hideNonApplicableSteps) {
applicableSteps = steps.filter(step => step.isApplicable);
}

const updatedSteps = getStepsInfo(
applicableSteps.length > 0 ? applicableSteps : form.steps,
submission,
currentPathname
);
const stepsToRender = addFixedSteps(
intl,
updatedSteps,
submission,
currentPathname,
showOverview,
showConfirmation,
isCompleted
);

const progressIndicator = form.showProgressIndicator ? (
<ProgressIndicator
title={form.name}
steps={form.steps}
hideNonApplicableSteps={form.hideNonApplicableSteps}
submission={state.submission || state.submittedSubmission}
submissionAllowed={form.submissionAllowed}
completed={state.completed}
title={PI_TITLE}
formTitle={formName}
steps={stepsToRender}
ariaMobileIconLabel={ariaMobileIconLabel}
accessibleToggleStepsLabel={accessibleToggleStepsLabel}
/>
) : null;

Expand Down
Loading

0 comments on commit 7aee061

Please sign in to comment.