diff --git a/.changeset/loud-turtles-mate.md b/.changeset/loud-turtles-mate.md new file mode 100644 index 0000000000..533f899c92 --- /dev/null +++ b/.changeset/loud-turtles-mate.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': patch +--- + +Scroll to the top of the page when navigating between steps in template forms. diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx index af9117ccf5..b0746e0672 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.test.tsx @@ -672,6 +672,40 @@ describe('Stepper', () => { ); }); + it('should scroll the first main element to top when activeStep changes', async () => { + const manifest: TemplateParameterSchema = { + steps: [ + { title: 'Step 1', schema: { properties: {} } }, + { title: 'Step 2', schema: { properties: {} } }, + ], + title: 'Scroll Test', + }; + + // Render a main element in the document for the Stepper to find + const main = document.createElement('main'); + document.body.appendChild(main); + const scrollToMock = jest.fn(); + main.scrollTo = scrollToMock; + + // Render Stepper as usual (do not pass container) + const { getByRole, unmount } = await renderInTestApp( + + + , + ); + + // Click next to change the activeStep + await act(async () => { + fireEvent.click(getByRole('button', { name: 'Next' })); + }); + + expect(scrollToMock).toHaveBeenCalledWith({ top: 0, behavior: 'auto' }); + + // Clean up + document.body.removeChild(main); + unmount(); + }); + describe('Scaffolder Layouts', () => { it('should render the step in the scaffolder layout', async () => { const ScaffolderLayout: LayoutTemplate = ({ properties }) => ( diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 6715eb0e1e..1f36eb58d3 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -37,6 +37,7 @@ import { merge } from 'lodash'; import { ComponentType, useCallback, + useEffect, useMemo, useState, type ReactNode, @@ -218,6 +219,13 @@ export const Stepper = (stepperProps: StepperProps) => { [validation, analytics], ); + useEffect(() => { + const main = document.querySelector('main'); + if (main && typeof main.scrollTo === 'function') { + main.scrollTo({ top: 0, behavior: 'auto' }); + } + }, [activeStep]); + const mergedUiSchema = merge({}, propUiSchema, currentStep?.uiSchema); const [isCreating, setIsCreating] = useState(false);