diff --git a/src/app/base/components/FormikFormContent/FormikFormContent.test.tsx b/src/app/base/components/FormikFormContent/FormikFormContent.test.tsx
index e4b9a97d16..00efb5cbcb 100644
--- a/src/app/base/components/FormikFormContent/FormikFormContent.test.tsx
+++ b/src/app/base/components/FormikFormContent/FormikFormContent.test.tsx
@@ -2,6 +2,7 @@ import { Field, Formik } from "formik";
import { Provider } from "react-redux";
import { MemoryRouter } from "react-router-dom";
import configureStore from "redux-mock-store";
+import type { Mock } from "vitest";
import * as Yup from "yup";
import FormikFormContent from "./FormikFormContent";
@@ -31,7 +32,12 @@ vi.mock("react-router-dom", async () => {
describe("FormikFormContent", () => {
let state: RootState;
+ let scrollIntoViewSpy: Mock;
+
beforeEach(() => {
+ scrollIntoViewSpy = vi.fn();
+ window.HTMLElement.prototype.scrollIntoView = scrollIntoViewSpy;
+
state = factory.rootState({
config: factory.configState({
items: [
@@ -101,6 +107,18 @@ describe("FormikFormContent", () => {
expect(screen.getByText("Uh oh!")).toBeInTheDocument();
});
+ it("scrolls non-field errors into view when present", () => {
+ renderWithBrowserRouter(
+
+ Content
+ ,
+ { state }
+ );
+
+ expect(screen.getByText("Uh oh!")).toBeInTheDocument();
+ expect(scrollIntoViewSpy).toHaveBeenCalledTimes(1);
+ });
+
it("can display non-field errors from the __all__ key", () => {
renderWithBrowserRouter(
diff --git a/src/app/base/components/FormikFormContent/FormikFormContent.tsx b/src/app/base/components/FormikFormContent/FormikFormContent.tsx
index a74d483df0..5c6b6ff6c9 100644
--- a/src/app/base/components/FormikFormContent/FormikFormContent.tsx
+++ b/src/app/base/components/FormikFormContent/FormikFormContent.tsx
@@ -1,5 +1,5 @@
import type { ReactNode, AriaAttributes } from "react";
-import React, { useEffect, useRef } from "react";
+import React, { useEffect, useId, useRef } from "react";
import { ContentSection } from "@canonical/maas-react-components";
import { Form, Notification } from "@canonical/react-components";
@@ -110,6 +110,7 @@ const FormikFormContent = ({
allowUnchanged,
});
const [hasSaved, resetHasSaved] = useCycled(saved);
+ const notificationId = useId();
const hasErrors = !!errors;
// Run onValuesChanged function whenever formik values change.
@@ -168,10 +169,18 @@ const FormikFormContent = ({
}
}, [navigate, savedRedirect, saved]);
+ useEffect(() => {
+ if (nonFieldError) {
+ document
+ .getElementById(notificationId)
+ ?.scrollIntoView({ behavior: "smooth" });
+ }
+ }, [nonFieldError, notificationId]);
+
const renderFormContent = () => (
<>
{!!nonFieldError && (
-
+
{nonFieldError}
)}
diff --git a/src/setupTests.ts b/src/setupTests.ts
index 1cf1e32c8f..785ee13a88 100644
--- a/src/setupTests.ts
+++ b/src/setupTests.ts
@@ -8,21 +8,26 @@ const fetchMocker = createFetchMock(vi);
fetchMocker.enableMocks();
const originalObserver = window.ResizeObserver;
+const originalScrollIntoView = window.HTMLElement.prototype.scrollIntoView;
beforeAll(() => {
// disable act warnings
global.IS_REACT_ACT_ENVIRONMENT = false;
});
-// mock ResizeObserver for MainToolbar
beforeEach(() => {
+ // mock ResizeObserver for MainToolbar
window.ResizeObserver = vi.fn(() => ({
observe: vi.fn(),
unobserve: vi.fn(),
disconnect: vi.fn(),
}));
+
+ // mock scrollIntoView for FormikFormContent
+ window.HTMLElement.prototype.scrollIntoView = vi.fn();
});
afterEach(() => {
window.ResizeObserver = originalObserver;
+ window.HTMLElement.prototype.scrollIntoView = originalScrollIntoView;
});