From f3abcdaeffd3d4196559cd71c7b7ab70cd726d69 Mon Sep 17 00:00:00 2001 From: Eirik Backer Date: Thu, 12 Sep 2024 21:30:17 +0200 Subject: [PATCH] fix(List): css principles (#2348) - Using principles from #2295 - Removing `asChild` on ListOrdered and ListUnordered as using other tags will break HTML validity and remove built in list-type styling - Removing the wrapping `div` caused by `List.Root` so root truly becomes a pure context provider and simplify DOM - Replacing `List.Root`, `List.Heading`, `List.Unordered`, `List.Ordered` with `` for more HTML-aligned API and less verbose DOM - Automatic connect heading with list if provided - not a must, but a nice a11y enhancement - Fix a11y for VoiceOver when `list-style: none` --------- Co-authored-by: Tobias Barsnes Co-authored-by: Michael Marszalek --- .changeset/happy-hounds-tie.md | 6 + .../tilgjengelighet/kontrast/page.mdx | 142 +++++----- apps/storefront/mdx-components.tsx | 21 +- apps/storybook/.storybook/preview.tsx | 32 +-- packages/css/list.css | 36 ++- .../ErrorSummary/ErrorSummaryHeading.tsx | 48 ++-- .../ErrorSummary/ErrorSummaryItem.tsx | 22 +- .../ErrorSummary/ErrorSummaryList.tsx | 18 +- .../ErrorSummary/ErrorSummaryRoot.tsx | 25 +- .../src/components/ErrorSummary/index.ts | 2 +- packages/react/src/components/List/List.mdx | 89 +++--- .../src/components/List/List.stories.tsx | 256 +++++++++++------- .../react/src/components/List/List.test.tsx | 72 +---- .../react/src/components/List/ListHeading.tsx | 45 --- .../react/src/components/List/ListItem.tsx | 13 +- .../react/src/components/List/ListRoot.tsx | 44 --- packages/react/src/components/List/Lists.tsx | 93 +++---- packages/react/src/components/List/index.ts | 38 +-- 18 files changed, 456 insertions(+), 546 deletions(-) create mode 100644 .changeset/happy-hounds-tie.md delete mode 100644 packages/react/src/components/List/ListHeading.tsx delete mode 100644 packages/react/src/components/List/ListRoot.tsx diff --git a/.changeset/happy-hounds-tie.md b/.changeset/happy-hounds-tie.md new file mode 100644 index 0000000000..1e64a38c85 --- /dev/null +++ b/.changeset/happy-hounds-tie.md @@ -0,0 +1,6 @@ +--- +"@digdir/designsystemet-css": patch +"@digdir/designsystemet-react": patch +--- + +List: Remove `List.Root` and `List.Heading`, which changes API diff --git a/apps/storefront/app/god-praksis/tilgjengelighet/kontrast/page.mdx b/apps/storefront/app/god-praksis/tilgjengelighet/kontrast/page.mdx index 4c5fa5f664..451472ef09 100644 --- a/apps/storefront/app/god-praksis/tilgjengelighet/kontrast/page.mdx +++ b/apps/storefront/app/god-praksis/tilgjengelighet/kontrast/page.mdx @@ -1,15 +1,13 @@ import { Card, CardContent, + Heading, List, - ListRoot, ListHeading, ListUnordered, ListItem, } from '@digdir/designsystemet-react'; -{/* The importing should allow likeList.Root, but it throw a react error */} - import { Image } from '@components'; import { PageLayout } from '@layouts'; import { Contributors } from '@blog'; @@ -40,29 +38,27 @@ Alle brukerne, også de med svekket syn, skal kunne se innholdet i digitale tjen - - - Gjeldende regelverk, WCAG 2.1 - - - - **1.4.3 Kontrast (minimum) (Nivå AA)**: Kontrastforholdet mellom - teksten og bakgrunnen er minst 4,5:1. [1.4.3 Kontrast (minimum), WCAG - 2.1 - (w3.org)](https://www.w3.org/Translations/WCAG21-no/#contrast-minimum) - - - **1.4.11 Kontrast for ikke-tekstlig innhold (Nivå AA)**: Den visuelle - presentasjonen av det følgende har et kontrastforhold på minst 3:1 mot - farge(r) som ligger ved siden av. [1.4.11 Kontrast for ikke-tekstlig - innhold, WCAG 2.1 - (w3.org)](https://www.w3.org/Translations/WCAG21-no/#non-text-contrast) - - - + + Gjeldende regelverk, WCAG 2.1 + + + + **1.4.3 Kontrast (minimum) (Nivå AA)**: Kontrastforholdet mellom + teksten og bakgrunnen er minst 4,5:1. [1.4.3 Kontrast (minimum), WCAG + 2.1 + (w3.org)](https://www.w3.org/Translations/WCAG21-no/#contrast-minimum) + + + **1.4.11 Kontrast for ikke-tekstlig innhold (Nivå AA)**: Den visuelle + presentasjonen av det følgende har et kontrastforhold på minst 3:1 mot + farge(r) som ligger ved siden av. [1.4.11 Kontrast for ikke-tekstlig + innhold, WCAG 2.1 + (w3.org)](https://www.w3.org/Translations/WCAG21-no/#non-text-contrast) + + @@ -70,36 +66,34 @@ Alle brukerne, også de med svekket syn, skal kunne se innholdet i digitale tjen - - - Fremtidig eller strengere: - - - - **1.4.6 Kontrast** (forbedret) (Nivå AAA): Den visuelle presentasjonen - av tekst og bilder av tekst har et kontrastforhold på minst 7:1, - unntatt uvesentlig tekst og skriftstørrelser større enn 18px eller - 14px fet. [ 1.4.6 Kontrast (forbedret), WCAG 2.1 - (w3.org)](https://www.w3.org/Translations/WCAG21-no/#contrast-enhanced) - - - **WCAG 2.2: 2.4.13** Focus Appearance (Nivå AAA), om utseende til - fokusmarkering krever at fokusindikator har en kontrastverdi på 3:1 - mellom samme piksler i fokusert og ikke-fokusert tilstand. - [Understanding Success Criterion 2.4.13: Focus Appearance | WAI | - W3C](https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance.html) - - - **WCAG 3** har et krav om farge og kontrast, visuell kontrast i tekst - (sølv): Sørg for tilstrekkelig kontrast mellom tekst i forgrunnen og - bakgrunnen for teksten. Her brukes det en ny metode, med navn APCA, - for å regne ut kontrasten. - - - + + Fremtidig eller strengere: + + + + **1.4.6 Kontrast** (forbedret) (Nivå AAA): Den visuelle presentasjonen + av tekst og bilder av tekst har et kontrastforhold på minst 7:1, + unntatt uvesentlig tekst og skriftstørrelser større enn 18px eller + 14px fet. [ 1.4.6 Kontrast (forbedret), WCAG 2.1 + (w3.org)](https://www.w3.org/Translations/WCAG21-no/#contrast-enhanced) + + + **WCAG 2.2: 2.4.13** Focus Appearance (Nivå AAA), om utseende til + fokusmarkering krever at fokusindikator har en kontrastverdi på 3:1 + mellom samme piksler i fokusert og ikke-fokusert tilstand. + [Understanding Success Criterion 2.4.13: Focus Appearance | WAI | + W3C](https://www.w3.org/WAI/WCAG22/Understanding/focus-appearance.html) + + + **WCAG 3** har et krav om farge og kontrast, visuell kontrast i tekst + (sølv): Sørg for tilstrekkelig kontrast mellom tekst i forgrunnen og + bakgrunnen for teksten. Her brukes det en ny metode, med navn APCA, + for å regne ut kontrasten. + + @@ -107,25 +101,23 @@ Alle brukerne, også de med svekket syn, skal kunne se innholdet i digitale tjen WCAG 3.0 foreslår nå en mer presis metode enn dagens standard, for å kalkulere kontrast og sette terskelverdier. - - - - Metoden forbedrer hvordan verdien mellom to farger bestemmes, og skiller - også på om fargene er i forgrunnen eller i bakgrunnen. - - - Den setter også tydelige terskelverdier eller målverdier for valg av font, - tekststørrelse og font-vekt. Metoden heter Advanced Perceptual Contrast - Algorithm (APCA). - - - Målet vårt er å ligge over AAA-krav i WCAG 2.1, og vi vil dermed ligge - nærmere terskelverdiene i APCA. Det øker sjansen for at vi klarer å - oppfylle kravet om at alle, også svaksynte, skal kunne se innholdet på - nettstedet. - - - + + + Metoden forbedrer hvordan verdien mellom to farger bestemmes, og skiller + også på om fargene er i forgrunnen eller i bakgrunnen. + + + Den setter også tydelige terskelverdier eller målverdier for valg av font, + tekststørrelse og font-vekt. Metoden heter Advanced Perceptual Contrast + Algorithm (APCA). + + + Målet vårt er å ligge over AAA-krav i WCAG 2.1, og vi vil dermed ligge + nærmere terskelverdiene i APCA. Det øker sjansen for at vi klarer å + oppfylle kravet om at alle, også svaksynte, skal kunne se innholdet på + nettstedet. + + ## I dag bruker vi en høyere standard enn kravene til tilgjengelighet diff --git a/apps/storefront/mdx-components.tsx b/apps/storefront/mdx-components.tsx index 17c8eada29..baedd6482f 100644 --- a/apps/storefront/mdx-components.tsx +++ b/apps/storefront/mdx-components.tsx @@ -11,7 +11,6 @@ import { Link, ListItem, ListOrdered, - ListRoot, ListUnordered, Paragraph, Table, @@ -26,22 +25,10 @@ import type { MDXComponents } from 'mdx/types'; export function useMDXComponents(components: MDXComponents): MDXComponents { return { ...components, - p: (props: ParagraphProps) => { - return ; - }, - a: (props) => { - return ; - }, - ol: (props: ListOrderedProps) => ( - - - - ), - ul: (props: ListUnorderedProps) => ( - - - - ), + p: (props: ParagraphProps) => , + a: (props) => , + ol: (props: ListOrderedProps) => , + ul: (props: ListUnorderedProps) => , li: (props: ListItemProps) => , h1: (props: HeadingProps) => ( diff --git a/apps/storybook/.storybook/preview.tsx b/apps/storybook/.storybook/preview.tsx index 87bd44aab9..8747bdb18c 100644 --- a/apps/storybook/.storybook/preview.tsx +++ b/apps/storybook/.storybook/preview.tsx @@ -49,24 +49,20 @@ const components = { /> ), ol: (props: Props) => ( - - - + ), ul: (props: Props) => ( - - - + ), li: (props: Props) => ( + /> ), a: (props: LinkProps) => { // if link starts with /, add current path to link @@ -86,7 +82,7 @@ const components = { href={href} className='sb-unstyled' data-ds-color-mode='light' - > + /> ); }, table: (props: Props) => ( diff --git a/packages/css/list.css b/packages/css/list.css index 3c886ff1d3..89986e373d 100644 --- a/packages/css/list.css +++ b/packages/css/list.css @@ -1,22 +1,32 @@ .ds-list { + --dsc-list-font-size: var(--ds-font-size-5); --dsc-list-padding-left: var(--ds-spacing-6); + --dsc-list-spacing: var(--ds-spacing-3); + --dsc-list-spacing-nested: var(--ds-spacing-2); + font-size: var(--dsc-list-font-size); /* Replace with composes paragraph-md */ + line-height: var(--ds-line-height-md); /* Replace with composes paragraph-md */ + margin: 0; padding-left: var(--dsc-list-padding-left); -} -.ds-list--sm { - --dsc-list-padding-left: var(--ds-spacing-4); -} + & > li + li { margin-top: var(--dsc-list-spacing) } + & > li > :is(ol, ul) { --dsc-list-spacing: var(--dsc-list-spacing-nested) } /* Shrink spacing a bit when nested */ -.ds-list--md, -.ds-list--lg { - --dsc-list-padding-left: var(--ds-spacing-6); -} + /* Add zero-width space to fix VoiceOver: https://gerardkcohen.me/writing/2017/voiceover-list-style-type.html + * This can also be acheived by using role="list" + role="listitem", but is nice to solve with CSS avoiding cluttered HTML + */ + & > li::before { + content: "\200B"; + position: absolute; + } -.ds-list__item { - margin-bottom: var(--ds-spacing-2); -} + &[data-size="sm"] { + --dsc-list-padding-left: var(--ds-spacing-4); + --dsc-list-font-size: var(--ds-font-size-4); /* Replace with composes paragraph-sm */ + } -.ds-list__item > .ds-list { - margin-top: var(--ds-spacing-2); + &[data-size="lg"] { + --dsc-list-font-size: var(--ds-font-size-6); /* Replace with composes paragraph-sm */ + --dsc-list-spacing: var(--ds-spacing-4); + } } diff --git a/packages/react/src/components/ErrorSummary/ErrorSummaryHeading.tsx b/packages/react/src/components/ErrorSummary/ErrorSummaryHeading.tsx index b3588cdf5b..03c843193a 100644 --- a/packages/react/src/components/ErrorSummary/ErrorSummaryHeading.tsx +++ b/packages/react/src/components/ErrorSummary/ErrorSummaryHeading.tsx @@ -1,31 +1,45 @@ -import { useContext, useEffect } from 'react'; +import { forwardRef, useContext, useEffect } from 'react'; -import type { ListHeadingProps } from '../List'; -import { List } from '../List'; +import { Heading, type HeadingProps } from '../Typography/Heading'; -import { ErrorSummaryContext } from './ErrorSummaryRoot'; +import { + ErrorSummaryContext, + type ErrorSummaryProps, +} from './ErrorSummaryRoot'; -export type ErrorSummaryHeadingProps = ListHeadingProps; +export type ErrorSummaryHeadingProps = HeadingProps; -export const ErrorSummaryHeading = ({ - id, - ...rest -}: ErrorSummaryHeadingProps) => { - const { headingId, setHeadingId } = useContext(ErrorSummaryContext); +const HEADING_SIZE_MAP: { + [key in NonNullable]: HeadingProps['size']; +} = { + sm: '2xs', + md: 'xs', + lg: 'sm', +} as const; + +export const ErrorSummaryHeading = forwardRef< + HTMLHeadingElement, + ErrorSummaryHeadingProps +>(function ErrorSummaryHeading( + { className, id, ...rest }: ErrorSummaryHeadingProps, + ref, +) { + const { size, headingId, setHeadingId } = useContext(ErrorSummaryContext); useEffect(() => { - if (id && headingId !== id) { - setHeadingId(id); - } + if (id && headingId !== id) setHeadingId(id); }, [headingId, id, setHeadingId]); return ( - ); -}; +}); ErrorSummaryHeading.displayName = 'ErrorSummaryHeading'; diff --git a/packages/react/src/components/ErrorSummary/ErrorSummaryItem.tsx b/packages/react/src/components/ErrorSummary/ErrorSummaryItem.tsx index 6acacaa5b7..c1af1940e5 100644 --- a/packages/react/src/components/ErrorSummary/ErrorSummaryItem.tsx +++ b/packages/react/src/components/ErrorSummary/ErrorSummaryItem.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'react'; import type { LinkProps } from '../Link'; import { Link } from '../Link'; import type { ListItemProps } from '../List'; @@ -22,21 +23,20 @@ type OptionalHref = { }; export type ErrorSummaryItemProps = (RequiredHref | OptionalHref) & - Omit; + Omit; -export const ErrorSummaryItem = ({ - href, - asChild, - children, - ...rest -}: ErrorSummaryItemProps) => { +export const ErrorSummaryItem = forwardRef< + HTMLLIElement, + ErrorSummaryItemProps +>(function ErrorSummaryItem( + { href, asChild, children, ...rest }: ErrorSummaryItemProps, + ref, +) { return ( - + {children} ); -}; - -ErrorSummaryItem.displayName = 'ErrorSummaryItem'; +}); diff --git a/packages/react/src/components/ErrorSummary/ErrorSummaryList.tsx b/packages/react/src/components/ErrorSummary/ErrorSummaryList.tsx index 59f10364f4..bb0dd36271 100644 --- a/packages/react/src/components/ErrorSummary/ErrorSummaryList.tsx +++ b/packages/react/src/components/ErrorSummary/ErrorSummaryList.tsx @@ -1,10 +1,14 @@ -import type { ComponentProps } from 'react'; -import { List } from '../List'; +import { forwardRef, useContext } from 'react'; +import { List, type ListUnorderedProps } from '../List'; -export type ErrorSummaryListProps = ComponentProps; +import { ErrorSummaryContext } from './ErrorSummaryRoot'; -export default function ErrorSummaryList({ ...rest }: ErrorSummaryListProps) { - return ; -} +export type ErrorSummaryListProps = ListUnorderedProps; -ErrorSummaryList.displayName = 'ErrorSummaryList'; +export const ErrorSummaryList = forwardRef< + HTMLOListElement, + ErrorSummaryListProps +>(function ErrorSummaryList({ ...rest }: ErrorSummaryListProps, ref) { + const { size } = useContext(ErrorSummaryContext); + return ; +}); diff --git a/packages/react/src/components/ErrorSummary/ErrorSummaryRoot.tsx b/packages/react/src/components/ErrorSummary/ErrorSummaryRoot.tsx index f93d0aef50..a5a1a568d7 100644 --- a/packages/react/src/components/ErrorSummary/ErrorSummaryRoot.tsx +++ b/packages/react/src/components/ErrorSummary/ErrorSummaryRoot.tsx @@ -1,32 +1,33 @@ -import { Slot } from '@radix-ui/react-slot'; -import type { HTMLAttributes } from 'react'; +import cl from 'clsx/lite'; import { createContext, forwardRef, useId, useState } from 'react'; +import type { HTMLAttributes } from 'react'; -import type { ListProps } from '../List'; -import { List } from '../List'; +import type { ListUnorderedProps } from '../List'; type ErrorSummaryContextType = { + size: ListUnorderedProps['size']; headingId?: string; setHeadingId: (id: string) => void; }; export const ErrorSummaryContext = createContext({ + size: 'md', headingId: 'heading', setHeadingId: () => {}, }); export type ErrorSummaryProps = { - size?: ListProps['size']; + size?: ListUnorderedProps['size']; } & HTMLAttributes; export const ErrorSummaryRoot = forwardRef( ( { - size, + className, + size = 'md', role = 'alert', 'aria-live': ariaLive = 'polite', 'aria-relevant': ariaRelevant = 'all', - children, ...rest }, ref, @@ -35,18 +36,16 @@ export const ErrorSummaryRoot = forwardRef( const [headingId, setHeadingId] = useState(randomId); return ( - - +
- {children} - + /> ); }, diff --git a/packages/react/src/components/ErrorSummary/index.ts b/packages/react/src/components/ErrorSummary/index.ts index 2a22db9d42..03ea6331a0 100644 --- a/packages/react/src/components/ErrorSummary/index.ts +++ b/packages/react/src/components/ErrorSummary/index.ts @@ -1,6 +1,6 @@ import { ErrorSummaryHeading } from './ErrorSummaryHeading'; import { ErrorSummaryItem } from './ErrorSummaryItem'; -import ErrorSummaryList from './ErrorSummaryList'; +import { ErrorSummaryList } from './ErrorSummaryList'; import { ErrorSummaryRoot } from './ErrorSummaryRoot'; type ErrorSummaryComponent = { diff --git a/packages/react/src/components/List/List.mdx b/packages/react/src/components/List/List.mdx index 059e0e1165..e03b81fbed 100644 --- a/packages/react/src/components/List/List.mdx +++ b/packages/react/src/components/List/List.mdx @@ -1,6 +1,5 @@ import { Meta, Primary, Controls, Canvas, ArgTypes } from '@storybook/blocks'; -import { ListHeading } from './ListHeading'; import * as ListStories from './List.stories'; @@ -16,17 +15,12 @@ Lister er strukturelementer, som gjør at vi kan utheve viktig informasjon for b Listen kan være både sortert og usortert, dette endrer du ved å bruke `List.Ordered` eller `List.Unordered`. -`List.Root` er påkrevd for å få riktig aria attributter (`aria-labelledby`) mellom listen og tittel. - ```tsx import { List } from '@digdir/designsystemet-react'; - - Heading - - Item - -; + + Item + ``` ## Eksempler @@ -47,14 +41,20 @@ En sortert liste brukes når det er viktig at brukerne forstår grader av viktig
-### Uten overskrift +### Med overskrift + +Liste kan brukes sammen med en `Heading` for å gi brukeren en rask oversikt over innholdet. Dette er spesielt nyttig for lengre lister. -Dersom du bruker `List.Heading` så legger vi automatisk på `aria-labelledby` på `List.Heading` og `List.Ordered/Unordered` -for å gi skjermlesere beskjed om at `List.Heading` er en overskrift for `List`. +Størrelse på overskrifter over lister er avhengig av bruksområde, og er derfor noe du må sette selv. +I løpende tekst anbefaler vi disse størrelsene: -Dersom du ikke legger til en overskrift kommer vi ikke til å legge til `aria-labelledby`. +| Størrelse på `List` | Størrelse på `Heading` | +| :----------------- | :---------------------- | +| sm | 2xs | +| md | xs | +| lg | sm | - + ### Lister i lister @@ -89,6 +89,7 @@ Passer ikke til å

+ ## Tekst i komponenten Alle elementer i en liste bør ha samme form og struktur. @@ -110,6 +111,30 @@ Når punktene er hele setninger, bruker vi ## Tilgjengelighet +### Lister til navigasjon + +Dersom liste brukes som navigasjonselement (for eksempel i bunn eller ved siden av hovedinnholdet), +kan du legge et `