Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Breadcrumbs): new component #2226

Merged
merged 16 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/storybook/.storybook/preview.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import '@digdir/designsystemet-css';
import '@digdir/designsystemet-css/index.css';
import '@digdir/designsystemet-theme/digdir.css';

import { withThemeByDataAttribute } from '@storybook/addon-themes';
Expand Down
72 changes: 72 additions & 0 deletions packages/css/breadcrumbs.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
.ds-breadcrumbs {
--dsc-breadcrumbs-spacing: var(--ds-spacing-2);
--dsc-breadcrumbs-chevron-size: var(--ds-sizing-6);
--dsc-breadcrumbs-link-color: inherit;
}

.ds-breadcrumbs--sm {
--dsc-breadcrumbs-spacing: var(--ds-spacing-1);
--dsc-breadcrumbs-chevron-size: var(--ds-sizing-5);
}

.ds-breadcrumbs--md {
--dsc-breadcrumbs-spacing: var(--ds-spacing-2);
--dsc-breadcrumbs-chevron-size: var(--ds-sizing-6);
}

.ds-breadcrumbs--lg {
--dsc-breadcrumbs-spacing: var(--ds-spacing-3);
--dsc-breadcrumbs-chevron-size: var(--ds-sizing-7);
}

.ds-breadcrumbs__list {
display: flex;
flex-wrap: wrap;
list-style-type: none;
margin: 0;
padding: 0;
gap: var(--dsc-breadcrumbs-spacing) 0;
}

.ds-breadcrumbs__item:where(:not(:last-child))::after,
.ds-breadcrumbs > .ds-breadcrumbs__link::before {
background: currentcolor;
content: '';
display: inline-block;
height: var(--dsc-breadcrumbs-chevron-size);
margin-inline: var(--dsc-breadcrumbs-spacing);
mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24'%3E%3Cpath d='M9.47 5.97a.75.75 0 0 1 1.06 0l5.5 5.5a.75.75 0 0 1 0 1.06l-5.5 5.5a.75.75 0 1 1-1.06-1.06L14.44 12 9.47 7.03a.75.75 0 0 1 0-1.06'/%3E%3C/svg%3E")
50% / contain no-repeat;
vertical-align: middle;
width: var(--dsc-breadcrumbs-chevron-size);
}

/* When link is direct child of Breadcrumbs, make it back button */
.ds-breadcrumbs > .ds-breadcrumbs__link::before {
margin: 0;
transform: rotate(180deg);
}

.ds-breadcrumbs__link {
--dsc-link-color: var(--dsc-breadcrumbs-link-color);
--dsc-link-color-visited: var(--dsc-breadcrumbs-link-color);
}

.ds-breadcrumbs__link[aria-current='page'] {
text-decoration: none;
}

.ds-breadcrumbs > .ds-breadcrumbs__link:where(:not(:only-child)) {
display: none;
}

@media (max-width: 650px) {
.ds-breadcrumbs > .ds-breadcrumbs__nav:where(:not(:only-child)) {
display: none;
}

.ds-breadcrumbs > .ds-breadcrumbs__link {
display: block;
width: fit-content;
}
}
eirikbacker marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions packages/css/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@
@import url('./spinner.css') layer(ds.components);
@import url('./table.css') layer(ds.components);
@import url('./combobox.css') layer(ds.components);
@import url('./breadcrumbs.css') layer(ds.components);
77 changes: 77 additions & 0 deletions packages/react/src/components/Breadcrumbs/Breadcrumbs.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { Meta, Controls, Primary, Canvas } from '@storybook/blocks';

import * as BreadcrumbsStories from './Breadcrumbs.stories';

<Meta of={BreadcrumbsStories} />

# Breadcrumbs

`Breadcrumb` er en navigasjon med synlig søkebane. Vi bruker denne komponenten til å hjelpe brukerne å forstå hvor de er i en struktur, for eksempel på en nettside. Da kan de lettere bytte mellom de ulike nivåene i strukturen.

<Primary />
<Controls of={BreadcrumbsStories.Preview} />

## Slik bruker du `Breadcrumb`

Den siste lenken i brødsmulestien blir automatisk markert med `aria-current="page"`

```tsx
<Breadcrumbs.Root>
<Breadcrumbs.List>
<Breadcrumbs.Item>
<Breadcrumbs.Link href="https://designsystemet.no/">Nivå 1</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href="https://designsystemet.no/niva-2/">Nivå 2</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href="https://designsystemet.no/niva-2/niva-3/">Nivå 3</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href="https://designsystemet.no/niva-2/niva-3/niva-4/">Nivå 4</Breadcrumbs.Link>
</Breadcrumbs.Item>
</Breadcrumbs.List>
</Breadcrumbs.Root>
```

### Varianter av `Breadcrumb`

Oppsummering av de ulike variantene, hvis nødvendig.

[Eksempler]

### Andre eksempler på denne komponenten

Gjelder hvordan komponenten kan brukes sammen med andre komponenter og stiler.

[Eksempel]
eventuelt "Gjør dette"- og "Ikke gjør dette"-eksempler.

## Retningslinjer for `breadcrumb`

En navigasjon med synlig søkebane vises vanligvis høyt oppe på nettsiden, gjerne rett under sidetittelen.

Det er først når det er tre eller flere nivåer at det er behov for en brødsmulesti.

Hvis vi bruker `breadcrumb` i applikasjoner, skal vi vise den på alle sider som ikke er forsiden. Alle elementene i brødsmulestien skal være lenker, men siden brukerne står på, skal se ut som vanlig tekst.

Hvis vi bare har to nivåer, er det bedre å bruke `Navlink`.

Vi bruker **ikke** brødsmulestier til lineær navigasjon, for eksempel for de ulike stegene i en arbeidsflyt et skjema.


## Tilgjengelighet

# ⚠️ NOE OM ARIA-LABEL HER

Det skal være enkelt for brukerne å benytte seg av brødsmulestien. Pass derfor på at det ikke ligger andre interaktive elementer for nær den.

`Breadcrumb` kan ha flere interaktive elementer som de som navigerer med tastaturet må komme seg forbi på en enkel måte. Pass derfor på at du legger inn at `[Skiplink](https://jokul.fremtind.no/universell-utforming/guide/#uu/hoppe-over-blokker)` til hovedinnholdet også skal hoppe over brødsmulestien.

### Skjule brødsmulestien på mobil

Hvis du har lang navigasjon, kan du sette opp komponenten til bare å vise første og siste del av søkebanen på mobil. (Noe om hvordan vi gjør det hos oss, med hvilken collapse-kode?)

### Brødsmulestier på mørk bakgrunn

Pass på at du inverterer til hvite lenker og piler på mørk bakgrunn, for eksempel i overskrifter, tilpassede komponenter og mønstre med mørk bakgrunn. Bakgrunnskontrasten må være minst 4.5:1 med hvit.
116 changes: 116 additions & 0 deletions packages/react/src/components/Breadcrumbs/Breadcrumbs.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import type { Meta, StoryFn } from '@storybook/react';

import { Breadcrumbs } from '.';

export default {
title: 'Komponenter/Breadcrumbs',
component: Breadcrumbs.Root,
argTypes: {
size: {
options: ['sm', 'md', 'lg'],
control: { type: 'radio' },
},
},
args: {
size: 'md',
},
eirikbacker marked this conversation as resolved.
Show resolved Hide resolved
} as Meta;

export const Preview: StoryFn<typeof Breadcrumbs.Root> = (args) => (
<Breadcrumbs.Root {...args}>
<Breadcrumbs.Link href='#' aria-label='Tilbake til Nivå 3'>
Nivå 3
</Breadcrumbs.Link>
<Breadcrumbs.Nav aria-label='Du er her:'>
<Breadcrumbs.List>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Nivå 1</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Nivå 2</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Nivå 3</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Nivå 4</Breadcrumbs.Link>
</Breadcrumbs.Item>
</Breadcrumbs.List>
</Breadcrumbs.Nav>
</Breadcrumbs.Root>
);

export const ListOnly: StoryFn<typeof Breadcrumbs.Root> = (args) => (
<Breadcrumbs.Root size='md'>
<Breadcrumbs.Nav aria-label='Du er her:'>
<Breadcrumbs.List>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Nivå 1</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Nivå 2</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Nivå 3</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Nivå 4</Breadcrumbs.Link>
</Breadcrumbs.Item>
</Breadcrumbs.List>
</Breadcrumbs.Nav>
</Breadcrumbs.Root>
);

export const BackOnly: StoryFn<typeof Breadcrumbs.Root> = (args) => (
<Breadcrumbs.Root size='md'>
<Breadcrumbs.Link href='#' aria-label='Tilbake til Nivå 3'>
Nivå 3
</Breadcrumbs.Link>
</Breadcrumbs.Root>
);

export const LongItems: StoryFn<typeof Breadcrumbs.Root> = (args) => (
<Breadcrumbs.Root {...args}>
<Breadcrumbs.Link
href='#'
aria-label='Tilbake til helsesertifikat for sjømat'
>
Slik søker du om helsesertifikat for sjømat
</Breadcrumbs.Link>
<Breadcrumbs.Nav>
<Breadcrumbs.List aria-label='Du er her:'>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Hjem</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>
Eksport til land utenfor EU/EØS
</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Eksport av mat og drikke</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>
Eksport av fisk og sjømat
</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>
Veiledning om helsesertifikat for sjømat
</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>
Slik søker du om helsesertifikat for sjømat
</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>
Slik søker du om helsesertifikat i ny eksportløsning
</Breadcrumbs.Link>
</Breadcrumbs.Item>
</Breadcrumbs.List>
</Breadcrumbs.Nav>
</Breadcrumbs.Root>
);
45 changes: 45 additions & 0 deletions packages/react/src/components/Breadcrumbs/Breadcrumbs.test.tsx
eirikbacker marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { render, screen } from '@testing-library/react';
import type { BreadcrumbsRootProps } from './BreadcrumbsRoot';

import { Breadcrumbs } from './';

const renderWithRoot = (props: BreadcrumbsRootProps) => {
render(
<Breadcrumbs.Root {...props}>
<Breadcrumbs.Link href='#' aria-label='Tilbake til Nivå 3'>
Nivå 3
</Breadcrumbs.Link>
<Breadcrumbs.Nav aria-label='Du er her:'>
<Breadcrumbs.List>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Nivå 1</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Nivå 2</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Nivå 3</Breadcrumbs.Link>
</Breadcrumbs.Item>
<Breadcrumbs.Item>
<Breadcrumbs.Link href='#'>Nivå 4</Breadcrumbs.Link>
</Breadcrumbs.Item>
</Breadcrumbs.List>
</Breadcrumbs.Nav>
</Breadcrumbs.Root>,
);
};

describe('Breadcrumbs.Root', () => {
it('should render correctly with default props', () => {
renderWithRoot({});

expect(screen.getByRole('navigation')).toBeInTheDocument();
});

it('should render correctly with custom props', () => {
renderWithRoot({
size: 'lg',
});
expect(screen.getByRole('navigation')).toBeInTheDocument();
});
});
27 changes: 27 additions & 0 deletions packages/react/src/components/Breadcrumbs/BreadcrumbsItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Slot } from '@radix-ui/react-slot';
import cl from 'clsx/lite';
import { type HTMLAttributes, forwardRef } from 'react';

export type BreadcrumbsItemProps = {
/**
* Change the default rendered element for the one passed as a child, merging their props and behavior.
* @default false
*/
asChild?: boolean;
} & Omit<HTMLAttributes<HTMLLIElement>, 'size'>;

export const BreadcrumbsItem = forwardRef<HTMLLIElement, BreadcrumbsItemProps>(
({ asChild, className, ...rest }, ref) => {
const Component = asChild ? Slot : 'li';

return (
<Component
ref={ref}
className={cl('ds-breadcrumbs__item', className)}
{...rest}
/>
);
},
);

BreadcrumbsItem.displayName = 'BreadcrumbsItem';
29 changes: 29 additions & 0 deletions packages/react/src/components/Breadcrumbs/BreadcrumbsLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { Slot } from '@radix-ui/react-slot';
import cl from 'clsx/lite';
import { forwardRef } from 'react';

import { Link } from '../Link';
import type { LinkProps } from '../Link';

export type BreadcrumbsLinkProps = LinkProps & {
mobileLabel?: string;
};

export const BreadcrumbsLink = forwardRef<
HTMLAnchorElement,
BreadcrumbsLinkProps
>(({ asChild, className, mobileLabel, ...rest }, ref) => {
const Component = asChild ? Slot : 'a';

return (
<Link asChild>
eirikbacker marked this conversation as resolved.
Show resolved Hide resolved
<Component
className={cl(`ds-breadcrumbs__link`, className)}
ref={ref}
{...rest}
/>
</Link>
);
});
eirikbacker marked this conversation as resolved.
Show resolved Hide resolved

BreadcrumbsLink.displayName = 'BreadcrumbsLink';
Loading
Loading