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 (
); + return ( +
+ +
+ ); }; diff --git a/.storybook/preview.js b/.storybook/preview.js index a86a07af9..ec3b30831 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -17,7 +17,11 @@ import 'styles.scss'; import OpenFormsModule from 'formio/module'; import OFLibrary from 'formio/templates'; -import {utrechtDocumentDecorator, withClearSessionStorage} from './decorators'; +import { + utrechtDocumentDecorator, + withClearSessionStorage, + withClearSubmissionLocalStorage, +} from './decorators'; import {reactIntl} from './reactIntl.js'; initialize({ @@ -35,7 +39,12 @@ Formio.use(OpenFormsModule); Templates.current = OFLibrary; export default { - decorators: [mswDecorator, withClearSessionStorage, utrechtDocumentDecorator], + decorators: [ + mswDecorator, + withClearSessionStorage, + withClearSubmissionLocalStorage, + utrechtDocumentDecorator, + ], globals: { locale: reactIntl.defaultLocale, locales: { diff --git a/design-tokens b/design-tokens index ff79cdda4..326390434 160000 --- a/design-tokens +++ b/design-tokens @@ -1 +1 @@ -Subproject commit ff79cdda4f02e2fec08e95e1e399ac25244ef8c4 +Subproject commit 326390434b5eb44bf82c3b1d2996b3a1012014eb diff --git a/package.json b/package.json index cf7715e1c..e723ce51b 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "@floating-ui/react": "^0.24.2", "@formio/protected-eval": "^1.2.1", "@fortawesome/fontawesome-free": "^6.1.1", - "@open-formulieren/design-tokens": "^0.50.0", + "@open-formulieren/design-tokens": "^0.51.0", "@sentry/react": "^6.13.2", "@sentry/tracing": "^6.13.2", "@trivago/prettier-plugin-sort-imports": "^4.0.0", diff --git a/src/Context.js b/src/Context.js index b9a554ae2..9bd99b470 100644 --- a/src/Context.js +++ b/src/Context.js @@ -1,5 +1,7 @@ import React from 'react'; +import {DEBUG} from 'utils'; + const FormContext = React.createContext({ uuid: '', name: '', @@ -29,10 +31,9 @@ const ConfigContext = React.createContext({ requiredFieldsWithAsterisk: true, displayComponents: { app: null, - form: null, - progressIndicator: null, loginOptions: null, }, + debug: DEBUG, }); ConfigContext.displayName = 'ConfigContext'; diff --git a/src/components/App.js b/src/components/App.js index 1e6ab8c80..68ed4fc70 100644 --- a/src/components/App.js +++ b/src/components/App.js @@ -1,13 +1,8 @@ -import PropTypes from 'prop-types'; -import React, {useContext} from 'react'; -import ReactDOM from 'react-dom'; +import React from 'react'; import {Navigate, Outlet, useMatch} from 'react-router-dom'; -import {ConfigContext} from 'Context'; -import AppDebug from 'components/AppDebug'; import {Cosign} from 'components/CoSign'; import Form from 'components/Form'; -import LanguageSelection from 'components/LanguageSelection'; import { CreateAppointment, appointmentRoutes, @@ -16,10 +11,6 @@ import { import useFormContext from 'hooks/useFormContext'; import useQuery from 'hooks/useQuery'; import useZodErrorMap from 'hooks/useZodErrorMap'; -import {I18NContext} from 'i18n'; -import {DEBUG} from 'utils'; - -import AppDisplay from './AppDisplay'; export const routes = [ { @@ -42,30 +33,18 @@ export const routes = [ }, ]; -const LanguageSwitcher = () => { - const {languageSelectorTarget: target} = useContext(I18NContext); - return target ? ReactDOM.createPortal(, target) : ; -}; - /* Top level router - routing between an actual form or supporting screens. */ -const App = ({noDebug = false}) => { +const App = () => { const form = useFormContext(); const query = useQuery(); - const config = useContext(ConfigContext); const appointmentMatch = useMatch('afspraak-maken/*'); const appointmentCancelMatch = useMatch('afspraak-annuleren/*'); // register localized error messages in the default zod error map useZodErrorMap(); - const AppDisplayComponent = config?.displayComponents?.app ?? AppDisplay; - - const {translationEnabled} = form; - const languageSwitcher = translationEnabled ? : null; - const appDebug = DEBUG && !noDebug ? : null; - const isAppointment = form.appointmentOptions?.isAppointment ?? false; if (isAppointment && !appointmentMatch && !appointmentCancelMatch) { return ( @@ -78,18 +57,9 @@ const App = ({noDebug = false}) => { /> ); } - - return ( - } - languageSwitcher={languageSwitcher} - appDebug={appDebug} - /> - ); + return ; }; -App.propTypes = { - noDebug: PropTypes.bool, -}; +App.propTypes = {}; export default App; diff --git a/src/components/App.stories.js b/src/components/App.stories.js index 5c14ab79d..3c50e8e9f 100644 --- a/src/components/App.stories.js +++ b/src/components/App.stories.js @@ -20,8 +20,9 @@ import {SUBMISSION_ALLOWED} from './constants'; export default { title: 'Private API / App', component: App, - decorators: [LayoutDecorator, ConfigDecorator], + decorators: [ConfigDecorator], args: { + name: 'Mock form', 'form.translationEnabled': true, submissionAllowed: SUBMISSION_ALLOWED.yes, hideNonApplicableSteps: false, @@ -55,10 +56,9 @@ export default { completed: false, }, ], + showExternalHeader: false, }, argTypes: { - form: {table: {disable: true}}, - noDebug: {table: {disable: true}}, submissionAllowed: { options: Object.values(SUBMISSION_ALLOWED), control: {type: 'radio'}, @@ -69,6 +69,9 @@ export default { }, }, parameters: { + config: { + debug: false, + }, msw: { handlers: [ mockLanguageInfoGet([ @@ -81,11 +84,11 @@ export default { }, }; -const Wrapper = ({form}) => { +const Wrapper = ({form, showExternalHeader}) => { const routes = [ { path: '*', - element: , + element: , children: nestedRoutes, }, ]; @@ -95,6 +98,9 @@ const Wrapper = ({form}) => { }); return ( + {showExternalHeader && ( +
External header
+ )}
); @@ -102,36 +108,38 @@ const Wrapper = ({form}) => { const render = args => { const form = buildForm({ + name: args.name, translationEnabled: args['form.translationEnabled'], explanationTemplate: '

Toelichtingssjabloon...

', submissionAllowed: args['submissionAllowed'], hideNonApplicableSteps: args['hideNonApplicableSteps'], steps: args['steps'], }); - return ; + return ; }; export const Default = { render, + decorators: [LayoutDecorator], }; export const TranslationEnabled = { - render, + ...Default, args: { 'form.translationEnabled': true, }, - play: async ({args, canvasElement}) => { + play: async ({canvasElement}) => { const langSelector = await within(canvasElement).findByText(/^nl$/i); await expect(langSelector).toBeTruthy(); }, }; export const TranslationDisabled = { - render, + ...Default, args: { 'form.translationEnabled': false, }, - play: async ({args, canvasElement}) => { + play: async ({canvasElement}) => { const canvas = within(canvasElement); // wait for spinners to disappear @@ -145,17 +153,8 @@ export const TranslationDisabled = { }; export const ActiveSubmission = { + ...Default, 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: [ { @@ -201,3 +200,178 @@ export const ActiveSubmission = { await userEvent.click(beginButton); }, }; + +export const SeveralStepsInMobileViewport = { + render, + args: { + showExternalHeader: true, + name: 'A rather long form name that overflows on mobile', + 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, + }, + { + uuid: '71fa50c1-53db-4179-9fe7-eb3378ef39ee', + slug: 'step-2', + formDefinition: 'Step 2', + index: 1, + literals: { + previousText: {resolved: 'Previous', value: ''}, + saveText: {resolved: 'Save', value: ''}, + nextText: {resolved: 'Next', value: ''}, + }, + url: `${BASE_URL}forms/mock/steps/71fa50c1-53db-4179-9fe7-eb3378ef39ee`, + isApplicable: true, + }, + { + uuid: 'f4f82113-ac83-429b-a17b-c1b145831fa9', + slug: 'step-3', + formDefinition: 'Step 3', + index: 2, + literals: { + previousText: {resolved: 'Previous', value: ''}, + saveText: {resolved: 'Save', value: ''}, + nextText: {resolved: 'Next', value: ''}, + }, + url: `${BASE_URL}forms/mock/steps/f4f82113-ac83-429b-a17b-c1b145831fa9`, + isApplicable: true, + }, + { + uuid: 'ae5af44e-004f-4d2f-be75-d46a22577244', + slug: 'step-4', + formDefinition: 'Step 4', + index: 3, + literals: { + previousText: {resolved: 'Previous', value: ''}, + saveText: {resolved: 'Save', value: ''}, + nextText: {resolved: 'Next', value: ''}, + }, + url: `${BASE_URL}forms/mock/steps/ae5af44e-004f-4d2f-be75-d46a22577244`, + isApplicable: true, + }, + { + uuid: 'cae11cf8-2773-4d70-80f4-71e5828b6fe3', + slug: 'step-5', + formDefinition: 'Step 5', + index: 4, + literals: { + previousText: {resolved: 'Previous', value: ''}, + saveText: {resolved: 'Save', value: ''}, + nextText: {resolved: 'Next', value: ''}, + }, + url: `${BASE_URL}forms/mock/steps/cae11cf8-2773-4d70-80f4-71e5828b6fe3`, + isApplicable: true, + }, + { + uuid: '3b14f4f5-2283-4a81-adf1-03848672c83b', + slug: 'step-6', + formDefinition: 'Step 6', + index: 5, + literals: { + previousText: {resolved: 'Previous', value: ''}, + saveText: {resolved: 'Save', value: ''}, + nextText: {resolved: 'Next', value: ''}, + }, + url: `${BASE_URL}forms/mock/steps/3b14f4f5-2283-4a81-adf1-03848672c83b`, + isApplicable: true, + }, + { + uuid: '3f0a2763-74de-4957-b970-a5117f15b023', + slug: 'step-7', + formDefinition: 'Step 7', + index: 6, + literals: { + previousText: {resolved: 'Previous', value: ''}, + saveText: {resolved: 'Save', value: ''}, + nextText: {resolved: 'Next', value: ''}, + }, + url: `${BASE_URL}forms/mock/steps/3f0a2763-74de-4957-b970-a5117f15b023`, + isApplicable: true, + }, + { + uuid: '03657dc1-6bb1-49cf-8113-4b663981b70f', + slug: 'step-8', + formDefinition: 'Step 8', + index: 7, + literals: { + previousText: {resolved: 'Previous', value: ''}, + saveText: {resolved: 'Save', value: ''}, + nextText: {resolved: 'Next', value: ''}, + }, + url: `${BASE_URL}forms/mock/steps/03657dc1-6bb1-49cf-8113-4b663981b70f`, + isApplicable: true, + }, + { + uuid: '4d4767b6-a3a4-4519-a1d3-f81e15bf829c', + slug: 'step-9', + formDefinition: 'Step 9', + index: 8, + literals: { + previousText: {resolved: 'Previous', value: ''}, + saveText: {resolved: 'Save', value: ''}, + nextText: {resolved: 'Next', value: ''}, + }, + url: `${BASE_URL}forms/mock/steps/4d4767b6-a3a4-4519-a1d3-f81e15bf829c`, + isApplicable: true, + }, + { + uuid: '9166d9b7-baa9-429a-a19e-c0c88f2fdaa8', + slug: 'step-10', + formDefinition: 'Step 10', + index: 9, + literals: { + previousText: {resolved: 'Previous', value: ''}, + saveText: {resolved: 'Save', value: ''}, + nextText: {resolved: 'Next', value: ''}, + }, + url: `${BASE_URL}forms/mock/steps/9166d9b7-baa9-429a-a19e-c0c88f2fdaa8`, + isApplicable: true, + }, + { + uuid: 'c5eb8263-39e7-4e8c-a6f4-77278f50ee52', + slug: 'step-11', + formDefinition: 'Step 11', + index: 10, + literals: { + previousText: {resolved: 'Previous', value: ''}, + saveText: {resolved: 'Save', value: ''}, + nextText: {resolved: 'Next', value: ''}, + }, + url: `${BASE_URL}forms/mock/steps/c5eb8263-39e7-4e8c-a6f4-77278f50ee52`, + isApplicable: true, + }, + { + uuid: '4f79f369-7aca-4346-9d91-5d5e628b7fb0', + slug: 'step-12', + formDefinition: 'Step 12', + index: 11, + literals: { + previousText: {resolved: 'Previous', value: ''}, + saveText: {resolved: 'Save', value: ''}, + nextText: {resolved: 'Next', value: ''}, + }, + url: `${BASE_URL}forms/mock/steps/4f79f369-7aca-4346-9d91-5d5e628b7fb0`, + isApplicable: true, + }, + ], + ariaMobileIconLabel: 'Progress step indicator toggle icon (mobile)', + accessibleToggleStepsLabel: 'Current step in form Formulier: Stap 2', + }, + parameters: { + chromatic: {disableSnapshot: true}, // TODO: set up viewports in chromatic + layout: 'fullscreen', // removes padding in canvas + viewport: { + defaultViewport: 'mobile1', + }, + }, +}; diff --git a/src/components/AppDisplay.js b/src/components/AppDisplay.js index 1392df7b6..1bbabfbca 100644 --- a/src/components/AppDisplay.js +++ b/src/components/AppDisplay.js @@ -1,18 +1,48 @@ +import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; -const AppDisplay = ({router, languageSwitcher = null, appDebug = null}) => ( -
+/** + * 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 ( - - ); -}; + + + {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 ( - -