diff --git a/.storybook/decorators.js b/.storybook/decorators.js
index 734c13074..97da30f9d 100644
--- a/.storybook/decorators.js
+++ b/.storybook/decorators.js
@@ -9,10 +9,29 @@ export const withClearSessionStorage = Story => {
return ;
};
+/**
+ * Submission ID is persisted in the localStorage once a submission is started, and we
+ * need to clear it for stories.
+ *
+ * The storage key is the form UUID, the default from api-mocks/form is
+ * 'e450890a-4166-410e-8d64-0a54ad30ba01'. You can specify another key via parameters
+ * or args.
+ */
+export const withClearSubmissionLocalStorage = (Story, {args, parameters}) => {
+ const formId =
+ args?.formId ?? parameters?.localStorage?.formId ?? 'e450890a-4166-410e-8d64-0a54ad30ba01';
+ window.localStorage.removeItem(formId);
+ return ;
+};
+
/**
* This decorator wraps stories so that they are inside a container with the class name "utrecht-document". This is
* needed so that components inherit the right font.
*/
export const utrechtDocumentDecorator = Story => {
- return (
Toelichtingssjabloon...
',
submissionAllowed: args['submissionAllowed'],
hideNonApplicableSteps: args['hideNonApplicableSteps'],
steps: args['steps'],
});
- return
+/**
+ * Main application display component - this is the layout wrapper around content.
+ *
+ * The Display component uses 'slots' for certain content blocks. The slot 'children'
+ * is reserved for the main content.
+ *
+ */
+export const AppDisplay = ({
+ children = null,
+ languageSwitcher = null,
+ progressIndicator = null,
+ appDebug = null,
+ router,
+}) => (
+
{languageSwitcher &&
{languageSwitcher}
}
-
{router}
+
{children || router}
+ {progressIndicator && (
+
{progressIndicator}
+ )}
{appDebug &&
{appDebug}
}
);
AppDisplay.propTypes = {
- router: PropTypes.node.isRequired,
+ children: PropTypes.node.isRequired,
languageSwitcher: PropTypes.node,
+ progressIndicator: PropTypes.node,
appDebug: PropTypes.node,
+ /**
+ * Main content.
+ *
+ * @deprecated Use children instead.
+ *
+ */
+ router: PropTypes.node,
};
export default AppDisplay;
diff --git a/src/components/Caption.js b/src/components/Caption.js
index d71fbc406..f7decbf47 100644
--- a/src/components/Caption.js
+++ b/src/components/Caption.js
@@ -3,9 +3,13 @@ import React from 'react';
import {getBEMClassName} from 'utils';
-const Caption = ({children, component = 'caption'}) => {
+const Caption = ({children, component = 'caption', ...props}) => {
const Component = `${component}`;
- return
{children} ;
+ return (
+
+ {children}
+
+ );
};
Caption.propTypes = {
diff --git a/src/components/Card.js b/src/components/Card.js
index c50102581..6dfbeb28b 100644
--- a/src/components/Card.js
+++ b/src/components/Card.js
@@ -47,10 +47,11 @@ const Card = ({
captionComponent,
blockClassName = 'card',
modifiers = [],
+ ...props
}) => {
const className = getBEMClassName(blockClassName, modifiers);
return (
-
+
{/* Emit header/title only if there is one */}
{title ? (
{
isCompleted
);
- const progressIndicator = form.showProgressIndicator ? (
-
- ) : null;
+ // Show the progress indicator if enabled on the form AND we're not in the payment
+ // status/overview screen.
+ const progressIndicator =
+ form.showProgressIndicator && !paymentOverviewMatch ? (
+
+ ) : null;
// Route the correct page based on URL
const router = (
@@ -429,15 +432,7 @@ const Form = () => {
);
// render the form step if there's an active submission (and no summary)
- const FormDisplayComponent = config?.displayComponents?.form ?? FormDisplay;
- return (
-
- );
+ return {router} ;
};
Form.propTypes = {};
diff --git a/src/components/FormDisplay.js b/src/components/FormDisplay.js
index dfeb5640b..350cfdbff 100644
--- a/src/components/FormDisplay.js
+++ b/src/components/FormDisplay.js
@@ -1,37 +1,52 @@
-import classNames from 'classnames';
import PropTypes from 'prop-types';
-import React from 'react';
+import React, {useContext} from 'react';
+
+import {ConfigContext} from 'Context';
+import AppDebug from 'components/AppDebug';
+import AppDisplay from 'components/AppDisplay';
+import LanguageSwitcher from 'components/LanguageSwitcher';
+import useFormContext from 'hooks/useFormContext';
/**
* Layout component to render the form container.
+ *
+ * Takes in the main body and (optional) progress indicator and forwards them to the
+ * AppDisplay component, while adding in any global/skeleton nodes.
+ *
* @return {JSX}
*/
-const FormDisplay = ({
- router,
- progressIndicator = null,
- showProgressIndicator = true,
- isPaymentOverview = false,
-}) => {
- const renderProgressIndicator = progressIndicator && showProgressIndicator && !isPaymentOverview;
+const FormDisplay = ({children = null, progressIndicator = null, router = null}) => {
+ const {translationEnabled} = useFormContext();
+ const config = useContext(ConfigContext);
+
+ const appDebug = config.debug ? : null;
+ const languageSwitcher = translationEnabled ? : null;
+
+ const AppDisplayComponent = config?.displayComponents?.app ?? AppDisplay;
return (
-
-
{router}
- {renderProgressIndicator && (
-
{progressIndicator}
- )}
-
+ {children || router}
+
);
};
FormDisplay.propTypes = {
- router: PropTypes.node.isRequired,
+ /**
+ * Main content.
+ */
+ children: PropTypes.node,
progressIndicator: PropTypes.node,
- showProgressIndicator: PropTypes.bool,
- isPaymentOverview: PropTypes.bool,
+ /**
+ * Main content.
+ *
+ * @deprecated Use children instead.
+ *
+ */
+ router: PropTypes.node,
};
export default FormDisplay;
diff --git a/src/components/FormDisplay.stories.js b/src/components/FormDisplay.stories.js
index 2a5d5d99e..31ca228c3 100644
--- a/src/components/FormDisplay.stories.js
+++ b/src/components/FormDisplay.stories.js
@@ -1,44 +1,46 @@
import Body from 'components/Body';
import Card from 'components/Card';
-import {LayoutDecorator} from 'story-utils/decorators';
+import {ConfigDecorator, LayoutDecorator} from 'story-utils/decorators';
import FormDisplay from './FormDisplay';
export default {
title: 'Composites / Form display',
component: FormDisplay,
- decorators: [LayoutDecorator],
+ decorators: [LayoutDecorator, ConfigDecorator],
render: args => (
- Body for relevant route(s)
-
- }
progressIndicator={
-
- Progress indicator
-
+ args.showProgressIndicator ? (
+
+ Progress indicator
+
+ ) : null
}
{...args}
- />
+ >
+
+ Body for relevant route(s)
+
+
),
argTypes: {
router: {table: {disable: true}},
progressIndicator: {table: {disable: true}},
},
+ parameters: {
+ config: {debug: false},
+ },
};
export const Default = {
args: {
showProgressIndicator: true,
- isPaymentOverview: false,
},
};
export const WithoutProgressIndicator = {
args: {
showProgressIndicator: false,
- isPaymentOverview: false,
},
};
diff --git a/src/components/FormStep/FormStep.stories.js b/src/components/FormStep/FormStep.stories.js
index d17be2372..4a98f3174 100644
--- a/src/components/FormStep/FormStep.stories.js
+++ b/src/components/FormStep/FormStep.stories.js
@@ -23,6 +23,9 @@ export default {
routerArgs: {table: {disable: true}},
},
parameters: {
+ config: {
+ debug: false,
+ },
reactRouter: {
routePath: '/stap/:step',
routeParams: {step: 'step-1'},
@@ -40,7 +43,6 @@ const render = ({
onStepSubmitted,
onLogout,
onSessionDestroyed,
- showDebug,
// story args
formioConfiguration,
}) => {
@@ -66,7 +68,6 @@ const render = ({
onStepSubmitted={onStepSubmitted}
onLogout={onLogout}
onSessionDestroyed={onSessionDestroyed}
- showDebug={showDebug}
/>
);
};
@@ -96,7 +97,6 @@ export const Default = {
},
form: buildForm(),
submission: buildSubmission(),
- showDebug: false,
},
};
@@ -126,6 +126,5 @@ export const SuspensionDisallowed = {
},
form: buildForm({suspensionAllowed: false}),
submission: buildSubmission(),
- showDebug: false,
},
};
diff --git a/src/components/FormStep/index.js b/src/components/FormStep/index.js
index 95d84178d..07c0c17cb 100644
--- a/src/components/FormStep/index.js
+++ b/src/components/FormStep/index.js
@@ -49,7 +49,6 @@ import {ValidationError} from 'errors';
import {PREFIX} from 'formio/constants';
import useTitle from 'hooks/useTitle';
import Types from 'types';
-import {DEBUG} from 'utils';
import hooks from '../../formio/hooks';
@@ -300,7 +299,6 @@ const FormStep = ({
onStepSubmitted,
onLogout,
onSessionDestroyed,
- showDebug = DEBUG,
}) => {
const intl = useIntl();
const config = useContext(ConfigContext);
@@ -867,7 +865,7 @@ const FormStep = ({
},
}}
/>
- {showDebug ? : null}
+ {config.debug ? : null}
{
+ const {languageSelectorTarget: target} = useContext(I18NContext);
+ return target ? ReactDOM.createPortal( , target) : ;
+};
+
+export default LanguageSwitcher;
diff --git a/src/components/ProgressIndicator/MobileButton.js b/src/components/ProgressIndicator/MobileButton.js
index 13a0909bf..c1003711e 100644
--- a/src/components/ProgressIndicator/MobileButton.js
+++ b/src/components/ProgressIndicator/MobileButton.js
@@ -1,35 +1,32 @@
import PropTypes from 'prop-types';
+import {forwardRef} from 'react';
import FAIcon from 'components/FAIcon';
-import {getBEMClassName} from 'utils';
-const MobileButton = ({
- ariaMobileIconLabel,
- accessibleToggleStepsLabel,
- formTitle,
- expanded,
- onExpandClick,
-}) => {
- return (
-
-
- {
+ return (
+
- {formTitle}
-
-
- );
-};
+
+
+ {formTitle}
+
+
+ );
+ }
+);
MobileButton.propTypes = {
ariaMobileIconLabel: PropTypes.string.isRequired,
diff --git a/src/components/ProgressIndicator/index.js b/src/components/ProgressIndicator/index.js
index 773f2dc6b..bb87195d2 100644
--- a/src/components/ProgressIndicator/index.js
+++ b/src/components/ProgressIndicator/index.js
@@ -1,5 +1,5 @@
import PropTypes from 'prop-types';
-import React, {useEffect, useState} from 'react';
+import React, {useEffect, useLayoutEffect, useRef, useState} from 'react';
import {useLocation} from 'react-router-dom';
import Caption from 'components/Caption';
@@ -18,10 +18,12 @@ const ProgressIndicator = ({
}) => {
const {pathname: currentPathname} = useLocation();
const [expanded, setExpanded] = useState(false);
+ const [verticalSpaceUsed, setVerticalSpaceUsed] = useState(null);
+ const buttonRef = useRef(null);
const modifiers = [];
- if (!expanded) {
- modifiers.push('mobile-collapsed');
+ if (expanded) {
+ modifiers.push('expanded');
}
// collapse the expanded progress indicator if nav occurred, see
@@ -34,17 +36,41 @@ const ProgressIndicator = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentPathname]);
+ useLayoutEffect(() => {
+ let isMounted = true;
+ if (buttonRef.current) {
+ const boundingBox = buttonRef.current.getBoundingClientRect();
+ // the offset from top + height of the element (including padding + borders)
+ isMounted && setVerticalSpaceUsed(boundingBox.bottom);
+ }
+ return () => {
+ isMounted = false;
+ };
+ }, [buttonRef, setVerticalSpaceUsed]);
+
+ const customProperties = verticalSpaceUsed
+ ? {
+ '--_of-progress-indicator-nav-mobile-inset-block-start': `${verticalSpaceUsed}px`,
+ }
+ : undefined;
return (
-
-
- setExpanded(!expanded)}
- />
- {title}
+
+ setExpanded(!expanded)}
+ />
+
+
+
+ {title}
+
{steps.map((step, index) => (
{
false
);
- expect(stepsToRender.length).toEqual(4);
+ expect(stepsToRender.length).toEqual(2);
expect(stepsToRender[0].to).toEqual('startpagina');
@@ -61,8 +61,5 @@ describe('Transforming form steps and injecting fixed steps', () => {
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 175d3aebf..764773790 100644
--- a/src/components/appointments/CreateAppointment/AppointmentProgress.js
+++ b/src/components/appointments/CreateAppointment/AppointmentProgress.js
@@ -1,9 +1,8 @@
import PropTypes from 'prop-types';
-import React, {useContext} from 'react';
+import React from 'react';
import {useIntl} from 'react-intl';
import {useLocation} from 'react-router-dom';
-import {ConfigContext} from 'Context';
import ProgressIndicator from 'components/ProgressIndicator';
import {PI_TITLE, STEP_LABELS} from 'components/constants';
import {checkMatchesPath} from 'components/utils/routers';
@@ -12,7 +11,6 @@ import {useCreateAppointmentContext} from './CreateAppointmentState';
import {APPOINTMENT_STEPS, APPOINTMENT_STEP_PATHS} from './routes';
const AppointmentProgress = ({title, currentStep}) => {
- const config = useContext(ConfigContext);
const {submission, submittedSteps} = useCreateAppointmentContext();
const intl = useIntl();
const {pathname: currentPathname} = useLocation();
@@ -55,13 +53,15 @@ const AppointmentProgress = ({title, currentStep}) => {
isCompleted: isConfirmation,
isApplicable: true,
isCurrent: checkMatchesPath(currentPathname, 'overzicht'),
- canNavigateTo: false,
+ canNavigateTo: steps.every(step => step.isCompleted),
},
{
to: 'bevestiging',
label: intl.formatMessage(STEP_LABELS.confirmation),
isCompleted: isSubmissionComplete,
+ isApplicable: true,
isCurrent: checkMatchesPath(currentPathname, 'bevestiging'),
+ canNavigateTo: isSubmissionComplete,
},
];
@@ -87,11 +87,8 @@ const AppointmentProgress = ({title, currentStep}) => {
},
{title, activeStepTitle}
);
-
- const ProgressIndicatorComponent =
- config?.displayComponents?.progressIndicator ?? ProgressIndicator;
return (
- {
const [sessionExpired, expiryDate, resetSession] = useSessionTimeout(clearSubmission);
- const config = useContext(ConfigContext);
- const FormDisplayComponent = config?.displayComponents?.form ?? FormDisplay;
const supportsMultipleProducts = form?.appointmentOptions.supportsMultipleProducts ?? false;
const currentStep =
@@ -52,6 +49,10 @@ const CreateAppointment = () => {
resetSession();
};
+ const progressIndicator = form.showProgressIndicator ? (
+
+ ) : null;
+
return (
{
submission={submission}
resetSession={reset}
>
-
-
- {isLoading ? (
-
- ) : (
-
-
-
-
-
- )}
-
-
- }
- progressIndicator={ }
- showProgressIndicator={form.showProgressIndicator}
- isPaymentOverview={false}
- />
+
+
+
+ {isLoading ? (
+
+ ) : (
+
+
+
+
+
+ )}
+
+
+
);
diff --git a/src/components/appointments/CreateAppointment/CreateAppointment.stories.js b/src/components/appointments/CreateAppointment/CreateAppointment.stories.js
index 6109de94f..27671bafc 100644
--- a/src/components/appointments/CreateAppointment/CreateAppointment.stories.js
+++ b/src/components/appointments/CreateAppointment/CreateAppointment.stories.js
@@ -219,6 +219,13 @@ export const HappyFlow = {
/I accept the privacy policy and consent to the processing of my personal data/
)
);
+
+ // test that the progress indicator displays all the expected links
+ await canvas.findByRole('link', {name: 'Product'});
+ await canvas.findByRole('link', {name: 'Afspraakdetails'});
+ await canvas.findByRole('link', {name: 'Contactgegevens'});
+ await canvas.findByRole('link', {name: 'Overzicht'});
+
const submitButton = canvas.getByRole('button', {name: 'Confirm'});
await waitFor(async () => {
expect(submitButton).not.toHaveAttribute('aria-disabled', 'true');
@@ -233,6 +240,7 @@ export const HappyFlow = {
},
{timeout: 2000, interval: 200}
);
+ await canvas.findByRole('link', {name: 'Bevestiging'});
});
},
};
diff --git a/src/scss/components/_app.scss b/src/scss/components/_app.scss
index 8b95eb238..5cc1443ce 100644
--- a/src/scss/components/_app.scss
+++ b/src/scss/components/_app.scss
@@ -10,23 +10,82 @@
* The default values are added for backwards compatibility.
*/
.openforms-app {
- display: flex;
- flex-direction: column;
- gap: var(--of-form-gap, 0);
+ display: grid;
+ grid-column-gap: var(--of-app-grid-column-gap, 20px);
+ grid-template-columns: 2fr 1fr;
+ grid-template-areas:
+ 'lang-switcher lang-switcher'
+ 'body progress-indicator'
+ 'debug debug';
position: relative;
- @include mobile-only {
- padding-block-end: var(--of-form-mobile-padding-block-end, 15px);
- padding-block-start: var(--of-form-mobile-padding-block-start, 0);
- padding-inline-end: var(--of-form-mobile-padding-inline-end, 15px);
- padding-inline-start: var(--of-form-mobile-padding-inline-start, 15px);
+ // When there's no progress indicator, stretch the main content
+ // over both containers.
+ @include bem.modifier('no-progress-indicator') {
+ @include bem.element('body') {
+ grid-row-start: body;
+ grid-row-end: progress-indicator;
+ grid-column-start: body;
+ grid-column-end: progress-indicator;
+ }
}
@include bem.element('language-switcher') {
+ grid-area: lang-switcher;
display: flex;
}
+ @include bem.element('body') {
+ grid-area: body;
+ padding-block-end: var(--of-app-body-padding-block-end, 0);
+ padding-block-start: var(--of-app-body-padding-block-start, 0);
+ }
+
+ @include bem.element('progress-indicator') {
+ grid-area: progress-indicator;
+ }
+
@include bem.element('debug') {
+ grid-area: debug;
margin-block-start: 2em;
}
+
+ // Responsive styles - switch to a column layout and re-order elements.
+ @include mobile-only {
+ // https://stackoverflow.com/a/63609468 just 1fr doesn't work, but minmax does?
+ grid-template-columns: minmax(0, 1fr);
+ grid-template-areas:
+ 'progress-indicator'
+ 'lang-switcher'
+ 'body'
+ 'debug';
+ grid-row-gap: var(--of-app-mobile-grid-row-gap, 0);
+
+ padding-block-end: var(--of-app-mobile-padding-block-end, 15px);
+ padding-block-start: var(--of-app-mobile-padding-block-start, 0);
+ padding-inline-end: var(--of-app-mobile-padding-inline-end, 15px);
+ padding-inline-start: var(--of-app-mobile-padding-inline-start, 15px);
+
+ @include bem.element('body') {
+ padding-block-end: var(
+ --of-app-body-mobile-padding-block-end,
+ var(--of-app-body-padding-block-end, 0)
+ );
+ padding-block-start: var(
+ --of-app-body-mobile-padding-block-start,
+ var(--of-app-body-padding-block-start, 15px)
+ );
+ }
+
+ @include bem.element('progress-indicator') {
+ margin-inline-end: var(--of-app-progress-indicator-mobile-margin-inline-end, -15px);
+ margin-inline-start: var(--of-app-progress-indicator-mobile-margin-inline-start, -15px);
+
+ // on mobile, the order of elements is swapped and to keep the progress indicator
+ // in view, we need to apply the positioning to this element rather than
+ // .openforms-progress-indicator
+ position: sticky;
+ inset-block-start: var(--of-app-progress-indicator-mobile-inset-block-start, 0);
+ }
+ }
}
diff --git a/src/scss/components/_card.scss b/src/scss/components/_card.scss
index 44959cfbe..fc9dbf27c 100644
--- a/src/scss/components/_card.scss
+++ b/src/scss/components/_card.scss
@@ -5,11 +5,18 @@
@import '../mixins/prefix';
-.#{prefix('card')} {
- @include color-background;
- padding: $grid-margin-8;
+@mixin card($block: 'card') {
+ background-color: var(--of-#{$block}-background-color, var(--of-color-bg));
box-sizing: border-box;
- width: 100%;
+ padding-block-end: var(--of-#{$block}-padding-block-end, 40px);
+ padding-block-start: var(--of-#{$block}-padding-block-start, 40px);
+ padding-inline-end: var(--of-#{$block}-padding-inline-end, 40px);
+ padding-inline-start: var(--of-#{$block}-padding-inline-start, 40px);
+ inline-size: 100%;
+}
+
+.openforms-card {
+ @include card;
@include bem.element('header') {
@include bem.modifier('padded') {
diff --git a/src/scss/components/_form.scss b/src/scss/components/_form.scss
deleted file mode 100644
index 4a9eb9e88..000000000
--- a/src/scss/components/_form.scss
+++ /dev/null
@@ -1,46 +0,0 @@
-@use 'microscope-sass/lib/bem';
-
-// @import instead of @use because breakpoints are defined globally
-@import 'microscope-sass/lib/responsive';
-
-/**
- * Custom component to manage our form layout, arranging body and progress indicator.
- *
- * The default values are added for backwards compatibility.
- */
-.openforms-form {
- display: grid;
- grid-template-columns: 2fr 1fr;
- grid-template-areas: 'body progress-indicator';
- grid-column-gap: var(--of-form-grid-column-gap, 20px);
- position: relative;
-
- @include bem.modifier('body-only') {
- grid-template-columns: 1fr;
- grid-template-areas: 'body';
- }
-
- @include bem.element('body') {
- grid-area: body;
- }
-
- @include bem.element('progress-indicator') {
- grid-area: progress-indicator;
- }
-
- @include mobile-only {
- grid-row-gap: var(--of-form-grid-mobile-row-gap, 15px);
- grid-template-columns: 1fr;
- grid-template-rows: auto;
- grid-template-areas:
- 'progress-indicator'
- 'body';
-
- @include bem.element('progress-indicator') {
- position: sticky;
- top: 0;
- margin-inline-end: var(--of-form-progress-indicator-mobile-margin-inline-end, -15px);
- margin-inline-start: var(--of-form-progress-indicator-mobile-margin-inline-start, -15px);
- }
- }
-}
diff --git a/src/scss/components/_language-selection.scss b/src/scss/components/_language-selection.scss
index 2c4c398e2..9783e30d1 100644
--- a/src/scss/components/_language-selection.scss
+++ b/src/scss/components/_language-selection.scss
@@ -17,15 +17,25 @@
}
}
- .openforms-button {
- padding: 0;
- }
-
// if we are not rendering in a designated portal node, ensure it's aligned to the
// right
@at-root .openforms-app__language-switcher & {
+ --utrecht-button-group-padding-block-end: var(--of-language-selection-in-app-padding-block-end);
+ --utrecht-button-group-padding-block-start: var(
+ --of-language-selection-in-app-padding-block-start
+ );
margin-inline-start: auto;
- margin-block-end: var(--of-language-selection-in-app-margin-block-end);
+
+ @include mobile-only {
+ --utrecht-button-group-padding-block-end: var(
+ --of-language-selection-in-app-mobile-padding-block-end,
+ var(--of-language-selection-in-app-padding-block-end)
+ );
+ --utrecht-button-group-padding-block-start: var(
+ --of-language-selection-in-app-mobile-padding-block-start,
+ var(--of-language-selection-in-app-padding-block-start)
+ );
+ }
}
}
diff --git a/src/scss/components/_list.scss b/src/scss/components/_list.scss
index 66d52f61b..7aa5ee28c 100644
--- a/src/scss/components/_list.scss
+++ b/src/scss/components/_list.scss
@@ -7,36 +7,32 @@
@import '../mixins/prefix';
.#{prefix('list')} {
- .#{prefix('caption')} + & {
- @include margin(true, $properties: margin-top);
- }
-
list-style: none;
margin: 0;
padding: 0;
+ display: flex;
+ flex-direction: column;
+ gap: var(--of-list-gap, 20px);
+
@include bem.element('item') {
@include body;
@include body--big;
}
+ // TODO: check if this can be removed
&:not(#{&}--compact, #{&}--extra-compact) &__item {
- @include margin;
display: flex;
align-items: center;
flex-wrap: wrap;
}
@include bem.modifier('compact') {
- @include bem.element('item') {
- @include margin($value-mobile: $grid-margin-1);
- }
+ --of-list-gap: var(--of-list-compact-gap, 8px);
}
@include bem.modifier('extra-compact') {
- @include bem.element('item') {
- @include margin(false);
- }
+ --of-list-gap: var(--of-list-extra-compact-gap, 0px);
}
@include bem.modifier('dash') {
diff --git a/src/scss/components/_progress-indicator.scss b/src/scss/components/_progress-indicator.scss
index 40b68b3f4..1153f98d2 100644
--- a/src/scss/components/_progress-indicator.scss
+++ b/src/scss/components/_progress-indicator.scss
@@ -1,7 +1,5 @@
@use 'microscope-sass/lib/bem';
-
-@import '~microscope-sass/lib/grid';
-@import '~microscope-sass/lib/typography';
+@use './card';
@import '../mixins/prefix';
@@ -11,79 +9,146 @@
}
}
-.#{prefix('progress-indicator')} {
- @extend .openforms-card; // TODO -> syntax highlighting trips on #{prefix('card')}
-
+/**
+ * Desktop and mobile styling for the progress indicator element.
+ *
+ * The parent component (_app.scss) is responsible for re-arranging the order of
+ * elements depending on the viewport. The styles of this component are responsible for
+ * managing how it looks/behaves on mobile/non-mobile viewports:
+ *
+ * - on mobile, the element takes a navbar-like approach that can be expanded/collapsed
+ * - on desktop, it behaves like a sticky element, so that it can be placed in a sidebar
+ *
+ * TODO: remove the fallbacks/defaults after a deprecation period. Many of these
+ * design tokens here didn't exist before 2.1 and had hardcoded values.
+ */
+.openforms-progress-indicator {
+ @include card.card('progress-indicator');
+
+ position: sticky;
+ inset-block-start: var(
+ --of-progress-indicator-inset-block-start,
+ var(--of-app-grid-column-gap, 20px)
+ );
+
+ // Do not display the toggle button on non-mobile devices
@include bem.element('mobile-header') {
- @include body;
- @include body--big;
- @include margin(-$grid-container-margin, $properties: margin-left);
-
- // style for replacing div with a button for accessibility
- background: none;
- border: none;
- padding: 0;
- cursor: pointer;
- width: calc(100% + $grid-container-margin);
-
- @include show-on-mobile(flex);
-
- align-items: center;
-
- .fa-icon {
- display: block;
- flex-shrink: 0;
- flex-basis: $grid-container-margin * 2;
- text-align: center;
- }
+ display: none;
}
- @include bem.element('form-title') {
- @include ellipsis;
- font-weight: bold;
+ @include bem.element('nav') {
+ display: flex;
+ flex-direction: column;
+ gap: var(--of-progress-indicator-nav-gap, 20px);
}
- // mobile styling for the progress indicator
+ /**
+ * Responsive styles, mobile viewports.
+ *
+ * The default state is collapsed, for the expanded state, see the expanded modifier
+ * styles.
+ */
@include mobile-only {
- @include margin($grid-container-margin, $properties: (padding-top, padding-bottom));
- box-shadow: var(--of-progress-indicator-mobile-box-shadow);
- // otherwise the bar is too short, and setting a width creates a horizontal scrollbar
- width: unset;
-
- // style layout
- @at-root .#{prefix('layout__row')} & {
- @include margin(
- calc(-1 * var(--of-progress-indicator-mobile-margin)),
- $properties: (margin-left, margin-right)
+ // remove any padding from the container element - instead, we apply mobile padding
+ // on the button element so that it's easier to tap on mobile devices.
+ --of-progress-indicator-padding-block-end: 0;
+ --of-progress-indicator-padding-block-start: 0;
+ --of-progress-indicator-padding-inline-end: 0;
+ --of-progress-indicator-padding-inline-start: 0;
+
+ /**
+ * The mobile-header button is visible on mobile, and while it is a button for
+ * acessibility, it should not look like one.
+ */
+ @include bem.element('mobile-header') {
+ // reset base/default user agent styles
+ all: unset;
+ box-shadow: var(--of-progress-indicator-mobile-box-shadow);
+ box-sizing: border-box;
+ cursor: pointer;
+
+ display: flex;
+ justify-content: flex-start;
+ align-items: center;
+ gap: var(--of-progress-indicator-mobile-header-gap, 0px);
+ inline-size: 100%;
+
+ padding-block-end: var(--of-progress-indicator-mobile-padding-block-end, 15px);
+ padding-block-start: var(--of-progress-indicator-mobile-padding-block-start, 15px);
+ padding-inline-end: var(--of-progress-indicator-mobile-padding-inline-end, 15px);
+ padding-inline-start: var(--of-progress-indicator-mobile-padding-inline-start, 15px);
+
+ // include for backwards compatibility reasons
+ color: var(--of-progress-indicator-mobile-header-color, var(--of-color-fg, inherit));
+ font-family: var(
+ --of-progress-indicator-mobile-header-font-family,
+ var(--utrecht-document-font-family, var(--of-typography-sans-serif-font-family, inherit))
);
+ font-size: var(--of-progress-indicator-mobile-header-font-size, 1.125rem);
+ line-height: var(--of-progress-indicator-mobile-header-line-height, 1.1333);
+
+ .fa-icon {
+ display: block;
+ flex-shrink: 0;
+ flex-basis: var(--of-progress-indicator-mobile-header-icon-flex-basis, 30px);
+ text-align: center;
+ }
}
- @include nested('caption') {
- @include margin(true, $properties: margin-top);
- @include margin($grid-container-margin, $properties: margin-left);
-
- @include mobile-only {
- display: none;
- }
+ // TODO: provide design tokens?
+ @include bem.element('form-title') {
+ @include ellipsis;
+ font-weight: bold;
}
- @include nested('list') {
- @include margin($grid-container-margin, $properties: margin-left);
+ // Hide by default on mobile
+ @include bem.element('nav') {
+ display: none;
}
- @include bem.modifier('mobile-collapsed') {
- @include nested('caption') {
- display: none;
- }
+ // Bit of a BEM violation, but the captions are due a refactor to NL DS at some
+ // point too.
+ @include nested('caption') {
+ display: none;
+ }
- @include nested('list') {
- display: none;
+ /**
+ * Appearance for the expanded variant.
+ */
+ @include bem.modifier('expanded') {
+ @include bem.element('nav') {
+ --of-list-gap: var(--of-progress-indicator-nav-mobile-list-gap, 15px);
+
+ box-shadow: var(--of-progress-indicator-mobile-box-shadow);
+ box-sizing: border-box;
+
+ display: block;
+ // absolute positioning to not push the content below down, since it must be
+ // an overlay.
+ // TODO: there are future CSS features that make anchoring elements to other
+ // elements much developer-friendlier without requiring 'magic numbers'.
+ position: absolute;
+ background: var(--of-progress-indicator-background-color, var(--of-color-bg));
+
+ padding-block-end: var(--of-progress-indicator-nav-mobile-padding-block-end, 15px);
+ padding-block-start: var(--of-progress-indicator-nav-mobile-padding-block-start, 15px);
+ padding-inline-end: var(--of-progress-indicator-nav-mobile-padding-inline-end, 15px);
+ padding-inline-start: var(--of-progress-indicator-nav-mobile-padding-inline-start, 30px);
+
+ z-index: 1;
+ inline-size: 100%;
+
+ // use the entire viewport minus the block space (vertical) used by the button and potential
+ // third party elements above
+ max-block-size: calc(100dvb - var(--_of-progress-indicator-nav-mobile-inset-block-start));
+ overflow-y: auto;
}
}
}
}
-.#{prefix('progress-indicator-item')} {
+// TODO: parametrize with design tokens
+.openforms-progress-indicator-item {
display: flex;
justify-content: flex-start;
diff --git a/src/styles.scss b/src/styles.scss
index 2eb60e937..d8dbcbf2b 100644
--- a/src/styles.scss
+++ b/src/styles.scss
@@ -31,7 +31,6 @@
@import './scss/components/checkbox';
@import './scss/components/content';
@import './scss/components/errors';
-@import './scss/components/form';
@import './scss/components/formio-component';
@import './scss/components/help-text';
@import './scss/components/image';
diff --git a/src/upgrade-notes.mdx b/src/upgrade-notes.mdx
new file mode 100644
index 000000000..abd11ec6c
--- /dev/null
+++ b/src/upgrade-notes.mdx
@@ -0,0 +1,91 @@
+import {Meta} from '@storybook/blocks';
+
+
+
+# Upgrading from 2.0.x to 2.1.x
+
+In the Open Forms SDK 2.1 we've refactor the app scaffolding to simplify the CSS and markup used,
+fix a number of bugs and add the ability to tweak the appearance through design tokens.
+
+This mostly affects used-to-be private API, but if you have or had custom CSS overrides for this,
+they are likely broken. Users that do not override the default Open Forms theme are not affected.
+
+## Summary of changes
+
+- The `.openforms-layout*` classes, markup and associated design tokens are gone
+- The outer container is now the `AppDisplay` component, with the `.openforms-app` class name and
+ associated `--of-app-*` design tokens. We now place elements using CSS grid to appropriately
+ target styling for mobile devices.
+- `FormDisplay` is now a thin wrapper around `AppDisplay` and can no longer be overridden. This
+ display configuration is still considered experimental.
+
+## Detailed changes and how to upgrade
+
+### `.utrecht-document` class and design tokens
+
+We do not create an element with the `utrecht-document` and `utrecht-document--surface` class names,
+but they are expected by the SDK. If you are embedding forms on your CMS/website, please make sure a
+parent of the form root has these class names.
+
+You can then use the following design tokens for basic styles:
+
+- `--utrecht-document-background-color`
+- `--utrecht-document-color`
+- `--utrecht-document-font-family`
+- `--utrecht-document-font-size`
+- `--utrecht-document-font-weight`
+- `--utrecht-document-line-height`
+
+These properties are inherited when not explicitly set.
+
+### App component
+
+The app component is the outer shell that creates the layout of the form body, progress indicator
+(sidebar on desktop, dropdown menu on mobile) and the language selection buttons.
+
+Currently, we have set default values for the design tokens, but they will be removed in SDK 3.0.
+
+Relevant design tokens:
+
+- `--of-app-body-padding-block-{end,start}`: additional vertical spacing for the main content
+- `--of-app-grid-column-gap`: spacing between main content and progress indicator, on devices larger
+ than mobile.
+
+**Mobile**
+
+- `--of-app-body-mobile-padding-block-{end,start}`: vertical spacing between main content and
+ progress indicator/debug information
+- `--of-app-mobile-grid-row-gap`: vertical spacing between components
+- `--of-app-mobile-padding-{block,inline}-{end,start}`: padding between content and device edges
+- `--of-app-progress-indicator-mobile-margin-inline-{end,start}`: margin for the progress indicator.
+ Setting this to the negative inline padding values above will stretch the button across the full
+ width of the screen.
+- `--of-app-progress-indicator-mobile-inset-block-start`: offset of the sticky toggle button/header
+ to the top of the screen - set to a non-zero value if your own menu should always visible in the
+ screen.
+
+### Card component
+
+The card component is used to wrap the main body content (the form fields, some modals...). It used
+to have hardcoded colors/paddings but they are now customizable through design tokens.
+
+Currently, we have set default values for the design tokens, but they may be removed in SDK 3.0.
+
+Relevant design tokens:
+
+- `--of-card-background-color`
+- `--of-card-padding-block-end`
+- `--of-card-padding-block-start`
+- `--of-card-padding-inline-end`
+- `--of-card-padding-inline-start`
+
+### Language-selection
+
+If a form supports multiple languages and you don't render the language selection into a particular
+node (provided at SDK init time), then you can control the spacing of the element using the
+following design tokens:
+
+- `--of-language-selection-in-app-padding-block-end`
+- `--of-language-selection-in-app-padding-block-start`
+- `--of-language-selection-in-app-mobile-padding-block-end`
+- `--of-language-selection-in-app-mobile-padding-block-start`