diff --git a/.storybook/test-runner.js b/.storybook/test-runner.js
new file mode 100644
index 000000000..58c7cd9ca
--- /dev/null
+++ b/.storybook/test-runner.js
@@ -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);
+ }
+ },
+};
diff --git a/src/api-mocks/submissions.js b/src/api-mocks/submissions.js
index 8858b7e53..1708aba32 100644
--- a/src/api-mocks/submissions.js
+++ b/src/api-mocks/submissions.js
@@ -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
@@ -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.
*/
diff --git a/src/components/Anchor/Anchor.mdx b/src/components/Anchor/Anchor.mdx
index e568db0e7..120dce313 100644
--- a/src/components/Anchor/Anchor.mdx
+++ b/src/components/Anchor/Anchor.mdx
@@ -31,6 +31,7 @@ The standard look of the anchor component without modifiers.
+
## Props
diff --git a/src/components/Anchor/Anchor.stories.js b/src/components/Anchor/Anchor.stories.js
index 4fdc55e89..d95889291 100644
--- a/src/components/Anchor/Anchor.stories.js
+++ b/src/components/Anchor/Anchor.stories.js
@@ -70,3 +70,11 @@ export const Inherit = {
label: 'Inherit',
},
};
+
+export const Placeholder = {
+ render,
+ args: {
+ label: 'placeholder',
+ placeholder: true,
+ },
+};
diff --git a/src/components/App.stories.js b/src/components/App.stories.js
index 6fa3b353b..5c14ab79d 100644
--- a/src/components/App.stories.js
+++ b/src/components/App.stories.js
@@ -1,13 +1,21 @@
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',
@@ -15,10 +23,50 @@ export default {
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: {
@@ -56,6 +104,9 @@ const render = args => {
const form = buildForm({
translationEnabled: args['form.translationEnabled'],
explanationTemplate: '
Toelichtingssjabloon...
',
+ submissionAllowed: args['submissionAllowed'],
+ hideNonApplicableSteps: args['hideNonApplicableSteps'],
+ steps: args['steps'],
});
return ;
};
@@ -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 ;
+ },
+ ],
+ 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);
+ },
+};
diff --git a/src/components/Form.js b/src/components/Form.js
index ef46d593d..73f9221b8 100644
--- a/src/components/Form.js
+++ b/src/components/Form.js
@@ -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';
@@ -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,
@@ -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;
@@ -260,14 +269,72 @@ const Form = () => {
return ;
}
+ // 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 ? (
) : null;
diff --git a/src/components/Link.js b/src/components/Link.js
index 9931b5578..6486e2d25 100644
--- a/src/components/Link.js
+++ b/src/components/Link.js
@@ -6,28 +6,35 @@ import Anchor from 'components/Anchor';
/**
* Custom Link component using the design system component, replacing react-router's Link.
*/
-const Link = React.forwardRef(({onClick, replace = false, state, target, to, ...rest}, ref) => {
- const href = useHref(to);
- const handleClick = useLinkClickHandler(to, {
- replace,
- state,
- target,
- });
- return (
- {
- onClick?.(event);
- if (!event.defaultPrevented) {
- handleClick(event);
- }
- }}
- ref={ref}
- target={target}
- />
- );
-});
+const Link = React.forwardRef(
+ ({onClick, placeholder, replace = false, state, target, to, ...rest}, ref) => {
+ const href = useHref(to);
+ const handleClick = useLinkClickHandler(to, {
+ replace,
+ state,
+ target,
+ });
+ return (
+ {
+ if (placeholder) {
+ event.preventDefault();
+ return;
+ }
+ onClick?.(event);
+ if (!event.defaultPrevented) {
+ handleClick(event);
+ }
+ }}
+ ref={ref}
+ target={target}
+ placeholder={placeholder}
+ />
+ );
+ }
+);
// Prop types deliberately unspecified, please use the typescript definitions of
// react-router-dom instead.
diff --git a/src/components/ProgressIndicator/MobileButton.js b/src/components/ProgressIndicator/MobileButton.js
new file mode 100644
index 000000000..13a0909bf
--- /dev/null
+++ b/src/components/ProgressIndicator/MobileButton.js
@@ -0,0 +1,42 @@
+import PropTypes from 'prop-types';
+
+import FAIcon from 'components/FAIcon';
+import {getBEMClassName} from 'utils';
+
+const MobileButton = ({
+ ariaMobileIconLabel,
+ accessibleToggleStepsLabel,
+ formTitle,
+ expanded,
+ onExpandClick,
+}) => {
+ return (
+
+ );
+};
+
+MobileButton.propTypes = {
+ ariaMobileIconLabel: PropTypes.string.isRequired,
+ accessibleToggleStepsLabel: PropTypes.string.isRequired,
+ formTitle: PropTypes.string.isRequired,
+ expanded: PropTypes.bool,
+ onExpandClick: PropTypes.func.isRequired,
+};
+
+export default MobileButton;
diff --git a/src/components/ProgressIndicator/ProgressIndicator.mdx b/src/components/ProgressIndicator/ProgressIndicator.mdx
index 6741c2222..232ee43d3 100644
--- a/src/components/ProgressIndicator/ProgressIndicator.mdx
+++ b/src/components/ProgressIndicator/ProgressIndicator.mdx
@@ -2,7 +2,6 @@ import {ArgTypes, Canvas, Meta, Story} from '@storybook/blocks';
import ProgressIndicator from '.';
import * as ProgressIndicatorStories from './ProgressIndicator.stories';
-import ProgressIndicatorDisplay from './ProgressIndicatorDisplay';
@@ -12,24 +11,34 @@ The progress indicator displays the progression through a particular form's step
## Presentation
-The `ProgressIndicatorDisplay` component shows the different steps in a form. The style of each step
-varies depending on whether it is applicable, it is completed or it is the currently active step.
+The `ProgressIndicator` component shows the different steps (`ProgressIndicatorItem`) in a form. The
+style of each step varies depending on whether it is applicable, it is completed or it is the
+currently active step.
-
+**Warning**
-### Props
-
-
+Since SDK 2.1 we've restructured the `ProgressIndicator` component - swapping out an alternative
+component now requires you to specify `progressIndicator` rather than `progressIndicatorDisplay`.
+The interface has also changed.
## Functional
-The `ProgressIndicator` component handles the 'smart' behaviour. It checks:
+The parent component must handle all the 'smart' behaviour such as constructing the list of items
+each with their individual states. The `Form` component is an example of this behaviour, it checks:
- If the steps are currently active, completed, applicable or if a step can be clicked to navigate
to it.
- If the current page is the start page, the summary page or the confirmation page.
- If all the applicable steps have been completed.
+The `ProgressIndicator` component passes through all the individual steps (dynamic and fixed) to the
+`ProgressIndicatorItem` component and checks:
+
+- If the container should be shown for a desktop or a mobile version.
+
+The `ProgressIndicatorItem` component renders all the steps as links or as simple texts depending on
+the props.
+
It is responsible for rendering the `ProgressIndicatorDisplay` component.
The current step is detected through the URL route match, based on the `slug` of an individual step.
@@ -39,15 +48,11 @@ passed as prop through the component.
Note that a particular step may become not-applicable while filling out the form because of certain
logic rules. The details of each step's status are available in `submission.steps[@index]`.
-
-
## Form start
On initial load, no submission is available yet, and the overview is rendered based on the form
rather than the submission.
-
-
## Props
diff --git a/src/components/ProgressIndicator/ProgressIndicator.stories.js b/src/components/ProgressIndicator/ProgressIndicator.stories.js
index 4d23b5321..ce780c26b 100644
--- a/src/components/ProgressIndicator/ProgressIndicator.stories.js
+++ b/src/components/ProgressIndicator/ProgressIndicator.stories.js
@@ -1,203 +1,85 @@
-import {useArgs} from '@storybook/client-api';
-import omit from 'lodash/omit';
+import {MINIMAL_VIEWPORTS} from '@storybook/addon-viewport';
+import {expect} from '@storybook/jest';
+import {userEvent, within} from '@storybook/testing-library';
import {withRouter} from 'storybook-addon-react-router-v6';
-import {buildSubmission} from 'api-mocks';
-import {SUBMISSION_ALLOWED} from 'components/constants';
-
import ProgressIndicator from '.';
-import ProgressIndicatorDisplay from './ProgressIndicatorDisplay';
export default {
- title: 'Composites / ProgressIndicator',
+ title: 'Private API / ProgressIndicator',
component: ProgressIndicator,
decorators: [withRouter],
- argTypes: {
- submissionAllowed: {
- options: Object.values(SUBMISSION_ALLOWED),
- control: {type: 'radio'},
- },
- 'submission.submissionAllowed': {
- options: Object.values(SUBMISSION_ALLOWED),
- control: {type: 'radio'},
- },
- submission: {table: {disable: true}},
- },
-};
-
-export const Display = {
- render: args => {
- const [_, updateArgs] = useArgs();
- return (
- updateArgs({...args, expanded: !args.expanded})}
- {...args}
- />
- );
- },
args: {
- activeStepTitle: 'Stap 2',
- formTitle: 'Storybookformulier',
+ title: 'Progress',
+ formTitle: 'Formulier',
steps: [
{
- uuid: '111',
- slug: 'stap1',
- formDefinition: 'Stap 1',
+ to: 'start-page',
+ label: 'Start page',
+ isCompleted: true,
+ isApplicable: true,
+ isCurrent: false,
+ canNavigateTo: true,
+ },
+ {
+ to: 'first-step',
+ label: 'Stap 1',
isCompleted: true,
isApplicable: true,
isCurrent: false,
canNavigateTo: true,
},
{
- uuid: '222',
- slug: 'stap2',
- formDefinition: 'Stap 2',
+ to: 'second-step',
+ label: 'Stap 2',
isCompleted: false,
isApplicable: true,
isCurrent: true,
canNavigateTo: true,
},
{
- uuid: '333',
- slug: 'stap3',
- formDefinition: 'Stap 3',
+ to: 'summary-page',
+ label: 'Summary',
isCompleted: false,
- isApplicable: false,
+ isApplicable: true,
isCurrent: false,
canNavigateTo: false,
},
- ],
- hasSubmission: true,
- isStartPage: false,
- isSummary: false,
- isConfirmation: false,
- isSubmissionComplete: false,
- areApplicableStepsCompleted: false,
- showOverview: true,
- showConfirmation: false,
- expanded: false,
- },
- argTypes: {
- title: {table: {disable: true}},
- 'submission.submissionAllowed': {table: {disable: true}},
- submissionAllowed: {table: {disable: true}},
- completed: {table: {disable: true}},
- },
-};
-
-const render = ({
- title,
- submissionAllowed,
- completed,
- steps = [],
- hideNonApplicableSteps = false,
- withoutSubmission = false,
- ...args
-}) => {
- const _submission = buildSubmission({
- submissionAllowed: args['submission.submissionAllowed'],
- steps,
- payment: {
- isRequired: false,
- hasPaid: false,
- },
- });
- return (
- omit(step, ['completed', 'isApplicable']))}
- submissionAllowed={submissionAllowed}
- completed={completed}
- hideNonApplicableSteps={hideNonApplicableSteps}
- />
- );
-};
-
-export const ActiveSubmission = {
- name: 'Active submission',
- render,
- args: {
- // story args
- steps: [
{
- url: 'https://backend/api/v1/form/9d49e773/steps/d6cab0dd',
- uuid: 'd6cab0dd',
- index: 0,
- slug: 'first-step',
- formDefinition: 'Stap 1',
+ to: 'confirmation-page',
+ label: 'Confirmation',
+ isCompleted: false,
isApplicable: true,
- completed: true,
- },
- {
- url: 'https://backend/api/v1/form/9d49e773/steps/8e62d7cf',
- uuid: '8e62d7cf',
- index: 1,
- slug: 'second-step',
- formDefinition: 'Stap 2',
- isApplicable: false,
- completed: false,
+ isCurrent: false,
+ canNavigateTo: false,
},
],
- 'submission.submissionAllowed': SUBMISSION_ALLOWED.yes,
- // component props
- title: 'Storybookformulier',
- submissionAllowed: SUBMISSION_ALLOWED.yes,
- completed: false,
- hideNonApplicableSteps: false,
- },
- argTypes: {
- submissionAllowed: {table: {disable: true}},
+ ariaMobileIconLabel: 'Progress step indicator toggle icon (mobile)',
+ accessibleToggleStepsLabel: 'Current step in form Formulier: Stap 2',
},
parameters: {
- reactRouter: {
- routePath: '/stap/:step',
- routeParams: {step: 'first-step'},
+ viewport: {
+ viewports: MINIMAL_VIEWPORTS,
},
},
};
-export const InitialFormLoad = {
- name: 'Initial form load',
- render,
- args: {
- // story args
- steps: [
- {
- url: 'https://backend/api/v1/form/9d49e773/steps/d6cab0dd',
- uuid: 'd6cab0dd',
- index: 0,
- slug: 'first-step',
- formDefinition: 'Stap 1',
- isApplicable: true,
- completed: false,
- },
- {
- url: 'https://backend/api/v1/form/9d49e773/steps/8e62d7cf',
- uuid: '8e62d7cf',
- index: 1,
- slug: 'second-step',
- formDefinition: 'Stap 2',
- isApplicable: true,
- completed: false,
- },
- ],
- 'submission.submissionAllowed': SUBMISSION_ALLOWED.yes,
- withoutSubmission: true,
- // component props
- title: 'Initial load',
- submissionAllowed: SUBMISSION_ALLOWED.yes,
- completed: false,
- },
- argTypes: {
- 'submission.submissionAllowed': {table: {disable: true}},
- withoutSubmission: {table: {disable: true}},
- completed: {table: {disable: true}},
- hideNonApplicableSteps: {table: {disable: true}},
- },
+export const Default = {};
+
+export const MobileViewport = {
+ name: 'Mobile version',
parameters: {
- reactRouter: {
- routePath: '/startpagina',
+ // TODO enable viewport in chromatic
+ chromatic: {disableSnapshot: true},
+ viewport: {
+ defaultViewport: 'mobile1',
},
},
+ play: async ({canvasElement}) => {
+ const canvas = within(canvasElement);
+
+ const toggleButton = await canvas.findByRole('button');
+ await expect(toggleButton).toHaveAttribute('aria-pressed', 'false');
+ await userEvent.click(toggleButton);
+ },
};
diff --git a/src/components/ProgressIndicator/ProgressIndicatorDisplay.js b/src/components/ProgressIndicator/ProgressIndicatorDisplay.js
deleted file mode 100644
index 53123177d..000000000
--- a/src/components/ProgressIndicator/ProgressIndicatorDisplay.js
+++ /dev/null
@@ -1,234 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-import {FormattedMessage, useIntl} from 'react-intl';
-
-import Anchor from 'components/Anchor';
-import Body from 'components/Body';
-import Caption from 'components/Caption';
-import Card from 'components/Card';
-import FAIcon from 'components/FAIcon';
-import Link from 'components/Link';
-import List from 'components/List';
-import {getBEMClassName} from 'utils';
-
-import ProgressItem from './ProgressItem';
-import {STEP_LABELS} from './constants';
-
-const getLinkModifiers = (active, isApplicable) => {
- return [
- 'inherit',
- 'hover',
- active ? 'active' : undefined,
- isApplicable ? undefined : 'muted',
- ].filter(mod => mod !== undefined);
-};
-
-const LinkOrSpan = ({isActive, isApplicable, to, useLink, children, ...props}) => {
- if (useLink) {
- return (
-
- {children}
-
- );
- }
-
- return (
-
- {children}
-
- );
-};
-
-LinkOrSpan.propTypes = {
- to: PropTypes.string.isRequired,
- useLink: PropTypes.bool.isRequired,
- isActive: PropTypes.bool.isRequired,
- isApplicable: PropTypes.bool.isRequired,
-};
-
-const SidebarStepStatus = ({
- to,
- formDefinition,
- canNavigate,
- isCurrent,
- isApplicable = false,
- completed = false,
-}) => {
- return (
-
-
-
-
-
- );
-};
-
-SidebarStepStatus.propTypes = {
- to: PropTypes.string.isRequired,
- formDefinition: PropTypes.string.isRequired,
- isCurrent: PropTypes.bool.isRequired,
- completed: PropTypes.bool,
- canNavigate: PropTypes.bool,
- isApplicable: PropTypes.bool,
-};
-
-const ProgressIndicatorDisplay = ({
- activeStepTitle,
- formTitle,
- steps,
- hideNonApplicableSteps,
- hasSubmission,
- isStartPage,
- isSummary,
- isConfirmation,
- isSubmissionComplete,
- areApplicableStepsCompleted,
- showOverview,
- showConfirmation,
- expanded = false,
- onExpandClick,
- sticky = true,
- summaryTo = '/overzicht',
-}) => {
- const intl = useIntl();
- // aria-labels are passed to DOM element, which can't handle , so we
- // use imperative API
- if (hideNonApplicableSteps) {
- steps = steps.filter(step => step.isApplicable);
- }
- const ariaIconLabel = 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 {formTitle}: {activeStepTitle}',
- },
- {formTitle, activeStepTitle}
- );
-
- const modifiers = [];
- if (sticky) {
- modifiers.push('sticky');
- }
- if (!expanded) {
- modifiers.push('mobile-collapsed');
- }
- return (
-
-
-
- );
-};
-
-// TODO: clean up props to be more generic and apply mapping in call-sites
-ProgressIndicatorDisplay.propTypes = {
- activeStepTitle: PropTypes.node,
- formTitle: PropTypes.string,
- steps: PropTypes.arrayOf(
- PropTypes.shape({
- uuid: PropTypes.string.isRequired,
- slug: PropTypes.string,
- to: PropTypes.string,
- formDefinition: PropTypes.string,
- isCompleted: PropTypes.bool,
- isApplicable: PropTypes.bool,
- isCurrent: PropTypes.bool,
- canNavigateTo: PropTypes.bool,
- })
- ),
- hasSubmission: PropTypes.bool,
- isStartPage: PropTypes.bool,
- isSummary: PropTypes.bool,
- isConfirmation: PropTypes.bool,
- isSubmissionComplete: PropTypes.bool,
- areApplicableStepsCompleted: PropTypes.bool,
- showOverview: PropTypes.bool,
- showConfirmation: PropTypes.bool,
- expanded: PropTypes.bool,
- onExpandClick: PropTypes.func.isRequired,
- sticky: PropTypes.bool,
- hideNonApplicableSteps: PropTypes.bool,
- summaryTo: PropTypes.string,
-};
-
-export default ProgressIndicatorDisplay;
diff --git a/src/components/ProgressIndicator/ProgressIndicatorItem.js b/src/components/ProgressIndicator/ProgressIndicatorItem.js
new file mode 100644
index 000000000..886649fc6
--- /dev/null
+++ b/src/components/ProgressIndicator/ProgressIndicatorItem.js
@@ -0,0 +1,73 @@
+import PropTypes from 'prop-types';
+import React from 'react';
+import {FormattedMessage} from 'react-intl';
+
+import Link from 'components/Link';
+import {getBEMClassName} from 'utils';
+
+import CompletionMark from './CompletionMark';
+
+const getLinkModifiers = (isActive, isApplicable, isCompleted) => {
+ return ['inherit', 'hover', isActive ? 'active' : undefined].filter(mod => mod !== undefined);
+};
+
+/**
+ * A single progress indicator item.
+ *
+ * Displays a link (which *may* be a placeholder depending on the `canNavigateTo` prop)
+ * to the specified route with the provided label.
+ *
+ * If the item is not applicable, the label is suffixed to mark it as such. Depending on
+ * server-side configuration, the item may be hidden alltogether (rather than showing it
+ * with the suffix).
+ *
+ * Once a step is completed, it is displayed with a completion checkmark in front of it.
+ */
+export const ProgressIndicatorItem = ({
+ label,
+ to,
+ isActive,
+ isCompleted,
+ canNavigateTo,
+ isApplicable,
+}) => {
+ return (
+
+ );
+};
+
+ProgressIndicatorItem.propTypes = {
+ label: PropTypes.node.isRequired,
+ to: PropTypes.string,
+ isActive: PropTypes.bool,
+ isCompleted: PropTypes.bool,
+ canNavigateTo: PropTypes.bool,
+ isApplicable: PropTypes.bool,
+};
+
+export default ProgressIndicatorItem;
diff --git a/src/components/ProgressIndicator/ProgressIndicatorItem.stories.js b/src/components/ProgressIndicator/ProgressIndicatorItem.stories.js
new file mode 100644
index 000000000..b776e532d
--- /dev/null
+++ b/src/components/ProgressIndicator/ProgressIndicatorItem.stories.js
@@ -0,0 +1,117 @@
+import {withRouter} from 'storybook-addon-react-router-v6';
+
+import {ProgressIndicatorItem} from './ProgressIndicatorItem';
+
+export default {
+ title: 'Private API / ProgressIndicator / ProgressIndicatorItem',
+ component: ProgressIndicatorItem,
+ decorators: [withRouter],
+ args: {
+ label: 'Stap 1',
+ to: '/dummy',
+ isActive: false,
+ isCompleted: true,
+ canNavigateTo: true,
+ isApplicable: true,
+ },
+ tags: ['autodocs'],
+};
+
+export const Default = {};
+
+/**
+ * The step is applicable, completed but it is not the currently active step.
+ */
+export const ApplicableNavigableCompletedNotActive = {
+ name: 'Applicable, navigable, completed, not active',
+ args: {
+ label: 'Stap 1',
+ to: '/dummy',
+ isActive: false,
+ isCompleted: true,
+ canNavigateTo: true,
+ isApplicable: true,
+ },
+};
+
+/**
+ * The step is applicable, completed and currently active/shown in the main body.
+ */
+export const ApplicableNavigableCompletedActive = {
+ name: 'Applicable, navigable, completed, active',
+ args: {
+ label: 'Stap 1',
+ to: '/dummy',
+ isActive: true,
+ isCompleted: true,
+ canNavigateTo: true,
+ isApplicable: true,
+ },
+};
+
+/**
+ * A step that is not completed yet, but is applicable and can be navigated to.
+ *
+ * E.g. if you are currently on step 2 making changes, and step 3 is unlocked - this
+ * can happen when you navigate back from step 3 to step 2.
+ */
+export const ApplicableNavigableNotCompletedNotActive = {
+ name: 'Applicable, navigable, not completed, not active',
+ args: {
+ label: 'Stap 1',
+ to: '/dummy',
+ isActive: false,
+ isCompleted: false,
+ canNavigateTo: true,
+ isApplicable: true,
+ },
+};
+
+/**
+ * An active, uncompleted step. The step itself is displayed in the main body.
+ *
+ * This is the state when you submit step x and it takes you to step x+1.
+ */
+export const ApplicableNavigableNotCompletedActive = {
+ name: 'Applicable, navigable, not completed, active',
+ args: {
+ label: 'Stap 1',
+ to: '/dummy',
+ isActive: true,
+ isCompleted: false,
+ canNavigateTo: true,
+ isApplicable: true,
+ },
+};
+
+/**
+ * A step that is relevant but not available yet, e.g. because the previous step needs
+ * to be completed first.
+ */
+export const ApplicableNotNavigable = {
+ name: 'Applicable, not navigable',
+ args: {
+ label: 'Stap 1',
+ to: '/dummy',
+ isActive: false,
+ isCompleted: false,
+ canNavigateTo: false,
+ isApplicable: true,
+ },
+};
+
+/**
+ * A step that is not relevant (due to logic, for example). It can be a past or future
+ * step.
+ */
+export const NotApplicableNotNavigable = {
+ name: 'Not applicable, not navigable',
+ args: {
+ label: 'Stap 1',
+ to: '/dummy',
+ isActive: false,
+ isCompleted: false,
+ canNavigateTo: false,
+ isApplicable: false,
+ },
+};
diff --git a/src/components/ProgressIndicator/ProgressItem.js b/src/components/ProgressIndicator/ProgressItem.js
deleted file mode 100644
index 73f85d33b..000000000
--- a/src/components/ProgressIndicator/ProgressItem.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import PropTypes from 'prop-types';
-import React from 'react';
-
-import {getBEMClassName} from 'utils';
-
-import CompletionMark from './CompletionMark';
-
-const ProgressItem = ({completed, children}) => {
- return (
-
- );
-};
-
-ProgressItem.propTypes = {
- completed: PropTypes.bool.isRequired,
- children: PropTypes.node,
-};
-
-export default ProgressItem;
diff --git a/src/components/ProgressIndicator/constants.js b/src/components/ProgressIndicator/constants.js
deleted file mode 100644
index ee81dac25..000000000
--- a/src/components/ProgressIndicator/constants.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import React from 'react';
-import {FormattedMessage} from 'react-intl';
-
-const STEP_LABELS = {
- login: ,
- overview: ,
- confirmation: (
-
- ),
-};
-
-export {STEP_LABELS};
diff --git a/src/components/ProgressIndicator/index.js b/src/components/ProgressIndicator/index.js
index 9bd4254f4..773f2dc6b 100644
--- a/src/components/ProgressIndicator/index.js
+++ b/src/components/ProgressIndicator/index.js
@@ -1,31 +1,29 @@
import PropTypes from 'prop-types';
-import React, {useContext, useEffect, useState} from 'react';
-import {useLocation, useMatch} from 'react-router-dom';
+import React, {useEffect, useState} from 'react';
+import {useLocation} from 'react-router-dom';
-import {ConfigContext} from 'Context';
-import {SUBMISSION_ALLOWED} from 'components/constants';
-import {IsFormDesigner} from 'headers';
-import Types from 'types';
+import Caption from 'components/Caption';
+import Card from 'components/Card';
+import List from 'components/List';
-import ProgressIndicatorDisplay from './ProgressIndicatorDisplay';
-import {STEP_LABELS} from './constants';
+import MobileButton from './MobileButton';
+import ProgressIndicatorItem from './ProgressIndicatorItem';
const ProgressIndicator = ({
title,
- submission = null,
+ formTitle,
steps,
- submissionAllowed,
- completed = false,
- hideNonApplicableSteps = false,
+ ariaMobileIconLabel,
+ accessibleToggleStepsLabel,
}) => {
- const {pathname} = useLocation();
- const config = useContext(ConfigContext);
- const summaryMatch = !!useMatch('/overzicht');
- const stepMatch = useMatch('/stap/:step');
- const confirmationMatch = !!useMatch('/bevestiging');
- const isStartPage = !summaryMatch && stepMatch == null && !confirmationMatch;
+ const {pathname: currentPathname} = useLocation();
const [expanded, setExpanded] = useState(false);
+ const modifiers = [];
+ if (!expanded) {
+ modifiers.push('mobile-collapsed');
+ }
+
// collapse the expanded progress indicator if nav occurred, see
// open-formulieren/open-forms#2673. It's important that *only* the pathname triggers
// the effect, which is why exhaustive deps is ignored.
@@ -34,109 +32,52 @@ const ProgressIndicator = ({
setExpanded(false);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
- }, [pathname]);
-
- // figure out the slug from the currently active step IF we're looking at a step
- const stepSlug = stepMatch ? stepMatch.params.step : '';
- const hasSubmission = !!submission;
-
- const applicableSteps = hasSubmission ? submission.steps.filter(step => step.isApplicable) : [];
- const applicableAndCompletedSteps = applicableSteps.filter(step => step.completed);
- const applicableCompleted =
- hasSubmission && applicableSteps.length === applicableAndCompletedSteps.length;
-
- // If any step cannot be submitted, there should NOT be an active link to the overview page.
- const canSubmitSteps = hasSubmission
- ? submission.steps.filter(step => !step.canSubmit).length === 0
- : false;
-
- // figure out the title for the mobile menu based on the state
- let activeStepTitle;
- if (isStartPage) {
- activeStepTitle = STEP_LABELS.login;
- } else if (summaryMatch) {
- activeStepTitle = STEP_LABELS.overview;
- } else if (confirmationMatch) {
- activeStepTitle = STEP_LABELS.confirmation;
- } else {
- const step = steps.find(step => step.slug === stepSlug);
- activeStepTitle = step.formDefinition;
- }
-
- const canNavigateToStep = index => {
- // The user can navigate to a step when:
- // 1. All previous steps have been completed
- // 2. The user is a form designer
- if (IsFormDesigner.getValue()) return true;
-
- if (!submission) return false;
-
- const previousSteps = submission.steps.slice(0, index);
- const previousApplicableButNotCompletedSteps = previousSteps.filter(
- step => step.isApplicable && !step.completed
- );
-
- return !previousApplicableButNotCompletedSteps.length;
- };
-
- const getStepsInfo = steps => {
- return steps.map((step, index) => ({
- uuid: step.uuid,
- slug: step.slug,
- to: step.href || `/stap/${step.slug}`,
- formDefinition: step.formDefinition,
- isCompleted: submission ? submission.steps[index].completed : false,
- isApplicable: submission ? submission.steps[index].isApplicable : step.isApplicable ?? true,
- isCurrent: step.slug === stepSlug,
- canNavigateTo: canNavigateToStep(index),
- }));
- };
-
- // try to get the value from the submission if provided, otherwise
- const submissionAllowedSpec = submission?.submissionAllowed ?? submissionAllowed;
- const showOverview = submissionAllowedSpec !== SUBMISSION_ALLOWED.noWithoutOverview;
- const showConfirmation = submissionAllowedSpec === SUBMISSION_ALLOWED.yes;
-
- const ProgressIndicatorDisplayComponent =
- config?.displayComponents?.progressIndicator ?? ProgressIndicatorDisplay;
+ }, [currentPathname]);
return (
- setExpanded(!expanded)}
- />
+
+
+
);
};
ProgressIndicator.propTypes = {
- title: PropTypes.string,
- submission: Types.Submission,
+ title: PropTypes.node.isRequired,
+ formTitle: PropTypes.string.isRequired,
steps: PropTypes.arrayOf(
PropTypes.shape({
- url: PropTypes.string.isRequired,
- uuid: PropTypes.string.isRequired,
- index: PropTypes.number.isRequired,
- slug: PropTypes.string.isRequired,
- href: PropTypes.string,
- formDefinition: PropTypes.string.isRequired,
+ to: PropTypes.string.isRequired,
+ label: PropTypes.node.isRequired,
+ isCompleted: PropTypes.bool,
isApplicable: PropTypes.bool,
+ isCurrent: PropTypes.bool,
+ canNavigateTo: PropTypes.bool,
})
).isRequired,
- submissionAllowed: PropTypes.oneOf(Object.values(SUBMISSION_ALLOWED)).isRequired,
- completed: PropTypes.bool,
- hideNonApplicableSteps: PropTypes.bool,
+ ariaMobileIconLabel: PropTypes.string.isRequired,
+ accessibleToggleStepsLabel: PropTypes.string.isRequired,
};
export default ProgressIndicator;
diff --git a/src/components/ProgressIndicator/progressIndicator.spec.js b/src/components/ProgressIndicator/progressIndicator.spec.js
new file mode 100644
index 000000000..04912c2b8
--- /dev/null
+++ b/src/components/ProgressIndicator/progressIndicator.spec.js
@@ -0,0 +1,96 @@
+import {render, screen} from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import messagesEN from 'i18n/compiled/en.json';
+import {IntlProvider} from 'react-intl';
+import {RouterProvider, createMemoryRouter} from 'react-router-dom';
+
+import {ConfigContext, FormContext} from 'Context';
+import {BASE_URL, buildForm} from 'api-mocks';
+import mswServer from 'api-mocks/msw-server';
+import {buildSubmission, mockSubmissionPost} from 'api-mocks/submissions';
+import App, {routes as nestedRoutes} from 'components/App';
+
+const routes = [
+ {
+ path: '*',
+ element: ,
+ children: nestedRoutes,
+ },
+];
+
+const renderApp = (initialRoute = '/') => {
+ const form = buildForm();
+ const router = createMemoryRouter(routes, {
+ initialEntries: [initialRoute],
+ initialIndex: [0],
+ });
+ render(
+
+
+
+
+
+
+
+ );
+};
+
+beforeEach(() => {
+ sessionStorage.clear();
+ localStorage.clear();
+});
+
+afterEach(() => {
+ sessionStorage.clear();
+ localStorage.clear();
+});
+
+describe('The progress indicator component', () => {
+ it('displays the available submission/form steps and hardcoded steps', async () => {
+ mswServer.use(mockSubmissionPost(buildSubmission()));
+ const user = userEvent.setup({delay: null});
+
+ renderApp();
+
+ const startFormLink = await screen.findByRole('link', {name: 'Start page'});
+ user.click(startFormLink);
+
+ const progressIndicator = await screen.findByText('Progress');
+ expect(progressIndicator).toBeVisible();
+
+ const startPageItem = await screen.findByText('Start page');
+ expect(startPageItem).toBeVisible();
+ const stepPageItem = await screen.findByText('Step 1');
+ expect(stepPageItem).toBeVisible();
+ const summaryPageItem = await screen.findByText('Summary');
+ expect(summaryPageItem).toBeVisible();
+ const confirmationPageItem = await screen.findByText('Confirmation');
+ expect(confirmationPageItem).toBeVisible();
+ });
+
+ it('renders steps in the correct order', async () => {
+ mswServer.use(mockSubmissionPost(buildSubmission()));
+ const user = userEvent.setup({delay: null});
+
+ renderApp();
+
+ const startFormLink = await screen.findByRole('link', {name: 'Start page'});
+ user.click(startFormLink);
+
+ const progressIndicatorSteps = screen.getAllByRole('listitem');
+
+ expect(progressIndicatorSteps[0]).toHaveTextContent('Start page');
+ expect(progressIndicatorSteps[1]).toHaveTextContent('Step 1');
+ expect(progressIndicatorSteps[2]).toHaveTextContent('Summary');
+ expect(progressIndicatorSteps[3]).toHaveTextContent('Confirmation');
+ });
+});
diff --git a/src/components/ProgressIndicator/test.spec.js b/src/components/ProgressIndicator/test.spec.js
deleted file mode 100644
index 6c32bb671..000000000
--- a/src/components/ProgressIndicator/test.spec.js
+++ /dev/null
@@ -1,399 +0,0 @@
-import {screen} from '@testing-library/react';
-import messagesNL from 'i18n/compiled/nl.json';
-import React from 'react';
-import {createRoot} from 'react-dom/client';
-import {act} from 'react-dom/test-utils';
-import {IntlProvider} from 'react-intl';
-import {MemoryRouter} from 'react-router-dom';
-
-import {SUBMISSION_ALLOWED} from 'components/constants';
-import {IsFormDesigner} from 'headers';
-
-import ProgressIndicator from './index';
-
-jest.mock('headers');
-
-let container = null;
-let root = null;
-beforeEach(() => {
- // setup a DOM element as a render target
- container = document.createElement('div');
- document.body.appendChild(container);
- root = createRoot(container);
-});
-
-afterEach(() => {
- // cleanup on exiting
- act(() => {
- root.unmount();
- container.remove();
- root = null;
- container = null;
- });
-});
-
-const submissionDefaults = {
- id: 'some-id',
- url: 'https://some-url',
- form: 'https://some-form',
- steps: [],
- payment: {
- isRequired: false,
- amount: '',
- hasPaid: false,
- },
-};
-
-it('Progress Indicator submission allowed', () => {
- act(() => {
- root.render(
-
-
-
-
-
- );
- });
-
- const progressIndicatorSteps = container.getElementsByTagName('ol')[0];
- expect(progressIndicatorSteps.textContent).toContain('Startpagina');
- expect(progressIndicatorSteps.textContent).toContain('Overzicht');
- expect(progressIndicatorSteps.textContent).toContain('Bevestiging');
-});
-
-it('Progress Indicator submission not allowed, with overview page', () => {
- act(() => {
- root.render(
-
-
-
-
-
- );
- });
-
- const progressIndicatorSteps = container.getElementsByTagName('ol')[0];
- expect(progressIndicatorSteps.textContent).toContain('Startpagina');
- expect(progressIndicatorSteps.textContent).toContain('Overzicht');
- expect(progressIndicatorSteps.textContent).not.toContain('Bevestiging');
-});
-
-it('Progress Indicator submission not allowed, without overview page', () => {
- act(() => {
- root.render(
-
-
-
-
-
- );
- });
-
- const progressIndicatorSteps = container.getElementsByTagName('ol')[0];
- expect(progressIndicatorSteps.textContent).toContain('Startpagina');
- expect(progressIndicatorSteps.textContent).not.toContain('Overzicht');
- expect(progressIndicatorSteps.textContent).not.toContain('Bevestiging');
-});
-
-it('Form landing page, no submission present in session', () => {
- act(() => {
- root.render(
-
-
-
-
-
- );
- });
-
- const progressIndicatorSteps = container.getElementsByTagName('ol')[0];
- expect(progressIndicatorSteps.textContent).toContain('Startpagina');
- expect(progressIndicatorSteps.textContent).toContain('Overzicht');
- expect(progressIndicatorSteps.textContent).toContain('Bevestiging');
-});
-
-it('Progress indicator does NOT let you navigate between steps if you are NOT a form designer', async () => {
- const steps = [
- {
- slug: 'step-1',
- formDefinition: 'Step 1',
- index: 0,
- isApplicable: true,
- url: 'http://test.nl/api/v1/forms/111/steps/111',
- uuid: '111',
- completed: true,
- },
- {
- slug: 'step-2',
- formDefinition: 'Step 2',
- index: 1,
- isApplicable: true,
- url: 'http://test.nl/api/v1/forms/111/steps/222',
- uuid: '222',
- completed: false,
- },
- {
- slug: 'step-3',
- formDefinition: 'Step 3',
- index: 2,
- isApplicable: true,
- url: 'http://test.nl/api/v1/forms/111/steps/333',
- uuid: '333',
- completed: false,
- },
- ];
-
- IsFormDesigner.getValue.mockReturnValue(false);
-
- act(() => {
- root.render(
-
-
-
-
-
- );
- });
-
- expect(await screen.findByRole('link', {name: 'Step 1'})).toHaveTextContent('Step 1');
- // span element: `generic` role:
- expect(await screen.findByRole('generic', {name: 'Step 3'})).toHaveTextContent('Step 3');
- expect(await screen.queryByRole('link', {name: 'Step 3'})).toBeNull();
-});
-
-it('Progress indicator DOES let you navigate between steps if you ARE a form designer', async () => {
- const steps = [
- {
- slug: 'step-1',
- formDefinition: 'Step 1',
- index: 0,
- isApplicable: true,
- url: 'http://test.nl/api/v1/forms/111/steps/111',
- uuid: '111',
- completed: true,
- },
- {
- slug: 'step-2',
- formDefinition: 'Step 2',
- index: 1,
- isApplicable: true,
- url: 'http://test.nl/api/v1/forms/111/steps/222',
- uuid: '222',
- completed: false,
- },
- {
- slug: 'step-3',
- formDefinition: 'Step 3',
- index: 2,
- isApplicable: true,
- url: 'http://test.nl/api/v1/forms/111/steps/333',
- uuid: '333',
- completed: false,
- },
- ];
-
- IsFormDesigner.getValue.mockReturnValue(true);
-
- act(() => {
- root.render(
-
-
-
-
-
- );
- });
-
- expect(await screen.findByRole('link', {name: 'Step 1'})).toHaveTextContent('Step 1');
- expect(await screen.findByRole('link', {name: 'Step 3'})).toHaveTextContent('Step 3');
-});
-
-it('Progress indicator DOES let you navigate between steps if you are NOT a form designer but a step is NOT applicable', async () => {
- const steps = [
- {
- slug: 'step-1',
- formDefinition: 'Step 1',
- index: 0,
- isApplicable: true,
- url: 'http://test.nl/api/v1/forms/111/steps/111',
- uuid: '111',
- completed: true,
- },
- {
- slug: 'step-2',
- formDefinition: 'Step 2',
- index: 1,
- isApplicable: false,
- url: 'http://test.nl/api/v1/forms/111/steps/222',
- uuid: '222',
- completed: false,
- },
- {
- slug: 'step-3',
- formDefinition: 'Step 3',
- index: 2,
- isApplicable: true,
- url: 'http://test.nl/api/v1/forms/111/steps/333',
- uuid: '333',
- completed: false,
- },
- ];
-
- IsFormDesigner.getValue.mockReturnValue(false);
-
- act(() => {
- root.render(
-
-
-
-
-
- );
- });
-
- expect(await screen.findByRole('link', {name: 'Step 3'})).toBeDefined();
- expect(await screen.findByRole('link', {name: 'Step 3'})).toHaveTextContent('Step 3');
-});
-
-it('Progress indicator does NOT let you navigate to the overview if a step is blocked', async () => {
- const steps = [
- {
- slug: 'step-1',
- formDefinition: 'Step 1',
- index: 0,
- isApplicable: true,
- url: 'http://test.nl/api/v1/forms/111/steps/111',
- uuid: '111',
- completed: true,
- canSubmit: false,
- },
- ];
-
- IsFormDesigner.getValue.mockReturnValue(false);
-
- act(() => {
- root.render(
-
-
-
-
-
- );
- });
-
- const overview = container.getElementsByTagName('li')[2];
-
- const overviewLink = overview.getElementsByTagName('a')[0];
- const overviewSpan = overview.getElementsByTagName('span')[0];
-
- // Check that the overview is not a link since a step cannot be submitted
- expect(overviewLink).toBeUndefined();
- expect(overviewSpan).not.toBeUndefined();
- expect(overviewSpan.textContent).toContain('Overzicht');
-});
-
-it('Progress indicator takes into account default step applicability when on start page', async () => {
- const steps = [
- {
- slug: 'step-1',
- formDefinition: 'Step 1',
- index: 0,
- isApplicable: true,
- url: 'http://test.nl/api/v1/forms/111/steps/111',
- uuid: '111',
- completed: true,
- canSubmit: false,
- },
- {
- slug: 'step-2',
- formDefinition: 'Step 2',
- index: 0,
- isApplicable: false,
- url: 'http://test.nl/api/v1/forms/111/steps/111',
- uuid: '222',
- completed: true,
- canSubmit: false,
- },
- ];
-
- IsFormDesigner.getValue.mockReturnValue(true);
-
- act(() => {
- root.render(
-
-
-
-
-
- );
- });
-
- expect(await screen.findByRole('link', {name: 'Step 1'})).toBeDefined();
- expect(await screen.queryByRole('link', {name: 'Step 2'})).toBeNull();
-});
diff --git a/src/components/ProgressIndicator/utils.js b/src/components/ProgressIndicator/utils.js
new file mode 100644
index 000000000..18a044a4f
--- /dev/null
+++ b/src/components/ProgressIndicator/utils.js
@@ -0,0 +1,83 @@
+import {STEP_LABELS} from 'components/constants';
+import {checkMatchesPath} from 'components/utils/routers';
+import {IsFormDesigner} from 'headers';
+
+const canNavigateToStep = (index, submission) => {
+ // The user can navigate to a step when:
+ // 1. All previous steps have been completed
+ // 2. The user is a form designer
+ if (IsFormDesigner.getValue()) return true;
+
+ if (!submission) return false;
+
+ const previousSteps = submission.steps.slice(0, index);
+ const previousApplicableButNotCompletedSteps = previousSteps.filter(
+ step => step.isApplicable && !step.completed
+ );
+
+ return !previousApplicableButNotCompletedSteps.length;
+};
+
+const getStepsInfo = (formSteps, submission, currentPathname) => {
+ return formSteps.map((step, index) => ({
+ to: `/stap/${step.slug}`,
+ label: step.formDefinition,
+ isCompleted: submission ? submission.steps[index].completed : false,
+ isApplicable: submission ? submission.steps[index].isApplicable : step.isApplicable ?? true,
+ isCurrent: checkMatchesPath(currentPathname, step.slug),
+ canNavigateTo: canNavigateToStep(index, submission),
+ }));
+};
+
+const addFixedSteps = (
+ intl,
+ steps,
+ submission,
+ currentPathname,
+ showOverview,
+ showConfirmation,
+ completed = false
+) => {
+ const hasSubmission = !!submission;
+ const isConfirmation = checkMatchesPath(currentPathname, 'bevestiging');
+ const applicableSteps = hasSubmission ? submission.steps.filter(step => step.isApplicable) : [];
+ const applicableAndCompletedSteps = applicableSteps.filter(step => step.completed);
+ const applicableCompleted =
+ hasSubmission && applicableSteps.length === applicableAndCompletedSteps.length;
+
+ const startPageStep = {
+ to: 'startpagina',
+ label: intl.formatMessage(STEP_LABELS.login),
+ isCompleted: hasSubmission,
+ isApplicable: true,
+ canNavigateTo: true,
+ isCurrent: checkMatchesPath(currentPathname, 'startpagina'),
+ };
+
+ const summaryStep = {
+ to: 'overzicht',
+ label: intl.formatMessage(STEP_LABELS.overview),
+ isCompleted: isConfirmation,
+ isApplicable: true,
+ isCurrent: checkMatchesPath(currentPathname, 'overzicht'),
+ canNavigateTo: applicableCompleted,
+ };
+
+ const confirmationStep = {
+ to: 'bevestiging',
+ label: intl.formatMessage(STEP_LABELS.confirmation),
+ isCompleted: completed,
+ isCurrent: checkMatchesPath(currentPathname, 'bevestiging'),
+ };
+
+ const finalSteps = [
+ startPageStep,
+ ...steps,
+ showOverview && summaryStep,
+ showConfirmation && confirmationStep,
+ ];
+
+ return finalSteps;
+};
+
+export {addFixedSteps, getStepsInfo};
diff --git a/src/components/ProgressIndicator/utils.spec.js b/src/components/ProgressIndicator/utils.spec.js
new file mode 100644
index 000000000..74185c6b9
--- /dev/null
+++ b/src/components/ProgressIndicator/utils.spec.js
@@ -0,0 +1,68 @@
+import messagesEN from 'i18n/compiled/en.json';
+import {createIntl, createIntlCache} from 'react-intl';
+
+import {buildSubmission} from 'api-mocks/submissions';
+
+import {addFixedSteps, getStepsInfo} from './utils';
+
+const cache = createIntlCache();
+const intl = createIntl({locale: 'en', messages: messagesEN}, cache);
+
+const formSteps = [
+ {
+ slug: 'step-1',
+ formDefinition: 'Step 1',
+ isCompleted: false,
+ isApplicable: true,
+ isCurrent: true,
+ canNavigateTo: true,
+ },
+];
+
+describe('Transforming form steps and injecting fixed steps', () => {
+ it('prepends start page and appends summary and confirmation steps', () => {
+ const submission = buildSubmission();
+ const updatedSteps = getStepsInfo(formSteps, submission, '/stap/step-1');
+ const stepsToRender = addFixedSteps(intl, updatedSteps, submission, '/stap/step-1', true, true);
+
+ expect(stepsToRender.length).toEqual(4);
+ expect(stepsToRender[0].to).toEqual('startpagina');
+
+ expect(stepsToRender[1].to).toEqual('/stap/step-1');
+ expect(stepsToRender[1].label).toEqual('Step 1');
+ expect(stepsToRender[1].isCompleted).toEqual(false);
+ expect(stepsToRender[1].isApplicable).toEqual(true);
+ expect(stepsToRender[1].isCurrent).toEqual(true);
+ expect(stepsToRender[1].canNavigateTo).toEqual(true);
+
+ expect(stepsToRender[2].to).toEqual('overzicht');
+ expect(stepsToRender[3].to).toEqual('bevestiging');
+ });
+
+ it('accepts parameters to not append summary or confirmation', () => {
+ const submission = buildSubmission();
+ const updatedSteps = getStepsInfo(formSteps, submission, '/stap/step-1');
+ const stepsToRender = addFixedSteps(
+ intl,
+ updatedSteps,
+ submission,
+ '/stap/step-1',
+ false,
+ false
+ );
+
+ expect(stepsToRender.length).toEqual(4);
+
+ expect(stepsToRender[0].to).toEqual('startpagina');
+
+ expect(stepsToRender[1].to).toEqual('/stap/step-1');
+ expect(stepsToRender[1].label).toEqual('Step 1');
+ expect(stepsToRender[1].isCompleted).toEqual(false);
+ expect(stepsToRender[1].isApplicable).toEqual(true);
+ expect(stepsToRender[1].isCurrent).toEqual(true);
+ expect(stepsToRender[1].canNavigateTo).toEqual(true);
+
+ expect(stepsToRender[2]).toBeFalsy();
+ expect(stepsToRender[3]).toBeFalsy();
+ });
+});
diff --git a/src/components/appointments/CreateAppointment/AppointmentProgress.js b/src/components/appointments/CreateAppointment/AppointmentProgress.js
index facd1a7e9..175d3aebf 100644
--- a/src/components/appointments/CreateAppointment/AppointmentProgress.js
+++ b/src/components/appointments/CreateAppointment/AppointmentProgress.js
@@ -1,13 +1,15 @@
import PropTypes from 'prop-types';
-import React, {useContext, useState} from 'react';
+import React, {useContext} from 'react';
import {useIntl} from 'react-intl';
import {useLocation} from 'react-router-dom';
import {ConfigContext} from 'Context';
-import ProgressIndicatorDisplay from 'components/ProgressIndicator/ProgressIndicatorDisplay';
+import ProgressIndicator from 'components/ProgressIndicator';
+import {PI_TITLE, STEP_LABELS} from 'components/constants';
+import {checkMatchesPath} from 'components/utils/routers';
import {useCreateAppointmentContext} from './CreateAppointmentState';
-import {APPOINTMENT_STEPS, APPOINTMENT_STEP_PATHS, checkMatchesPath} from './routes';
+import {APPOINTMENT_STEPS, APPOINTMENT_STEP_PATHS} from './routes';
const AppointmentProgress = ({title, currentStep}) => {
const config = useContext(ConfigContext);
@@ -15,10 +17,8 @@ const AppointmentProgress = ({title, currentStep}) => {
const intl = useIntl();
const {pathname: currentPathname} = useLocation();
- const isSummary = checkMatchesPath(currentPathname, 'overzicht');
const isConfirmation = checkMatchesPath(currentPathname, 'bevestiging');
-
- const [expanded, setExpanded] = useState(false);
+ const isSummary = checkMatchesPath(currentPathname, 'overzicht');
const isSubmissionComplete = isConfirmation && submission === null;
@@ -33,8 +33,8 @@ const AppointmentProgress = ({title, currentStep}) => {
const stepCompleted = submittedSteps.includes(path);
return {
- uuid: `appointments-${path}`,
to: path,
+ label: intl.formatMessage(name),
isCompleted: stepCompleted || isSubmissionComplete,
isApplicable: true,
isCurrent: checkMatchesPath(currentPathname, path),
@@ -43,30 +43,60 @@ const AppointmentProgress = ({title, currentStep}) => {
previousStepCompleted ||
index === currentStepIndex ||
isSubmissionComplete,
- formDefinition: intl.formatMessage(name),
};
});
- const ProgressIndicatorDisplayComponent =
- config?.displayComponents?.progressIndicator ?? ProgressIndicatorDisplay;
+ // Add the fixed steps to the the original steps array
+ const finalSteps = [
+ ...steps,
+ {
+ to: 'overzicht',
+ label: intl.formatMessage(STEP_LABELS.overview),
+ isCompleted: isConfirmation,
+ isApplicable: true,
+ isCurrent: checkMatchesPath(currentPathname, 'overzicht'),
+ canNavigateTo: false,
+ },
+ {
+ to: 'bevestiging',
+ label: intl.formatMessage(STEP_LABELS.confirmation),
+ isCompleted: isSubmissionComplete,
+ isCurrent: checkMatchesPath(currentPathname, 'bevestiging'),
+ },
+ ];
+
+ // Figure out the title for the mobile menu based on the state
+ let activeStepTitle;
+ if (isSummary) {
+ activeStepTitle = intl.formatMessage(STEP_LABELS.overview);
+ } else if (isConfirmation) {
+ activeStepTitle = intl.formatMessage(STEP_LABELS.confirmation);
+ } else {
+ activeStepTitle = currentStep;
+ }
+
+ 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 {title}: {activeStepTitle}',
+ },
+ {title, activeStepTitle}
+ );
+
+ const ProgressIndicatorComponent =
+ config?.displayComponents?.progressIndicator ?? ProgressIndicator;
return (
- setExpanded(!expanded)}
+ steps={finalSteps}
+ ariaMobileIconLabel={ariaMobileIconLabel}
+ accessibleToggleStepsLabel={accessibleToggleStepsLabel}
/>
);
};
diff --git a/src/components/appointments/CreateAppointment/CreateAppointment.js b/src/components/appointments/CreateAppointment/CreateAppointment.js
index 22fb26eb5..f98b29933 100644
--- a/src/components/appointments/CreateAppointment/CreateAppointment.js
+++ b/src/components/appointments/CreateAppointment/CreateAppointment.js
@@ -8,6 +8,7 @@ import FormDisplay from 'components/FormDisplay';
import {LiteralsProvider} from 'components/Literal';
import Loader from 'components/Loader';
import {RequireSession} from 'components/Sessions';
+import {checkMatchesPath} from 'components/utils/routers';
import useFormContext from 'hooks/useFormContext';
import useGetOrCreateSubmission from 'hooks/useGetOrCreateSubmission';
import useSessionTimeout from 'hooks/useSessionTimeout';
@@ -15,7 +16,7 @@ import useSessionTimeout from 'hooks/useSessionTimeout';
import {AppointmentConfigContext} from '../Context';
import AppointmentProgress from './AppointmentProgress';
import CreateAppointmentState from './CreateAppointmentState';
-import {APPOINTMENT_STEP_PATHS, checkMatchesPath} from './routes';
+import {APPOINTMENT_STEP_PATHS} from './routes';
const useIsConfirmation = () => {
// useMatch requires absolute paths... and react-router are NOT receptive to changing that.
diff --git a/src/components/appointments/CreateAppointment/routes.js b/src/components/appointments/CreateAppointment/routes.js
index 30ede3302..c142d5c35 100644
--- a/src/components/appointments/CreateAppointment/routes.js
+++ b/src/components/appointments/CreateAppointment/routes.js
@@ -1,5 +1,5 @@
import {defineMessage} from 'react-intl';
-import {Navigate, matchPath, resolvePath} from 'react-router-dom';
+import {Navigate} from 'react-router-dom';
import useQuery from 'hooks/useQuery';
@@ -67,19 +67,3 @@ export const routes = [
element: ,
},
];
-
-/**
- * Check if a given relative path from the routes matches the current location.
- * @param currentPathname The current router location.pathname, from useLocation.
- * @param path The relative path to check for matches
- */
-export const checkMatchesPath = (currentPathname, path) => {
- // we need to transform the path into a parent-route lookup, instead of using the
- // default relative ./ behaviour. The idea is that this component is mounted
- // somewhere in a larger route definition but the exact parent route is not relevant.
- const resolvedPath = resolvePath(`../${path}`, currentPathname);
- // if the relative path is not the current URL, matchPath returns null, otherwise
- // a match object.
- const match = matchPath(resolvedPath.pathname, currentPathname);
- return match !== null;
-};
diff --git a/src/components/constants.js b/src/components/constants.js
index 9823078be..780bd3789 100644
--- a/src/components/constants.js
+++ b/src/components/constants.js
@@ -1,11 +1,38 @@
+import {FormattedMessage, defineMessages} from 'react-intl';
+
const SUBMISSION_ALLOWED = {
yes: 'yes',
noWithOverview: 'no_with_overview',
noWithoutOverview: 'no_without_overview',
};
+const STEP_LABELS = defineMessages({
+ login: {
+ description: 'Start page title',
+ defaultMessage: 'Start page',
+ },
+ overview: {
+ description: 'Summary page title',
+ defaultMessage: 'Summary',
+ },
+ confirmation: {
+ description: 'Confirmation page title',
+ defaultMessage: 'Confirmation',
+ },
+});
+
const START_FORM_QUERY_PARAM = '_start';
const SUBMISSION_UUID_QUERY_PARAM = 'submission_uuid';
-export {SUBMISSION_ALLOWED, START_FORM_QUERY_PARAM, SUBMISSION_UUID_QUERY_PARAM};
+const PI_TITLE = (
+
+);
+
+export {
+ SUBMISSION_ALLOWED,
+ START_FORM_QUERY_PARAM,
+ SUBMISSION_UUID_QUERY_PARAM,
+ STEP_LABELS,
+ PI_TITLE,
+};
diff --git a/src/components/utils/routers.js b/src/components/utils/routers.js
new file mode 100644
index 000000000..af3210bd2
--- /dev/null
+++ b/src/components/utils/routers.js
@@ -0,0 +1,17 @@
+import {matchPath, resolvePath} from 'react-router-dom';
+
+/**
+ * Check if a given relative path from the routes matches the current location.
+ * @param currentPathname The current router location.pathname, from useLocation.
+ * @param path The relative path to check for matches
+ */
+export const checkMatchesPath = (currentPathname, path) => {
+ // we need to transform the path into a parent-route lookup, instead of using the
+ // default relative ./ behaviour. The idea is that this component is mounted
+ // somewhere in a larger route definition but the exact parent route is not relevant.
+ const resolvedPath = resolvePath(`../${path}`, currentPathname);
+ // if the relative path is not the current URL, matchPath returns null, otherwise
+ // a match object.
+ const match = matchPath(resolvedPath.pathname, currentPathname);
+ return match !== null;
+};
diff --git a/src/hooks/useRecycleSubmission.js b/src/hooks/useRecycleSubmission.js
index 3de996f4d..e4db277de 100644
--- a/src/hooks/useRecycleSubmission.js
+++ b/src/hooks/useRecycleSubmission.js
@@ -11,6 +11,8 @@ const useRecycleSubmission = (form, currentSubmission, onSubmissionLoaded, onErr
const location = useLocation();
const config = useContext(ConfigContext);
const queryParams = useQuery();
+ // XXX: use sessionStorage instead of localStorage for this, so that it's scoped to
+ // a single tab/window?
let [submissionId, setSubmissionId, removeSubmissionId] = useLocalStorage(form.uuid, '');
// If no submissionID is in the localStorage see if one can be retrieved from the query param
diff --git a/src/i18n/compiled/en.json b/src/i18n/compiled/en.json
index 3934b1d00..d2849121f 100644
--- a/src/i18n/compiled/en.json
+++ b/src/i18n/compiled/en.json
@@ -1241,26 +1241,14 @@
"value": "Times are in your local time"
}
],
- "hHS5vj": [
- {
- "type": 0,
- "value": "Something went wrong while submitting the form."
- }
- ],
- "hQRQCS": [
- {
- "type": 0,
- "value": "Er is een fout opgetreden in de communicatie met DigiD. Probeert u het later nogmaals. Indien deze fout blijft aanhouden, kijk dan op de website https://www.digid.nl voor de laatste informatie."
- }
- ],
- "hR1LxA": [
+ "hFqzG6": [
{
"type": 0,
"value": "Current step in form "
},
{
"type": 1,
- "value": "formTitle"
+ "value": "title"
},
{
"type": 0,
@@ -1271,6 +1259,18 @@
"value": "activeStepTitle"
}
],
+ "hHS5vj": [
+ {
+ "type": 0,
+ "value": "Something went wrong while submitting the form."
+ }
+ ],
+ "hQRQCS": [
+ {
+ "type": 0,
+ "value": "Er is een fout opgetreden in de communicatie met DigiD. Probeert u het later nogmaals. Indien deze fout blijft aanhouden, kijk dan op de website https://www.digid.nl voor de laatste informatie."
+ }
+ ],
"hkZ8N1": [
{
"type": 0,
@@ -1341,6 +1341,24 @@
"value": "Your products"
}
],
+ "jdzNwf": [
+ {
+ "type": 0,
+ "value": "Current step in form "
+ },
+ {
+ "type": 1,
+ "value": "formName"
+ },
+ {
+ "type": 0,
+ "value": ": "
+ },
+ {
+ "type": 1,
+ "value": "activeStepTitle"
+ }
+ ],
"jnYRyz": [
{
"type": 0,
diff --git a/src/i18n/compiled/nl.json b/src/i18n/compiled/nl.json
index 02671942d..d64910fca 100644
--- a/src/i18n/compiled/nl.json
+++ b/src/i18n/compiled/nl.json
@@ -1245,26 +1245,14 @@
"value": "U wordt op dit tijdstip op de afspraak verwacht"
}
],
- "hHS5vj": [
- {
- "type": 0,
- "value": "Er is iets fout gegaan bij het verzenden."
- }
- ],
- "hQRQCS": [
- {
- "type": 0,
- "value": "Er is een fout opgetreden in de communicatie met DigiD. Probeert u het later nogmaals. Indien deze fout blijft aanhouden, kijk dan op de website https://www.digid.nl voor de laatste informatie."
- }
- ],
- "hR1LxA": [
+ "hFqzG6": [
{
"type": 0,
"value": "Huidige stap in formulier "
},
{
"type": 1,
- "value": "formTitle"
+ "value": "title"
},
{
"type": 0,
@@ -1275,6 +1263,18 @@
"value": "activeStepTitle"
}
],
+ "hHS5vj": [
+ {
+ "type": 0,
+ "value": "Er is iets fout gegaan bij het verzenden."
+ }
+ ],
+ "hQRQCS": [
+ {
+ "type": 0,
+ "value": "Er is een fout opgetreden in de communicatie met DigiD. Probeert u het later nogmaals. Indien deze fout blijft aanhouden, kijk dan op de website https://www.digid.nl voor de laatste informatie."
+ }
+ ],
"hkZ8N1": [
{
"type": 0,
@@ -1345,6 +1345,24 @@
"value": "Uw producten"
}
],
+ "jdzNwf": [
+ {
+ "type": 0,
+ "value": "Huidige stap in formulier "
+ },
+ {
+ "type": 1,
+ "value": "formName"
+ },
+ {
+ "type": 0,
+ "value": ": "
+ },
+ {
+ "type": 1,
+ "value": "activeStepTitle"
+ }
+ ],
"jnYRyz": [
{
"type": 0,
diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json
index 03e72a82a..05f1d9c15 100644
--- a/src/i18n/messages/en.json
+++ b/src/i18n/messages/en.json
@@ -599,6 +599,11 @@
"description": "Appoinments: time select help text",
"originalDefault": "Times are in your local time"
},
+ "hFqzG6": {
+ "defaultMessage": "Current step in form {title}: {activeStepTitle}",
+ "description": "Active step accessible label in mobile progress indicator",
+ "originalDefault": "Current step in form {title}: {activeStepTitle}"
+ },
"hHS5vj": {
"defaultMessage": "Something went wrong while submitting the form.",
"description": "Generic submission error",
@@ -609,11 +614,6 @@
"description": "DigiD error message. MUST BE THIS EXACT STRING!",
"originalDefault": "Er is een fout opgetreden in de communicatie met DigiD. Probeert u het later nogmaals. Indien deze fout blijft aanhouden, kijk dan op de website https://www.digid.nl voor de laatste informatie."
},
- "hR1LxA": {
- "defaultMessage": "Current step in form {formTitle}: {activeStepTitle}",
- "description": "Active step accessible label in mobile progress indicator",
- "originalDefault": "Current step in form {formTitle}: {activeStepTitle}"
- },
"hkZ8N1": {
"defaultMessage": "Invalid input.",
"description": "ZOD 'custom' error message",
@@ -659,6 +659,11 @@
"description": "Product summary on appointments location and time step heading",
"originalDefault": "Your products"
},
+ "jdzNwf": {
+ "defaultMessage": "Current step in form {formName}: {activeStepTitle}",
+ "description": "Active step accessible label in mobile progress indicator",
+ "originalDefault": "Current step in form {formName}: {activeStepTitle}"
+ },
"jnYRyz": {
"defaultMessage": "Expected {expected}, received {received}.",
"description": "ZOD 'invalid_type' error message",
diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json
index af0937c2b..7fbe46c98 100644
--- a/src/i18n/messages/nl.json
+++ b/src/i18n/messages/nl.json
@@ -605,6 +605,11 @@
"description": "Appoinments: time select help text",
"originalDefault": "Times are in your local time"
},
+ "hFqzG6": {
+ "defaultMessage": "Huidige stap in formulier {title}: {activeStepTitle}",
+ "description": "Active step accessible label in mobile progress indicator",
+ "originalDefault": "Current step in form {title}: {activeStepTitle}"
+ },
"hHS5vj": {
"defaultMessage": "Er is iets fout gegaan bij het verzenden.",
"description": "Generic submission error",
@@ -616,11 +621,6 @@
"isTranslated": true,
"originalDefault": "Er is een fout opgetreden in de communicatie met DigiD. Probeert u het later nogmaals. Indien deze fout blijft aanhouden, kijk dan op de website https://www.digid.nl voor de laatste informatie."
},
- "hR1LxA": {
- "defaultMessage": "Huidige stap in formulier {formTitle}: {activeStepTitle}",
- "description": "Active step accessible label in mobile progress indicator",
- "originalDefault": "Current step in form {formTitle}: {activeStepTitle}"
- },
"hkZ8N1": {
"defaultMessage": "Ongeldige invoer.",
"description": "ZOD 'custom' error message",
@@ -666,6 +666,11 @@
"description": "Product summary on appointments location and time step heading",
"originalDefault": "Your products"
},
+ "jdzNwf": {
+ "defaultMessage": "Huidige stap in formulier {formName}: {activeStepTitle}",
+ "description": "Active step accessible label in mobile progress indicator",
+ "originalDefault": "Current step in form {formName}: {activeStepTitle}"
+ },
"jnYRyz": {
"defaultMessage": "Verwachtte type {expected} maar kreeg {received}.",
"description": "ZOD 'invalid_type' error message",
diff --git a/src/scss/components/_anchor.scss b/src/scss/components/_anchor.scss
index d16ae1af2..da3661679 100644
--- a/src/scss/components/_anchor.scss
+++ b/src/scss/components/_anchor.scss
@@ -22,7 +22,6 @@
var(--of-typography-sans-serif-font-family, var(--utrecht-document-font-family))
);
margin: 0;
- cursor: pointer;
}
// swap the hover/no-hover text decoration rules compared to the non-modified variant
diff --git a/src/scss/components/_progress-indicator.scss b/src/scss/components/_progress-indicator.scss
index 8dd13892b..81513562c 100644
--- a/src/scss/components/_progress-indicator.scss
+++ b/src/scss/components/_progress-indicator.scss
@@ -14,11 +14,6 @@
.#{prefix('progress-indicator')} {
@extend .openforms-card; // TODO -> syntax highlighting trips on #{prefix('card')}
- @include bem.modifier('sticky') {
- position: sticky;
- top: var(--of-progress-indicator-sticky-spacing);
- }
-
@include bem.element('mobile-header') {
@include body;
@include body--big;