Skip to content

Commit

Permalink
ErrorSummary: Add fallback for heading + better focus handling (#3140)
Browse files Browse the repository at this point in the history
Co-authored-by: Ken <[email protected]>
Co-authored-by: Ken <[email protected]>
  • Loading branch information
3 people authored Sep 12, 2024
1 parent 4e5e7dc commit 431d4de
Show file tree
Hide file tree
Showing 9 changed files with 129 additions and 51 deletions.
5 changes: 5 additions & 0 deletions .changeset/great-crews-hang.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@navikt/ds-react": major
---

ErrorSummary: Added fallback text for `heading`.
6 changes: 6 additions & 0 deletions .changeset/thick-roses-sneeze.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@navikt/ds-react": patch
"@navikt/ds-css": patch
---

ErrorSummary: Focus heading instead of container for improved experience with screen reader.
8 changes: 8 additions & 0 deletions @navikt/core/css/form/error-summary.css
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
padding: var(--a-spacing-3);
}

.navds-error-summary__heading {
scroll-margin-top: var(--a-spacing-6);
}

.navds-error-summary--small .navds-error-summary__heading {
scroll-margin-top: var(--a-spacing-4);
}

.navds-error-summary__heading:focus {
outline: none;
}
Expand Down
32 changes: 20 additions & 12 deletions @navikt/core/react/src/form/error-summary/ErrorSummary.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import cl from "clsx";
import React, { HTMLAttributes, forwardRef, isValidElement } from "react";
import React, { HTMLAttributes, forwardRef, useRef } from "react";
import { BodyShort, Heading } from "../../typography";
import { useId } from "../../util/hooks";
import ErrorSummaryItem, { ErrorSummaryItemType } from "./ErrorSummaryItem";
import { useId, useMergeRefs } from "../../util/hooks";
import ErrorSummaryItem from "./ErrorSummaryItem";

export interface ErrorSummaryProps extends HTMLAttributes<HTMLDivElement> {
/**
Expand All @@ -16,6 +16,7 @@ export interface ErrorSummaryProps extends HTMLAttributes<HTMLDivElement> {
size?: "medium" | "small";
/**
* Heading above links.
* @default "Du må rette disse feilene før du kan fortsette:"
*/
heading?: React.ReactNode;
/**
Expand All @@ -41,7 +42,7 @@ interface ErrorSummaryComponent
* </ErrorSummary.Item>
* ```
*/
Item: ErrorSummaryItemType;
Item: typeof ErrorSummaryItem;
}

/**
Expand Down Expand Up @@ -69,16 +70,21 @@ export const ErrorSummary = forwardRef<HTMLDivElement, ErrorSummaryProps>(
className,
size = "medium",
headingTag = "h2",
heading,
heading = "Du må rette disse feilene før du kan fortsette:",
...rest
},
ref,
) => {
const headingId = useId();

const sectionRef = useRef<HTMLDivElement>(null);
const headingRef = useRef<HTMLHeadingElement>(null);

const mergedRef = useMergeRefs(ref, sectionRef);

return (
<section
ref={ref}
ref={mergedRef}
{...rest}
className={cl(
className,
Expand All @@ -89,22 +95,24 @@ export const ErrorSummary = forwardRef<HTMLDivElement, ErrorSummaryProps>(
aria-live="polite"
aria-relevant="all"
aria-labelledby={headingId}
onFocus={(event) => {
if (event.target === sectionRef.current) {
headingRef?.current?.focus();
}
}}
>
<Heading
className="navds-error-summary__heading"
as={headingTag}
size="small"
id={headingId}
ref={headingRef}
tabIndex={-1}
>
{heading}
</Heading>
<BodyShort as="ul" size={size} className="navds-error-summary__list">
{React.Children.map(children, (child) => {
if (!isValidElement(child)) {
return null;
}
return <li key={child.toString()}>{child}</li>;
})}
{children}
</BodyShort>
</section>
);
Expand Down
18 changes: 10 additions & 8 deletions @navikt/core/react/src/form/error-summary/ErrorSummaryItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,23 @@ export interface ErrorSummaryItemProps
href?: string;
}

export type ErrorSummaryItemType = OverridableComponent<
type ErrorSummaryItemType = OverridableComponent<
ErrorSummaryItemProps,
HTMLAnchorElement
>;

export const ErrorSummaryItem: ErrorSummaryItemType = forwardRef(
({ children, as: Component = "a", className, ...rest }, ref) => {
return (
<Component
{...rest}
ref={ref}
className={cl(className, "navds-error-summary__item", "navds-link")}
>
{children}
</Component>
<li>
<Component
{...rest}
ref={ref}
className={cl(className, "navds-error-summary__item", "navds-link")}
>
{children}
</Component>
</li>
);
},
);
Expand Down
97 changes: 76 additions & 21 deletions @navikt/core/react/src/form/error-summary/error-summary.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,12 @@
import { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { expect, userEvent, within } from "@storybook/test";
import React, { useRef } from "react";
import { VStack } from "../../layout/stack";
import { ErrorSummary } from "./ErrorSummary";

export default {
title: "ds-react/Errorsummary",
title: "ds-react/ErrorSummary",
component: ErrorSummary,
argTypes: {
headingTag: {
control: {
type: "text",
},
},
size: {
control: {
type: "radio",
},
options: ["medium", "small"],
},
},
parameters: {
chromatic: { disable: true },
},
Expand All @@ -27,18 +15,32 @@ export default {
type Story = StoryObj<typeof ErrorSummary>;

export const Default: Story = {
render: (props) => (
<ErrorSummary
heading="Feiloppsummering komponent"
headingTag={props.headingTag || "h2"}
size={props.size ?? undefined}
>
render: ({ headingTag, ...rest }) => (
<ErrorSummary headingTag={headingTag || undefined} {...rest}>
<ErrorSummary.Item href="#1">Checkbox må fylles ut</ErrorSummary.Item>
<ErrorSummary.Item href="#2">
Tekstfeltet må ha en godkjent e-mail
</ErrorSummary.Item>
</ErrorSummary>
),
argTypes: {
heading: {
control: {
type: "text",
},
},
headingTag: {
control: {
type: "text",
},
},
size: {
control: {
type: "radio",
},
options: ["medium", "small"],
},
},
};

export const Small: Story = {
Expand All @@ -52,6 +54,59 @@ export const Small: Story = {
),
};

export const A11yDemo: Story = {
name: "A11y Demo",
render: () => {
const ref = useRef<HTMLHeadingElement>(null);
return (
<div>
<button
onClick={() => {
ref.current?.focus();
}}
>
Fokuser ErrorSummary
</button>
<ErrorSummary heading="Feiloppsummering tittel" ref={ref}>
<ErrorSummary.Item href="#1">Checkbox må fylles ut</ErrorSummary.Item>
<ErrorSummary.Item href="#2">
Tekstfeltet må ha en godkjent e-mail
</ErrorSummary.Item>
</ErrorSummary>
</div>
);
},
};

export const FocusDemo: Story = {
render: () => {
const ref = useRef<HTMLHeadingElement>(null);
return (
<div>
<button onClick={() => ref.current?.focus()}>Focus summary</button>
<ErrorSummary heading="Feiloppsummering tittel" ref={ref}>
<ErrorSummary.Item href="#1">Checkbox må fylles ut</ErrorSummary.Item>
<ErrorSummary.Item href="#2">
Tekstfeltet må ha en godkjent e-mail
</ErrorSummary.Item>
</ErrorSummary>
</div>
);
},
play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);

const button = canvas.getByText("Focus summary");
const heading = canvas.getByText("Feiloppsummering tittel");

await step("click button", async () => {
await userEvent.click(button);
});

expect(heading).toHaveFocus();
},
};

export const Chromatic: Story = {
render: () => (
<VStack gap="4">
Expand Down
2 changes: 1 addition & 1 deletion aksel.nav.no/website/pages/eksempler/errorsummary/demo.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { withDsExample } from "@/web/examples/withDsExample";

const Example = () => {
return (
<ErrorSummary heading="Du må rette disse feilene før du kan sende inn søknaden:">
<ErrorSummary>
<ErrorSummary.Item href="#1">
Felt må fylles ut med alder
</ErrorSummary.Item>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,7 @@ const Example = () => {
return (
<VStack gap="12" align="start">
{hasError && (
<ErrorSummary
ref={errorRef}
heading="Du må rette disse feilene før du kan sende inn søknaden:"
>
<ErrorSummary ref={errorRef}>
<ErrorSummary.Item href="#1">
Felt må fylles ut med alder
</ErrorSummary.Item>
Expand All @@ -40,5 +37,5 @@ export const Demo = {

export const args = {
index: 3,
desc: "Sett fokus på ErrorSummary ved submit. Hvis du gjør en ny sidelasting kan du også sette en ID på ErrorSummary og referere til den i URLens hash.",
desc: "Sett fokus på ErrorSummary ved submit. Hvis du gjør en ny sidelasting kan du også sette en `id` og referere til den i URLens hash.",
};
5 changes: 1 addition & 4 deletions aksel.nav.no/website/pages/eksempler/errorsummary/small.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { withDsExample } from "@/web/examples/withDsExample";

const Example = () => {
return (
<ErrorSummary
heading="Du må rette disse feilene før du kan sende inn søknaden:"
size="small"
>
<ErrorSummary size="small">
<ErrorSummary.Item href="#1">
Felt må fylles ut med alder
</ErrorSummary.Item>
Expand Down

0 comments on commit 431d4de

Please sign in to comment.