Compare commits

...
Author SHA1 Message Date
github-actions[bot]andgithub-actions[bot] 085cc532ee Version Packages (#2839)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-11 12:21:05 +02:00
KenandHalvor Haugan deae4ecba5 Datepicker/MonthPicker required bugs (#2838)
* 🐛 Required-prop på monthpicker stoppet ikke de-select av valg

* 🧪 Tester required for datepicker

* 🔥 Fjernet console.logs

* 📝 Changeset

* Update .changeset/fifty-hairs-fix.md

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update @navikt/core/react/src/date/datepicker/datepicker.stories.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🔥 Disabled-attrb er ikke nødvendig i required demo

* 🐛 Satt defaultSelected-dato til statisk dato

* ♻️ Fjernet unødvendig required-sjekk

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-04-11 11:55:27 +02:00
KenandHalvor Haugan cd2adcb655 Erstattet Radix togglegroup med intern implementasjon (#2620)
* 🎉 Ny hook og API 'descendants'

* 📝 Bedre storiers

* 🎨 implementert togglegroup med descendants

* 🎨 Implemented togglegroup.item

* 🎨 Fjernet gammel togglegroup

* 🐛 Fikset keydown

* 🔥 Fjernet comment

* 🎉 RowingFocus resetter nå ved focus-exit

* 🔥 Fjernet radix toggleroup fra dependencies

* 📝 Changeset

* [V6] Deprecated Grid og ContentContainer (#2622)

* 🔥 Fjernet grid css

* 🔥 Fjernet grid tsx

* 🔥 Fjernet content-container fra systemet

* 🐛 la til deprecatePrefix-tag for stylelint

* 📝 Changeset

* Textarea: Fjern wrapper (#2635)

* [V6] Chat-props deprecation (#2631)

* 🎨 Satt opp codemod boilerplate for chat deprecation

* 🐛 v5 -> v6 i codemod

* 🧪 Tester for alias-import av chat

* 🧪 idempotent codemod-test

* 🔥 Fjernet deprecated props fra Chat

* 📝 Changeset

* 📝 Bedre advarsel i chat-codemod

* 📝 v6-migration update

* 🧪 Tester vanlig import av Chat

* 📝 Oppdatert tekster i codemod

* 🔥 Fjernet return fra removeProps

* Update @navikt/aksel/src/codemod/migrations.ts

* [V6] 1440px breakpoint (#2632)

* ✨ La til nytt brekkpunkt 1440px

* 🎉 Bleed støtter 2xl breakpoint

* 🎉 Bleed box 2xl breakpoint

* 🎉 hgrid 2xl breakpoint

* 🎉 resterende primitives støtter 1440px

* 📝 tailwind migreringsdocs

* 🔥 Fjernet navno-mal fra chromatic snapshot

* 📝 Changeset

* 🐛 2xl hgrid css brakk

* 📝 Oppdatert changset med tailwind-ref

* Update @navikt/core/react/src/layout/bleed/Bleed.tsx

* Update @navikt/core/react/src/layout/bleed/Bleed.tsx

* 📝 Mer konsekvent story for breakpoints i box

* 🐛 JSDOC for Bleed matcher nå syntax

* [V6] Fjernet z-index-modal token  (#2637)

* 🔥 Fjernet zindex-modal token

* 📝 Oppdatert docs

* 📝 Changeset

* [V6] ErrorSummary magic (#2642)

* ♻️ Flyttet li-tag fra ErrorSummary-parent til child

* 🏷️ Bedre typer

* 🔒 Heading er nå required i ErrorSummary

* 🎨 Egen prop for ErrorSummary fokus

* 📝 Changeset

* 🎨 Bedre fokushåndtering i ErrorSummary

* ♻️ focusTarget -> focusTargetRef

* 📝 Fikset jsdoc

* 📝 Migreringstekst

* 📝 changeset

* 📝 Migration-text

* 📝 JSDoc-staving

* 📝 Oppdatert migreringsguide for z-index + tailwind

* 🐛 Fikset bruk av z-modal i tailwind

* 🔥 Fjernet descendants-filer

* 📝 Rename context og types til namespace

* 🔥 Fjernet openOnFocus-prop fra date

* [V6] Modal: strengere prop-typer (#2718)

* 🎨 SortState i Table har nå none tilgjengelig (#2726)

* [V6] Oppdatert bruk av action-farger (#2717)

* 💄 Button secondary hover

* 💄 Radio/Checkbox action-farger

* :WIP: combobox action-farger

* 🐛 Tilpasset hover-farge på checkbox i confirmationpanel

* 💄 copybutton hover-text action

* 💄 dropdown, helptext hover-text

* 💄 link og linkpanel actionsfarger

* 💄 Search action-farger

* 💄 select action-farger

* 💄 Stepper action

* 💄 Textarea action

* 💄 Textarea og textfield colors

* 🐛 Riktig hover-bakgrunn på combobox selected option

* 📝 Changeset

* 📝 changeset tokens

* 📝 Dokumentert nye tokens

* [V6] Native ESM-støtte 🎉  (#2677)

* 🎨 Fungerende cjs/esm bundle med use-client

* 🎨 Oppdatert dir-output i for esm

* ⬆️ Bump date-fns og react-day-picker

* 🎨 oppdatert date-fns imports

* 🐛 Endret bruk av reat-day-picker i monthpicker

* 🐛 Fikset rendring av button i Datepicker-row

* ♻️ Bruker date-fns imports ssom støtter v2 og v3

* ⬆️ Synced date-fns versions

* ✨ Astro testmiljø

* 📝 Changeset

* 🎨 Skriver packagejson med type for esm

* 🔥 Fjernet .yarn/* fra astro

* 🎨 La til .yarn i gitignore for astro

* ⏪ Bruker riktig ds-react versjon

* ⬆️ Lik tsconfig som ds-react i aksel-icons

* 🐛 La tilbake provernance

* 🐛 Undo Day/Row fix fra main

* 🔥 Fjernet mulighet til å laste date-fns v2

* 🔥 Fjernet fs-extra dependency

* 🐛 Fikset feil versjon på core/react

* [V6] React-pakker støtter nå native SSR ved å bruke `use-client` 🎉  (#2707)

* 🎉 use-client blir lagt til på export av ds-react og aksel-icons

* 🎉 La til nextjs med appdir for testing

* ⚡ Tom yarn.lock med i git

* 📝 changeset

* 📝 Oppdatert migration-guide

* Update @navikt/aksel-icons/config/index-template.js

* 🎉 ToggleGroup-prop fill

* 🐛 Async-lasting av date-fns er nå v3 esm compliant

v3 av date-fns støtter ikke default export/import, noe som brakk async-lasting da vi brukte .default

* 🧪 Forbedret ToggleGroup-tester

* ⬆️ yarn sync

* 🐛 Fikset import av parts

* 🔥 Fjernet duplikate changesets

* ⬇️ Reversert duplikate endringer som kom fra v6

* 📝 rowing -> roving

* 📝 Bedre test-navn

* 📝 Bedre navngivning for tester

* ⚡ Fjernet useEffect for state update

* 📝 Oppdatert changeset og jsdoc

* ♻️ Inlinet aria-desribedby

* 📝 Demo for fill-prop på aksel.nav.no

* 🐛 Togglegroup krever onChange-prop

* 📝 ToggleGroup story for chromatic

* ⬆️ yarn lock

* 🎉 Bedre visning av eksempel på aksel.nav.no

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-04-11 11:29:33 +02:00
Ken 0d4385a545 🐛 Fill-eksempel på Tabs viste ikke funksjon riktig 2024-04-11 11:25:54 +02:00
Ken 6f1b9751f3 🛂 La til feedback på nye gp-sider (#2840) 2024-04-11 07:21:44 +02:00
Ken 7fb97808f0 🚚 Flyttet data-aksel-template attr dypere i dom-treet (#2841) 2024-04-11 07:21:29 +02:00
KenandHalvor Haugan d8ac958de2 Erstattet Radix tabs med intern implementasjon (#2621)
* 🎉 Ny hook og API 'descendants'

* 📝 Bedre storiers

* 🎉 Setup for ny tabs

* 🎨 Setup context

* 🎨 useTabs-hook

* 🎨 Tablist hook

* 🚚 Flyttet tablist i egen mappe

* 🎨 TabList

* 🐛 Fikset import av tabList

* 🎨 Tabs.Tab implementert

* 🎨 Tabs.TabPanel implementert

* 🐛 Fikset import av tabpanel

* 🐛 onOnClick -> onClick

* 🐛 Fikset keydown-håndtering i tablist

* 🐛 Fikset plassering av scrollbuttons i overflow

* ♿ Scrollbutton er nå

* ✨ Fitted variant

* 🐛 Fikset imports

* 🔥 Fjernet radix-tabs import

* 🎨 Panel-children rendres bare hvis selected

* 🎨 lazy-prop for å bare rendre children i panel hvis selected

* 🎨 Tabs.Tab håndterer nå tab-key og tabindex riktig

* 📝 Changeset

* 📝 Changeset

* [V6] Deprecated Grid og ContentContainer (#2622)

* 🔥 Fjernet grid css

* 🔥 Fjernet grid tsx

* 🔥 Fjernet content-container fra systemet

* 🐛 la til deprecatePrefix-tag for stylelint

* 📝 Changeset

* Textarea: Fjern wrapper (#2635)

* [V6] Chat-props deprecation (#2631)

* 🎨 Satt opp codemod boilerplate for chat deprecation

* 🐛 v5 -> v6 i codemod

* 🧪 Tester for alias-import av chat

* 🧪 idempotent codemod-test

* 🔥 Fjernet deprecated props fra Chat

* 📝 Changeset

* 📝 Bedre advarsel i chat-codemod

* 📝 v6-migration update

* 🧪 Tester vanlig import av Chat

* 📝 Oppdatert tekster i codemod

* 🔥 Fjernet return fra removeProps

* Update @navikt/aksel/src/codemod/migrations.ts

* [V6] 1440px breakpoint (#2632)

* ✨ La til nytt brekkpunkt 1440px

* 🎉 Bleed støtter 2xl breakpoint

* 🎉 Bleed box 2xl breakpoint

* 🎉 hgrid 2xl breakpoint

* 🎉 resterende primitives støtter 1440px

* 📝 tailwind migreringsdocs

* 🔥 Fjernet navno-mal fra chromatic snapshot

* 📝 Changeset

* 🐛 2xl hgrid css brakk

* 📝 Oppdatert changset med tailwind-ref

* Update @navikt/core/react/src/layout/bleed/Bleed.tsx

* Update @navikt/core/react/src/layout/bleed/Bleed.tsx

* 📝 Mer konsekvent story for breakpoints i box

* 🐛 JSDOC for Bleed matcher nå syntax

* [V6] Fjernet z-index-modal token  (#2637)

* 🔥 Fjernet zindex-modal token

* 📝 Oppdatert docs

* 📝 Changeset

* [V6] ErrorSummary magic (#2642)

* ♻️ Flyttet li-tag fra ErrorSummary-parent til child

* 🏷️ Bedre typer

* 🔒 Heading er nå required i ErrorSummary

* 🎨 Egen prop for ErrorSummary fokus

* 📝 Changeset

* 🎨 Bedre fokushåndtering i ErrorSummary

* ♻️ focusTarget -> focusTargetRef

* 📝 Fikset jsdoc

* 📝 Migreringstekst

* 📝 changeset

* 📝 Migration-text

* 📝 JSDoc-staving

* 📝 Oppdatert migreringsguide for z-index + tailwind

* 🐛 Fikset bruk av z-modal i tailwind

* 🔥 Fjernet descendants-filer

* 📝 Rename context og types til namespace

* 🔥 Fjernet openOnFocus-prop fra date

* [V6] Modal: strengere prop-typer (#2718)

* 🎨 SortState i Table har nå none tilgjengelig (#2726)

* [V6] Oppdatert bruk av action-farger (#2717)

* 💄 Button secondary hover

* 💄 Radio/Checkbox action-farger

* :WIP: combobox action-farger

* 🐛 Tilpasset hover-farge på checkbox i confirmationpanel

* 💄 copybutton hover-text action

* 💄 dropdown, helptext hover-text

* 💄 link og linkpanel actionsfarger

* 💄 Search action-farger

* 💄 select action-farger

* 💄 Stepper action

* 💄 Textarea action

* 💄 Textarea og textfield colors

* 🐛 Riktig hover-bakgrunn på combobox selected option

* 📝 Changeset

* 📝 changeset tokens

* 📝 Dokumentert nye tokens

* [V6] Native ESM-støtte 🎉  (#2677)

* 🎨 Fungerende cjs/esm bundle med use-client

* 🎨 Oppdatert dir-output i for esm

* ⬆️ Bump date-fns og react-day-picker

* 🎨 oppdatert date-fns imports

* 🐛 Endret bruk av reat-day-picker i monthpicker

* 🐛 Fikset rendring av button i Datepicker-row

* ♻️ Bruker date-fns imports ssom støtter v2 og v3

* ⬆️ Synced date-fns versions

* ✨ Astro testmiljø

* 📝 Changeset

* 🎨 Skriver packagejson med type for esm

* 🔥 Fjernet .yarn/* fra astro

* 🎨 La til .yarn i gitignore for astro

* ⏪ Bruker riktig ds-react versjon

* ⬆️ Lik tsconfig som ds-react i aksel-icons

* 🐛 La tilbake provernance

* 🐛 Undo Day/Row fix fra main

* 🔥 Fjernet mulighet til å laste date-fns v2

* 🔥 Fjernet fs-extra dependency

* 🐛 Fikset feil versjon på core/react

* [V6] React-pakker støtter nå native SSR ved å bruke `use-client` 🎉  (#2707)

* 🎉 use-client blir lagt til på export av ds-react og aksel-icons

* 🎉 La til nextjs med appdir for testing

* ⚡ Tom yarn.lock med i git

* 📝 changeset

* 📝 Oppdatert migration-guide

* Update @navikt/aksel-icons/config/index-template.js

* 🚚 rename fitted -> fill in tabs

* 🐛 Async-lasting av date-fns er nå v3 esm compliant

v3 av date-fns støtter ikke default export/import, noe som brakk async-lasting da vi brukte .default

* 🧪 Forbedret Tabs-tester

Lagt til flere test-caser, men implementert testing av keyboard-nav og tabbing

* ⬆️ yarn sync

* ⬆️ Yarn lock

* ⬇️ Reversert duplikate endringer som kom fra v6

* 🎨 Aria-hidden blir nå satt på ikon hvis label allerede er satt på tab

* Update @navikt/core/react/src/tabs/Tabs.types.ts

* 📝 Testnavn for tabs

* ⚡ Skipper unødvendig memo for tab-context, punktum etter all jsdoc

* 🔥 Fjernet TabsContextProps

* 📝 Fjernet comma på changeset

* 📝 Fill-demo for aksel.nav.no

* 📝 Changeset for lazy-prop

* Update @navikt/core/react/src/tabs/parts/tabpanel/TabPanel.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🐛 Beholder  attr fra radix med i ny implementasjon

* ⚡ Flyttet fokus-state update ut av useeffect, fjernet unødvendig klasse for hidden

* ⚡ Unngår unødvendig sjekk for disabled i onFocus for useTab2

* ♻️ tab og tabpanel støtter nå egne id-er

* Update @navikt/core/react/src/tabs/useTabs.ts

* 🐛 I none tilfeller førte setFocusedValue utenfor useEffect til infinite re-renders

* Update @navikt/core/react/src/tabs/parts/tab/Tab.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🐛 bruker-id var satt på feil attrb

* 📝 Dokumentert custom-ids for tabs

* ⚡ Chroamtic-format for tabs-stories

* ♻️ Omskrevet tabs-stories for ny syntax

* 📝 Bruker bold markdown istedenfor note syntax

* 📝 Flyttet warning til newline

* 📝 Bruker bare ett linjeskift for jsdoc

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-04-10 13:36:11 +02:00
dependabot[bot]andKen b417c74374 ⬆️ Bump vite from 5.1.6 to 5.1.7 (#2836)
* ⬆️ Bump vite from 5.1.6 to 5.1.7

Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.1.6 to 5.1.7.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.1.7/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.1.7/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>

* ⬆️ bumpet vite til 5.1.7 i shadow-dom demo

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2024-04-10 12:24:54 +02:00
Halvor Haugan ff1d3534a6 🆙 Oppgrader Chromatic (#2837) 2024-04-10 12:13:48 +02:00
KenandHalvor Haugan 57d015209d [aksel.nav.no] Oppdatert ny GP-struktur og frontend (#2778)
* 🎉 Animert hero-select for nye gp-sider

* 🎨 Implementert keyboard-handler for tema-hero

* ♿ Gjemmer elementer ikke synlige ved hero-select

* 🎉 Ny hero for gp-side

* 🎨 Oppdatet gp-forside

* 🧪 Stories for nye komponenter

* 🐛 tabindex -> tabIndex

* ♻️ Oppdatert queriy definition

* 💄 Justert for mobilvisning

* 🎨 Bruker prictogram-data fra sanity i cards

* 🎉 Bruker faktiske innholdstyper og undertema nå

* 🔥 Fjernet TODO

* 🎨 Oppdatert go-forside til å støtte riktekst description

* 🐛 stories var ikke oppdatert

* 🎉 Bruker hero-cards i tema-hero

* 🐛 stories var ikke oppdatert

* 🐛 Manglet image i response-type

* ♻️ Omstrukturert modalvariant av dialog

* 🎉 Dynamisk hero for gp-sider

* 💫 Dynamisk skalering er nå animert

* 💫 Justert exit-animasjon

* 🔥 Fjernet gammel inline style

* ♻️ Forenklet Static hero callbacks

* 🔥 Fjernet hasRefs sjekk etter query for gp-artikler

* 🎉 Oppdatert serverside interface for gp-tema

* 🎉 Visninger for filtervalg på gp-tema

* 🐛 Valgt element ble ikke fokusert ved hero-åpning

* 🎉 Oppdatert artikkelkort for nye gp-sider

* 💄 Justert spacing for heading/desc seksjoner

* 🎉 Fikset lasting av data og chips

* 🎉 Fungerende chip-navigering

* ⚰️ Fjernet gammel kode

* 🚚 rename tema-hero filer

* 📝 Dokumentert count articles funksjoner

* 🧪 tester for count-articles

* 🚚 Flyttet cardgrid ut til egen fil

* 🔍 lagt til seo for tema

* ⬇️ reversert til prod-dataset

* 🎉 Artikler på gp-forside er sortert etter mest populære

* 🎨 satt description på undertema til required

* 🎉 Panel for forhåndsvisning av undertema/innholdstype på artikkelside

* 🎉 Lagt til forklarings-felt i ny god-praksis struktur

* 🐛 Fjernet chiplist for lister uten noe data

* 🎉 Tilpasset temaheader for kontrast

* 💄 normal -> semibold for statichero title

* ♿ Wrappet lister i ul > li

* 🎉 Oppdatert til ul > li på artikkel-lister

* 🐛 Cards strakk seg ikke til høyde etter wrapping i li

* 🐛 Manglet main-element og skip-link støtte

* 🔍 Bedre meta-description

* 🔥 Fjernet kommentar

* 🎨 Gjort klar sitemap for ny god-praksis

* ♿ Bedre semantikk for temasider

* 🎨 Justert tema header

* 🎨 Oppdatert akselchevron

* 💫 Justert page-animasjon

* 🎨 La til artikkel-chips

* ♻️ Link chip -> temalink

* 🎨 Tema-tagging på gp-artikler

* 💄 Gradient og illustrasjon på tema-hero

* 💄 La til bildestøtte i hero-tittel

* 🐛 stories brakk

* 💄 Satt god praksis heading til text- default for bedre kontrast

* 💄 z-index problem på intro hero

* 💄 Småjusteringer design

* 🐛 z-index stacking av cube-illustrasjon nå bak tekst

* 📝 Lagt til kommentar

* 🚚 Flyttet over støtte for wc definering i react

* 🐛 Brukte feil temahenter i sitemap som brakk playwright

* ♿ uu-oppdateringer etter review

* ♻️ Explanation -> description for undertema og tema

* 💄 Oppdatert Hero for teamsider

* 💄 Blur-bg på tekst i hero, no underline på hover for komponentsforside

* ♻️ Fokus beholdet på select-knapp ved åpning av tema-dialog

* 📝 Commentarer på escapeKeyDown hook

* 💄 Gjennbruker gradient for alle hero-headers

* 📝 Skrivefeil

* 📝 Kommentert useGpViews

* 📝 Kommentert endringer

* ⬆️ Bumpet sanity til v3.36

* ♻️ Fjernet Modal variant av tema hero.

Bruker samme dialog på mobil og desktop

* 🎨 aria headings -> h3 for gp-forside lenke

* 📝 Lowercase i html for artikkelcards

* Update aksel.nav.no/website/pages/gp/index.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update aksel.nav.no/website/components/website-modules/AnimatedChevron.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update aksel.nav.no/website/components/layout/god-praksis-page/cards/GpArticleCard.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update aksel.nav.no/website/components/layout/god-praksis-page/hero/tema-hero/parts/HeroCardList.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* 🚚 ArticleView -> ArticleSections

* 🚚 bosheight -> dialogheight

* 🐛 Import feilet av GpCardGrid etter navneendring

* 🐛 hero navigasjon var ikke sortert etter temanavn

* 🔥 Fjernet explanation-felt for innholdstyper

* ♻️ Ryddet opp i kommentarer fra github

* ♻️ Bruker SanityImageSource for Image-type

* 💄 Selected-variant av herocard. Inverted-pictogram i sanity

* 📝 view -> queryView

* ♻️ Skrevet om ArticleSections for å unngå repetert kode

* ⬆️ yarn lock

* ♻️ Fjernet unødvendig react-fragment

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-04-10 10:15:56 +02:00
Ken 5969859024 🐛 Unngå at global Escape-event lukker ikon-sidebar
Dette gjelder ved escape i Søk(modal) og søkefelt på ikonside
2024-04-10 09:05:51 +02:00
github-actions[bot]andgithub-actions[bot] 9bbc3e6813 Version Packages (#2835)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-09 15:25:38 +02:00
Ken 5250b47ca4 💄 Border-radius på FileUpload.Item er nå rund (#2834) 2024-04-09 15:20:40 +02:00
Ken fc7709ae2a 🐛 Søk filtrerte ut beste treff fra 'nyeste artikler' 2024-04-09 09:40:20 +02:00
github-actions[bot]andgithub-actions[bot] 2364e689ed Version Packages (#2831)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-09 09:12:06 +02:00
Halvor Haugan d57c0b2c2c Justering av eksempler på Checkbox (#2833) 2024-04-09 09:06:58 +02:00
Ken 868bb543a5 🐛 Preview brakk for god-praksis
Siden 'userState' ikke hentes fra Sanity, blir den undefined ved lasting av previewdata
2024-04-09 07:33:16 +02:00
Torkjel SkjetneandHalvor Haugan 8f9ed7a404 FileUpload.Item: 💥 Endret API og støtte for å sette ID på knappen (#2824)
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2024-04-08 09:16:53 +02:00
Halvor HauganandKen 5ea6255f6d Justeringer av eksempler på Radio og Checkbox med mer (#2830)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2024-04-04 20:54:12 +02:00
github-actions[bot]andgithub-actions[bot] 491fccbe80 Version Packages (#2828)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-03 11:56:35 +02:00
Ken cca2a8263f 💄 Fjernet Illustrasjon fra bloggsider (#2827) 2024-04-03 11:50:12 +02:00
Ken 3140ec2765 🔥 Fjernet role=gridcell fra datoknapper (#2826)
Siden knappene allerede er i en table og da også td er det unødvendig med gricell
2024-04-03 11:37:05 +02:00
Halvor Haugan d8fce83ae3 Oppgrader til Storybook 8 (#2825) 2024-04-03 10:03:44 +02:00
github-actions[bot]andgithub-actions[bot] f88b00daf6 Version Packages (#2821)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-03 09:45:19 +02:00
Ken 6d0baea5c1 👽 Oppdatert om Aksel lenke i footer
Side er flyttet fra god-praksis til standalone-side url
Er opprettet automatisk redirect slik at eldre lenker ikke brekker
2024-04-03 09:25:54 +02:00
Ken 880e77b9c2 ♻️ Fjernet max-krav for antall tegn i Relaterte lenker-module
Siden lenketeksten ofte refererer til en sidetittel skaper det utfordringer da de ofte er litt lengre
2024-04-03 09:03:57 +02:00
Ken 6d16373f85 👽 Oppdatert skriv for aksel lenke i footer
Side er flyttet fra god-praksis til standalone-side url
Er opprettet automatisk redirect slik at eldre lenker ikke brekker
2024-04-03 08:42:08 +02:00
Halvor Haugan ae078254dc Modal: Fjern tomt element (#2819) 2024-04-02 10:20:00 +02:00
github-actions[bot]andgithub-actions[bot] 638e8260b4 Version Packages (#2811)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-02 08:45:10 +02:00
Halvor Haugan d991011690 FileUpload.Dropzone: id prop settes på button (#2817) 2024-03-22 11:03:05 +01:00
KenandHalvor Haugan b6086f7b1f 📝 Oppdatert radio-error dokumentasjon (#2800)
* 📝 Oppdatert radio-error dokumentasjon

* Update aksel.nav.no/website/pages/eksempler/radio/error.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update aksel.nav.no/website/pages/eksempler/radio/error.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update aksel.nav.no/website/pages/eksempler/radio/error.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-03-22 10:42:38 +01:00
Ken 05462081aa useControllableState i Expansioncard og Switch state-bug (#2810)
* ♻️ Bruker controllableState i ExpansionCard

* 🐛 Fikset tilfelle der checked switch var visuelt unchecked

* 📝 Changeset

* 🔥 Fjernet utkommentert kode
2024-03-22 10:24:35 +01:00
Ken 63e42de3ee 🆙 Oppdatert Playroom (#2815) 2024-03-22 09:31:00 +01:00
Halvor Haugan 2e85178166 yarn.lock sync 2024-03-21 16:36:11 +01:00
Ken 456d4e2e07 🐛 Slack var ikke i outbound accessPolicy for feedback 2024-03-21 11:52:48 +01:00
Ken b5a0cb8cdf Oppdatert logging for Aksel.nav.no + bumpet feedback pagination page-size (#2812)
* 🆙 Oppgradert logging

* 🎨 Bedre loggeoutput

* 🎨 Bruker childlogger

* 🧪 Custome error-logg

* ⚡ Større paginering-page for slackusers

* 📝 error -> info
2024-03-21 11:36:43 +01:00
Ken a2af9bcc37 Lagt til mer padding-right på ReadMore (#2806)
* 💄 Oppdatert padding-right på readmore

* 📝 Changeset

* 🐛 Manglet chromatic-disable på readmore story
2024-03-21 10:16:14 +01:00
bd45754adb [aksel.nav.no] Slackbot for feedback (#2785)
* 🎉 Satt opp boilerplate for auth på Aksel

* 🎉 Auth boilerplate

* 🎨 Demo-setup for å teste auth i dev

* 🚚 Flyttet auth til getserversideprops

* 🔥 Fjernet authprovider fra _app

* 🐛 revalidate er ikke valid key for getserversideprops i nextjs

* 🚚 rename auth-dir

* 🔥 Fjernet auth types

* 🎉 Auth for api

* 🎨 Oppdatert workflows med token

* 🔥 Fjernet testing av auth på god-praksis sider

* 🎨 La til sidecar på prod-deploy

* 🐛 only return boolean in Promise<boolean>

* ✨ feedback form

* 🎨 showcase all states of new FeedBackForm

* 🔥 remove prop name (will use auth api)

* 🎨 use getAuthUser()

* 🎨 use useAuth() for login/logout

* 🎉 Slack feedback-api

* 🐛 handle case where token is null

* 🎨 scroll to anchor on login redirect

* ⚡ Utility-funksjoner for slack-api

* 🧪 Tests for slack-utilities

* 🎨 Ryddet opp i henting av slack memebers

* 🚚 Flyttet caching inn i funkson

* 📱 make FeedbackForm responsive

* 🎉 Strengere typet slack-utilities

* 🎨 Fikset typefeil i feedback-api

* ⬆️ Migrert til @navikt/oasis

* 🏷️ Satt typer til strict for å kjøre tester

* 🎨 Slackbot-API with protected auth and actual user

* 🏷️ strengere typer

* ⚡ Implementert paginering av slacbot

* 🎨 Lagt til slackbot-token i workflows

* 🏷️ strict tsconfig

* 🏷️ Typer for slackbot input

* 🔥 Fjernet demo-body

* 🎨 oppdatert medvirkende tekst

* ⬆️ Yarn lock

* 🎨 move state handling down to Feedback

* 🏷️ typing fixes

* 🚧 API usage

* 🏷️ fix type error

* 👷 update AccessPolicy

* 🦺 add validation to feedback form

* 💄 justify-items-stretch on TextArea needed 🤔

* ♻️ avoid prop drilling, use sanity data hook for document id

* 🔧 add auth to webpack config for storybook

* ♻️ Feedback form  (also add error state handling)

* 👷 add stories for Feedback form

* 🔊 log feedback results metadata

* ♻️ remove refs, use formData and event.currentTarget

* 🧪 retry flaky test

* ⚗️ test promises for dynamic import in test

* 🧪 flaky test config

* ⬆️ yarn lock sync

* ⬆️ yarn lock sync

* ♻️ Bruker nå egen userState-type for fortelle komponenter om bruker er logget inn eller ikke

* 🔥 remove unused CSS

* 📝 lagt til todods

* 🔥 remove anonymous feedback submission

* 🔥 remove CSS

* :goals_net: catch fetch clientside, feedback

* ✨ send feedback to private slack channel as well

notify everyone if there are 0 editors

* 💄 + ♿ aria-hiddens and better styling, Feedback

* ♻️ refactor + add more feedback (loading state + alert)

* ⚗️ test encoding uri for anchor (redirect)

* ⚗️ more testing URL redirect

* ⚗️ more testing

* ⚗️ testing

* ✅ stories for Feedback

* 🔒 sanitize user objects (then cache)

* 🏷️ type SanitizeUser in find-user

* 🐛 optionally render the message about zero editors

* ⚗️ more testing for URL redirect / fragment issue

* ⚗️ log the redirect URL

* ⚗️ more testing

* ♻️ Gjennbruker error-return for feedback-api

* 🧪 Tester alternativ løsning for scroll etter login

* 🐛 Query update slapte loop i useEffect

* 🧪 Tester alternativ løsning for scroll etter login

* 🚚 Flyttet autofocus inn i headingseksjon

* 🐛 Wonderwall fjerner & fra query

* 🐛 encodeUrl as defined in wonderwall docs

* 🧪 Reversert til anchor-løsning

* 🧪 Reversert til anchor-løsning med encoding

* 🐛 Fikset edgecase der asPath med allerede eksisterende hash brakk login-redirect

* 🔥 Cleanup etter anchor-testing

* ⬆️ yarn lock sync

* 🎨 Oppdatert til navikt channelid

* 📝 Endret på error-tekst i feedback

* Fjernet ts-expect-error

* ⬆️ yarn lock sync

* 🔥 Fjernet filterering på godkjent mail

* 📝 Oppdatert slackbot-block

---------

Co-authored-by: Julian Nymark <julian.nymark@nav.no>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2024-03-20 15:48:49 +01:00
github-actions[bot]andgithub-actions[bot] a882a8a389 Version Packages (#2807)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-03-20 13:26:51 +01:00
Ken d1606d358a 🐛 useDatepicker håndterer isInvalid validation riktig (#2809)
* 🐛 useDatepicker håndterer isInvalid validation riktig nå

* Update @navikt/core/react/src/date/hooks/useDatepicker.tsx

* Update @navikt/core/react/src/date/hooks/useDatepicker.tsx
2024-03-20 13:21:05 +01:00
Ken bb87837632 🧪 Bump timeout and retry of dateStr testing 2024-03-20 12:33:56 +01:00
Halvor Haugan 9c8b2301ed FileUpload.Dropzone: Fiks feil med duplisert id (#2805) 2024-03-20 12:31:08 +01:00
Ken 54ba483d2d ✅ Oppdatert Chromatic-tester
✅ Oppdatert dropdown-tester

🐛 satt dropdown-story til open

✅ Oppdatert errorsummary-tester

✅ Oppdatert fieldset-tester
2024-03-20 11:51:46 +01:00
247 changed files with 8458 additions and 5841 deletions

No files matched your search

+1
View File
@@ -25,6 +25,7 @@ jobs:
- name: add tokens to .env
run: |
echo "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_WRITE_KEY=${{ secrets.SANITY_FEEDBACK_WRITE_KEY }}" >> aksel.nav.no/website/.env
echo "SANITY_PREVIEW_TOKEN=${{ secrets.SANITY_PREVIEW_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_PRIVATE_NO_DRAFTS=${{ secrets.SANITY_PRIVATE_NO_DRAFTS }}" >> aksel.nav.no/website/.env
+1
View File
@@ -59,6 +59,7 @@ jobs:
- name: add tokens to .env
run: |
echo "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_WRITE_KEY=${{ secrets.SANITY_FEEDBACK_WRITE_KEY }}" >> aksel.nav.no/website/.env
echo "SANITY_PREVIEW_TOKEN=${{ secrets.SANITY_PREVIEW_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_PRIVATE_NO_DRAFTS=${{ secrets.SANITY_PRIVATE_NO_DRAFTS }}" >> aksel.nav.no/website/.env
+2 -2
View File
@@ -1,4 +1,3 @@
import { mergeConfig } from "vite";
import turbosnap from "vite-plugin-turbosnap";
export default {
@@ -7,7 +6,7 @@ export default {
stories: () => ["../@navikt/**/*.stories.@(js|jsx|ts|tsx|mdx)", "./*.mdx"],
addons: [
"@storybook/addon-a11y",
"@xfinx/storybook-addon-html",
"@whitespace/storybook-addon-html",
"@storybook/addon-interactions",
{
name: "@storybook/addon-storysource",
@@ -42,6 +41,7 @@ export default {
},
async viteFinal(config, { configType }) {
const { mergeConfig } = await import("vite");
return mergeConfig(config, {
plugins:
configType === "PRODUCTION"
+14
View File
@@ -1,5 +1,19 @@
# @navikt/aksel-icons
## 6.5.0
## 6.4.1
## 6.4.0
## 6.3.6
## 6.3.5
## 6.3.4
## 6.3.3
## 6.3.2
## 6.3.1
@@ -28,7 +28,7 @@
"postcss": "^8.4.31",
"postcss-url": "^10.1.3",
"typescript": "^5.1.6",
"vite": "^5.0.12",
"vite": "^5.1.7",
"vite-plugin-singlefile": "^1.0.0",
"vite-plugin-svgr": "^4.2.0"
}
+4 -4
View File
@@ -3,12 +3,12 @@ import React from "react";
import * as AkselIcons from "@navikt/aksel-icons";
import { StarFillIcon } from "@navikt/aksel-icons";
const meta = {
const meta: Meta<typeof StarFillIcon> = {
title: "aksel-icons/Icons",
} satisfies Meta<typeof StarFillIcon>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof StarFillIcon>;
export const Icons: Story = {
render: () => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "6.3.2",
"version": "6.5.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
+14
View File
@@ -1,5 +1,19 @@
# @navikt/aksel-stylelint
## 6.5.0
## 6.4.1
## 6.4.0
## 6.3.6
## 6.3.5
## 6.3.4
## 6.3.3
## 6.3.2
## 6.3.1
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "6.3.2",
"version": "6.5.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
"repository": {
@@ -35,8 +35,8 @@
"dev": "yarn watch:lint"
},
"devDependencies": {
"@navikt/ds-css": "^6.3.2",
"@navikt/ds-tokens": "^6.3.2",
"@navikt/ds-css": "^6.5.0",
"@navikt/ds-tokens": "^6.5.0",
"concurrently": "7.2.1",
"postcss-selector-parser": "^6.0.13",
"postcss-value-parser": "^4.2.0",
+49
View File
@@ -1,5 +1,54 @@
# @navikt/aksel
## 6.5.0
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@6.5.0
## 6.4.1
### Patch Changes
- Updated dependencies [[`5250b47ca`](https://github.com/navikt/aksel/commit/5250b47ca410f9cd57c5350bacc77b1b2ec9b2c0)]:
- @navikt/ds-css@6.4.1
## 6.4.0
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@6.4.0
## 6.3.6
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@6.3.6
## 6.3.5
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@6.3.5
## 6.3.4
### Patch Changes
- Updated dependencies [[`a2af9bcc3`](https://github.com/navikt/aksel/commit/a2af9bcc37eb12cc7cf3a1596d57dc0421898657)]:
- @navikt/ds-css@6.3.4
## 6.3.3
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@6.3.3
## 6.3.2
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "6.3.2",
"version": "6.5.0",
"description": "Aksel command line interface. Handles css-imports, codemods and more",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -29,7 +29,7 @@
},
"homepage": "https://aksel.nav.no/grunnleggende/kode/kommandolinje",
"dependencies": {
"@navikt/ds-css": "6.3.2",
"@navikt/ds-css": "6.5.0",
"axios": "1.6.0",
"chalk": "4.1.0",
"clipboardy": "^2.3.0",
+22
View File
@@ -1,5 +1,27 @@
# @navikt/ds-css
## 6.5.0
## 6.4.1
### Patch Changes
- FileUpload: Item-illustrasjon er nå radius-full ([#2834](https://github.com/navikt/aksel/pull/2834))
## 6.4.0
## 6.3.6
## 6.3.5
## 6.3.4
### Patch Changes
- ReadMore: Padding-right på button er bumpet noen px ([#2806](https://github.com/navikt/aksel/pull/2806))
## 6.3.3
## 6.3.2
## 6.3.1
+1 -1
View File
@@ -182,7 +182,7 @@
.navds-file-item__icon {
background-color: var(--a-surface-neutral-subtle);
color: var(--a-icon-default);
border-radius: var(--a-border-radius-xlarge);
border-radius: var(--a-border-radius-full);
min-height: 3rem;
min-width: 3rem;
display: grid;
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "6.3.2",
"version": "6.5.0",
"description": "CSS for NAV Designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -27,7 +27,7 @@
"css:get-version": "node config/get-version.js"
},
"devDependencies": {
"@navikt/ds-tokens": "^6.3.2",
"@navikt/ds-tokens": "^6.5.0",
"cssnano": "6.0.0",
"fast-glob": "3.2.11",
"lodash": "4.17.21",
+2 -2
View File
@@ -8,12 +8,12 @@
gap: var(--a-spacing-05);
color: var(--ac-read-more-text, var(--a-text-action));
border-radius: var(--a-border-radius-small);
padding: var(--a-spacing-1) var(--a-spacing-1) var(--a-spacing-1) var(--a-spacing-05);
padding: var(--a-spacing-1) var(--a-spacing-2) var(--a-spacing-1) var(--a-spacing-05);
text-align: start;
}
.navds-read-more--small .navds-read-more__button {
padding: var(--a-spacing-05) var(--a-spacing-1) var(--a-spacing-05) var(--a-spacing-05);
padding: var(--a-spacing-05) var(--a-spacing-1-alt) var(--a-spacing-05) var(--a-spacing-05);
}
@media (forced-colors: active) {
+9 -1
View File
@@ -7,7 +7,7 @@
.navds-tabs__tablist {
display: flex;
max-width: 100%;
width: auto;
width: 100%;
scroll-behavior: smooth;
-ms-overflow-style: none; /* for Internet Explorer, Edge */
scrollbar-width: none; /* for Firefox */
@@ -83,6 +83,10 @@
gap: var(--a-spacing-1);
}
.navds-tabs__tab-inner > * {
display: inline-flex;
}
.navds-tabs__tab-inner svg {
flex-shrink: 0;
}
@@ -117,6 +121,10 @@
font-size: 1.5rem;
}
.navds-tabs__tab--fill {
flex: 1 1 100%;
}
.navds-tabs__tabpanel:focus-visible {
outline: 2px solid transparent;
box-shadow: inset 0 0 0 2px var(--a-border-focus);
+4
View File
@@ -4,6 +4,10 @@
gap: var(--a-spacing-2);
}
.navds-toggle-group__wrapper--fill {
justify-items: initial;
}
.navds-toggle-group {
border-radius: var(--a-border-radius-medium);
background-color: var(--ac-toggle-group-bg, var(--a-surface-transparent));
+88
View File
@@ -1,5 +1,93 @@
# @navikt/ds-react
## 6.5.0
### Minor Changes
- Tabs: Ny prop `lazy` som rendrer innhold i TabPanel selv når panel er skjult (har fortsatt display:none) ([#2621](https://github.com/navikt/aksel/pull/2621))
- ToggleGroup: Erstattet bruk av dependency `@radix-ui/react-toggle-group` med egen implementasjon. ([#2620](https://github.com/navikt/aksel/pull/2620))
- Tabs: La til ny prop `fill` som lar Tabs.Tab-elementer strekke seg over tilgjengelig bredde. ([#2621](https://github.com/navikt/aksel/pull/2621))
- Tabs: Erstattet bruk av dependency `@radix-ui/react-tabs` med egen implementasjon. ([#2621](https://github.com/navikt/aksel/pull/2621))
### Patch Changes
- DatePicker/MonthPicker: Valgte datoer får nå `aria-pressed` for å bedre indikere valg for skjermleser. ([#2838](https://github.com/navikt/aksel/pull/2838))
- DatePicker/MonthPicker: `required`-prop stoppet ikke de-select av allerede valgt dato. ([#2838](https://github.com/navikt/aksel/pull/2838))
- ToggleGroup: La til ny prop `fill` som strekker ToggleGroup til å ta opp all tilgjengelig bredde. ([#2620](https://github.com/navikt/aksel/pull/2620))
- Updated dependencies []:
- @navikt/ds-tokens@6.5.0
- @navikt/aksel-icons@6.5.0
## 6.4.1
### Patch Changes
- Updated dependencies []:
- @navikt/ds-tokens@6.4.1
- @navikt/aksel-icons@6.4.1
## 6.4.0
### Minor Changes
- FileUpload.Item: :boom: Endret API og støtte for å sette ID på knappen ([#2824](https://github.com/navikt/aksel/pull/2824))
### Patch Changes
- Updated dependencies []:
- @navikt/ds-tokens@6.4.0
- @navikt/aksel-icons@6.4.0
## 6.3.6
### Patch Changes
- DatePicker: Fjernet role gridcell fra dato-knapper. ([#2826](https://github.com/navikt/aksel/pull/2826))
- Updated dependencies []:
- @navikt/ds-tokens@6.3.6
- @navikt/aksel-icons@6.3.6
## 6.3.5
### Patch Changes
- Modal: Fjern tomt element ([#2819](https://github.com/navikt/aksel/pull/2819))
- Updated dependencies []:
- @navikt/ds-tokens@6.3.5
- @navikt/aksel-icons@6.3.5
## 6.3.4
### Patch Changes
- Switch: Fikset edgecase der checked Switch var visuelt unchecked ([#2810](https://github.com/navikt/aksel/pull/2810))
- FileUpload.Dropzone: id prop settes på button ([#2817](https://github.com/navikt/aksel/pull/2817))
- Updated dependencies []:
- @navikt/ds-tokens@6.3.4
- @navikt/aksel-icons@6.3.4
## 6.3.3
### Patch Changes
- FileUpload.Dropzone: Fiks feil med duplisert id ([#2805](https://github.com/navikt/aksel/pull/2805))
- useDatePicker: `isInvalid`-key i onValidate var hadde flipper boolean i noen tilfeller ([#2809](https://github.com/navikt/aksel/pull/2809))
- Updated dependencies []:
- @navikt/ds-tokens@6.3.3
- @navikt/aksel-icons@6.3.3
## 6.3.2
### Patch Changes
+3 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "6.3.2",
"version": "6.5.0",
"description": "React components from the Norwegian Labour and Welfare Administration.",
"author": "Aksel, a team part of the Norwegian Labour and Welfare Administration.",
"license": "MIT",
@@ -562,10 +562,8 @@
},
"dependencies": {
"@floating-ui/react": "0.25.4",
"@navikt/aksel-icons": "^6.3.2",
"@navikt/ds-tokens": "^6.3.2",
"@radix-ui/react-tabs": "1.0.0",
"@radix-ui/react-toggle-group": "1.0.0",
"@navikt/aksel-icons": "^6.5.0",
"@navikt/ds-tokens": "^6.5.0",
"clsx": "^2.1.0",
"date-fns": "^3.0.0",
"react-day-picker": "8.10.0"
@@ -33,6 +33,7 @@ export interface CopyButtonProps
/**
* Text shown when button is clicked.
* Only set if used with `text`-prop.
* @default "Kopiert!"
*/
activeText?: string;
/**
@@ -1,6 +1,8 @@
import { Meta, StoryObj } from "@storybook/react";
import { expect, userEvent, within } from "@storybook/test";
import { isSameDay } from "date-fns";
import React, { useId, useState } from "react";
import { act } from "react-dom/test-utils";
import { Button } from "../../button";
import { HGrid } from "../../layout/grid";
import { VStack } from "../../layout/stack";
@@ -160,6 +162,7 @@ export const UseDatepicker = () => {
fromDate: new Date("Aug 23 2019"),
toDate: new Date("Feb 23 2024"),
onDateChange: console.log,
onValidate: console.log,
});
return (
@@ -439,6 +442,43 @@ export const WeekDayClick = () => {
);
};
export const Required = {
render: () => {
const { datepickerProps } = useDatepicker({
defaultSelected: new Date("Apr 10 2024"),
required: true,
});
return (
<div style={{ height: "20rem" }}>
<DatePicker.Standalone {...datepickerProps} />
</div>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const button10 = canvas.getByRole("button", { pressed: true });
await act(async () => {
await userEvent.click(button10);
});
expect(button10.ariaPressed).toBe("true");
const button17 = canvas.getByText("17").closest("button");
expect(button17?.ariaPressed).toBe("false");
await act(async () => {
button17 && (await userEvent.click(button17));
});
expect(button17?.ariaPressed).toBe("true");
expect(button10.ariaPressed).toBe("false");
},
};
export const ModalDemo = () => {
const { datepickerProps, inputProps } = useDatepicker({
fromDate: new Date("Aug 23 2019"),
@@ -20,8 +20,11 @@ const DayButton = (props: DayProps) => {
name="day"
ref={buttonRef}
{...dayRender.buttonProps}
role={undefined}
aria-label={dateTime}
aria-hidden={dayRender.activeModifiers.outside}
aria-selected={undefined}
aria-pressed={!!dayRender.activeModifiers.selected}
/>
);
};
@@ -229,12 +229,16 @@ export const useDatepicker = (
/* Only allow de-selecting if not required */
const handleDayClick: DayClickEventHandler = (day, { selected }) => {
if (selected && required) {
return;
}
if (day && !selected) {
handleOpen(false);
anchorRef?.focus();
}
if (!required && selected) {
if (selected) {
updateDate(undefined);
setInputValue("");
updateValidation({ isValidDate: false, isEmpty: true });
@@ -272,7 +276,7 @@ export const useDatepicker = (
) {
updateDate(undefined);
updateValidation({
isInvalid: isValidDate(day),
isInvalid: !isValidDate(day),
isWeekend: disableWeekends && isWeekend(day),
isDisabled: disabled && isMatch(day, disabled),
isValidDate: false,
@@ -225,13 +225,17 @@ export const useMonthpicker = (
/* Only allow de-selecting if not required */
const handleMonthClick = (month?: Date) => {
if (!month && required) {
return;
}
if (month) {
handleOpen(false);
setYear(month);
anchorRef?.focus();
}
if (!required && !month) {
if (!month) {
updateMonth(undefined);
updateValidation({ isValidMonth: false, isEmpty: true });
setInputValue("");
@@ -66,6 +66,7 @@ export const MonthButton = ({
type="button"
onClick={() => onSelect(isSelected ? undefined : month)}
disabled={isDisabled}
aria-pressed={!!isSelected}
className={cl("navds-date__month-button", {
"rdp-day_today": dateIsInCurrentMonth(month, year),
"rdp-day_selected": isSelected,
@@ -1,6 +1,8 @@
import { Meta, StoryFn, StoryObj } from "@storybook/react";
import { expect, userEvent, within } from "@storybook/test";
import { setYear } from "date-fns";
import React, { useId, useState } from "react";
import { act } from "react-dom/test-utils";
import { Button } from "../../button";
import { useMonthpicker } from "../hooks";
import { DateInputProps } from "../parts/DateInput";
@@ -121,25 +123,41 @@ export const UseMonthpickerFormat = () => {
);
};
export const Required = () => {
const { inputProps, monthpickerProps } = useMonthpicker({
locale: "nb",
defaultSelected: new Date(),
disabled: [new Date("Apr 1 2022")],
required: true,
});
export const Required = {
render: () => {
const { monthpickerProps } = useMonthpicker({
defaultSelected: new Date("Apr 10 2024"),
required: true,
});
return (
<div style={{ height: "20rem" }}>
<MonthPicker {...monthpickerProps}>
<MonthPicker.Input
{...inputProps}
label="Velg måned"
variant="monthpicker"
/>
</MonthPicker>
</div>
);
return (
<div style={{ height: "20rem" }}>
<MonthPicker.Standalone {...monthpickerProps} />
</div>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const buttonApr = canvas.getByRole("button", { pressed: true });
await act(async () => {
await userEvent.click(buttonApr);
});
expect(buttonApr.ariaPressed).toBe("true");
const buttonSep = canvas.getByText("september").closest("button");
expect(buttonSep?.ariaPressed).toBe("false");
await act(async () => {
buttonSep && (await userEvent.click(buttonSep));
});
expect(buttonSep?.ariaPressed).toBe("true");
expect(buttonApr.ariaPressed).toBe("false");
},
};
export const UserControlled = () => {
@@ -193,7 +211,6 @@ export const Chromatic: Story = {
<DisabledMonths />
<UseMonthpicker />
<UseMonthpickerFormat />
<Required />
<UserControlled />
<FollowYear />
</div>
@@ -1,4 +1,4 @@
import { Meta } from "@storybook/react";
import { Meta, StoryObj } from "@storybook/react";
import React, { useState } from "react";
import { Button } from "../button";
import Dropdown from "./Dropdown";
@@ -6,10 +6,15 @@ import Dropdown from "./Dropdown";
export default {
title: "ds-react/Dropdown",
component: Dropdown,
} as Meta;
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof Dropdown>;
export const Default = () => {
return (
type Story = StoryObj<typeof Dropdown>;
export const Default: Story = {
render: () => (
<Dropdown onSelect={(event) => console.log(event)}>
<Dropdown.Toggle>Toggle</Dropdown.Toggle>
<Dropdown.Menu strategy="fixed">
@@ -33,10 +38,10 @@ export const Default = () => {
</Dropdown.Menu.List>
</Dropdown.Menu>
</Dropdown>
);
),
};
export const DefaultOpen = {
export const DefaultOpen: Story = {
render: () => (
<Dropdown onSelect={(event) => console.log(event)} defaultOpen>
<Button as={Dropdown.Toggle}>Toggle</Button>
@@ -57,20 +62,41 @@ export const DefaultOpen = {
</Dropdown.Menu>
</Dropdown>
),
args: { chromatic: { delay: 300 } },
};
export const ControlledOpen = () => {
const [openState, setOpenState] = useState(true);
return (
<Dropdown onSelect={(event) => console.log(event)} open={openState}>
<Button as={Dropdown.Toggle} onClick={() => setOpenState(!openState)}>
Toggle
</Button>
<Dropdown.Menu
strategy="fixed"
onClose={() => console.log("ONCLOSE CONTROLLED")}
>
export const ControlledOpen: Story = {
render: () => {
const [openState, setOpenState] = useState(true);
return (
<Dropdown onSelect={(event) => console.log(event)} open={openState}>
<Button as={Dropdown.Toggle} onClick={() => setOpenState(!openState)}>
Toggle
</Button>
<Dropdown.Menu
strategy="fixed"
onClose={() => console.log("ONCLOSE CONTROLLED")}
>
<Dropdown.Menu.GroupedList>
<Dropdown.Menu.GroupedList.Heading>
Systemer og oppslagsverk
</Dropdown.Menu.GroupedList.Heading>
<Dropdown.Menu.GroupedList.Item
onClick={() => console.log("GroupedList.Item-click")}
>
Gosys
</Dropdown.Menu.GroupedList.Item>
</Dropdown.Menu.GroupedList>
</Dropdown.Menu>
</Dropdown>
);
},
};
export const Chromatic: Story = {
render: () => (
<Dropdown onSelect={(event) => console.log(event)} open>
<Dropdown.Toggle>Toggle</Dropdown.Toggle>
<Dropdown.Menu strategy="fixed">
<Dropdown.Menu.GroupedList>
<Dropdown.Menu.GroupedList.Heading>
Systemer og oppslagsverk
@@ -81,8 +107,18 @@ export const ControlledOpen = () => {
Gosys
</Dropdown.Menu.GroupedList.Item>
</Dropdown.Menu.GroupedList>
<Dropdown.Menu.Divider />
<Dropdown.Menu.List>
<Dropdown.Menu.List.Item onClick={() => console.log("Item-click")}>
Gosys
</Dropdown.Menu.List.Item>
<Dropdown.Menu.List.Item>Psys</Dropdown.Menu.List.Item>
<Dropdown.Menu.List.Item disabled>Infotrygd</Dropdown.Menu.List.Item>
</Dropdown.Menu.List>
</Dropdown.Menu>
</Dropdown>
);
),
parameters: {
chromatic: { disable: false, delay: 300 },
},
};
ControlledOpen.args = { chromatic: { delay: 300 } };
@@ -1,5 +1,6 @@
import cl from "clsx";
import React, { forwardRef, useRef, useState } from "react";
import React, { forwardRef, useRef } from "react";
import { useControllableState } from "../util/hooks";
import { OverridableComponent } from "../util/types";
import ExpansionCardContent, {
ExpansionCardContentProps,
@@ -116,24 +117,24 @@ export const ExpansionCard = forwardRef<HTMLDivElement, ExpansionCardProps>(
},
ref,
) => {
const [_open, _setOpen] = useState(defaultOpen);
const shouldFade = useRef<boolean>(!(Boolean(open) || defaultOpen));
const handleOpen = () => {
if (open === undefined) {
const newOpen = !_open;
_setOpen(newOpen);
onToggle?.(newOpen);
} else {
onToggle?.(!open);
}
shouldFade.current = true;
};
const [_open, _setOpen] = useControllableState({
value: open,
onChange: (newValue) => {
onToggle?.(newValue);
shouldFade.current = true;
},
defaultValue: defaultOpen,
});
return (
<ExpansionCardContext.Provider
value={{ open: open ?? _open, toggleOpen: handleOpen, size }}
value={{
open: open ?? _open,
toggleOpen: () => _setOpen((x) => !x),
size,
}}
>
<section
{...rest}
@@ -16,7 +16,7 @@ export const CheckboxGroupContext = createContext<CheckboxGroupState | null>(
export interface CheckboxGroupProps
extends Omit<
FieldsetProps,
"onChange" | "errorPropagation" | "defaultValue"
"onChange" | "errorPropagation" | "defaultValue" | "nativeReadOnly"
> {
/**
* Collection of `<Checkbox/>`.
@@ -27,7 +27,7 @@ export interface CheckboxGroupProps
*/
value?: any[];
/**
* Default checked checkboxes on render.
* Default checked checkboxes.
*/
defaultValue?: any[];
/**
@@ -44,10 +44,10 @@ export interface CheckboxGroupProps
*
* @example
* ```jsx
* <CheckboxGroup legend="Hvor vil du sitte?">
* <Checkbox value="Bakerst">Bakerst</Checkbox>
* <Checkbox value="Midterst">Midterst</Checkbox>
* <Checkbox value="Fremst">Fremst</Checkbox>
* <CheckboxGroup legend="Transportmiddel">
* <Checkbox value="car">Bil</Checkbox>
* <Checkbox value="taxi">Drosje</Checkbox>
* <Checkbox value="public">Kollektivt</Checkbox>
* </CheckboxGroup>
* ```
*/
@@ -14,7 +14,7 @@ export interface CheckboxProps
*/
errorId?: string;
/**
* Checkbox label
* Checkbox label.
*/
children: React.ReactNode;
/**
@@ -22,7 +22,7 @@ export interface CheckboxProps
*/
hideLabel?: boolean;
/**
* The value of the HTML element.
* Checkbox value.
*/
value?: any;
/**
@@ -31,7 +31,7 @@ export interface CheckboxProps
*/
indeterminate?: boolean;
/**
* Adds a description to extend labeling of Checkbox.
* Adds a description to extend the labeling.
*/
description?: string;
}
@@ -1,5 +1,6 @@
import { Meta } from "@storybook/react";
import { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { VStack } from "../../layout/stack";
import { ErrorSummary } from "./ErrorSummary";
export default {
@@ -18,9 +19,14 @@ export default {
},
},
},
} as Meta;
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof ErrorSummary>;
export const Default = {
type Story = StoryObj<typeof ErrorSummary>;
export const Default: Story = {
render: (props) => (
<ErrorSummary
heading="Feiloppsummering komponent"
@@ -35,11 +41,41 @@ export const Default = {
),
};
export const Small = () => (
<ErrorSummary heading="Feiloppsummering komponent" size="small">
<ErrorSummary.Item href="#1">Checkbox må fylles ut</ErrorSummary.Item>
<ErrorSummary.Item href="#2">
Tekstfeltet må ha en godkjent e-mail
</ErrorSummary.Item>
</ErrorSummary>
);
export const Small: Story = {
render: () => (
<ErrorSummary heading="Feiloppsummering komponent" size="small">
<ErrorSummary.Item href="#1">Checkbox må fylles ut</ErrorSummary.Item>
<ErrorSummary.Item href="#2">
Tekstfeltet må ha en godkjent e-mail
</ErrorSummary.Item>
</ErrorSummary>
),
};
export const Chromatic: Story = {
render: () => (
<VStack gap="4">
<div>
<h2>Default</h2>
<ErrorSummary heading="Feiloppsummering komponent">
<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>
<div>
<h2>Small</h2>
<ErrorSummary heading="Feiloppsummering komponent" size="small">
<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>
</VStack>
),
parameters: {
chromatic: { disable: false },
},
};
@@ -1,5 +1,6 @@
import { Meta } from "@storybook/react";
import { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { VStack } from "../../layout/stack";
import TextField from "../textfield/TextField";
import { Fieldset } from "./index";
@@ -26,7 +27,12 @@ export default {
type: "boolean",
},
},
} as Meta;
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof Fieldset>;
type Story = StoryObj<typeof Fieldset>;
const content = (
<>
@@ -35,88 +41,117 @@ const content = (
</>
);
export const Default = {
render: (props) => {
return (
<Fieldset legend="Mollit eiusmod" {...props}>
{content}
</Fieldset>
);
},
const contentWithError = (
<>
<TextField label="Textfield label" hideLabel error="Må være fylt ut" />
<TextField label="Textfield label" hideLabel />
</>
);
export const Default: Story = {
args: {
legend: "Mollit eiusmod",
description:
"Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure.",
errorPropagation: true,
children: content,
},
};
export const Small = () => (
<Fieldset
size="small"
legend="Mollit eiusmod"
description="Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure."
>
{content}
</Fieldset>
);
export const Small: Story = {
args: {
legend: "Mollit eiusmod",
description:
"Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure.",
errorPropagation: true,
children: content,
size: "small",
},
};
export const Description = () => (
<Fieldset
legend="Mollit eiusmod"
description="Esse cupidatat reprehenderit est culpa consectetur sit dolor esse."
>
<TextField
label="Textfield label"
description="Amet quis cillum incididunt "
/>
<TextField
label="Textfield label"
description="Enim et occaecat voluptate labore sit do exercitation laborum non "
/>
</Fieldset>
);
export const ErrorPropagation: Story = {
args: {
legend: "Mollit eiusmod",
description:
"Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure.",
errorPropagation: false,
children: contentWithError,
},
};
export const ErrorPropagation = () => (
<Fieldset
legend="Mollit eiusmod"
error="Fieldsett error"
errorPropagation={false}
>
<TextField label="Textfield label" hideLabel error="Må være fylt ut" />
<TextField label="Textfield label" hideLabel />
</Fieldset>
);
export const Error: Story = {
args: {
legend: "Mollit eiusmod",
description:
"Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure.",
children: content,
error: "Laborum officia nisi aliqua esse minim in amet.",
},
};
export const Error = () => (
<div className="colgap">
<Fieldset
legend="Mollit eiusmod"
error="Laborum officia nisi aliqua esse minim in amet."
>
{content}
</Fieldset>
<Fieldset
size="small"
legend="Mollit eiusmod"
error="Laborum officia nisi aliqua esse minim in amet."
>
{content}
</Fieldset>
</div>
);
export const Disabled: Story = {
args: {
legend: "Mollit eiusmod",
description:
"Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure.",
children: content,
disabled: true,
},
};
export const Disabled = () => (
<div className="colgap">
<Fieldset legend="Mollit eiusmod" disabled>
{content}
</Fieldset>
<Fieldset size="small" legend="Mollit eiusmod" disabled>
{content}
</Fieldset>
</div>
);
export const HideLegend: Story = {
args: {
legend: "Mollit eiusmod",
description:
"Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure.",
children: content,
hideLegend: true,
},
};
export const HideLegend = () => (
<Fieldset legend="Mollit eiusmod" hideLegend>
{content}
</Fieldset>
);
export const Chromatic: Story = {
render: () => {
return (
<VStack gap="4">
<div>
<h2>Default</h2>
{/* @ts-expect-error Args are Partial here */}
<Fieldset {...Default.args} />
</div>
<div>
<h2>Small</h2>
{/* @ts-expect-error Args are Partial here */}
<Fieldset {...Small.args} />
</div>
<div>
<h2>ErrorPropagation</h2>
{/* @ts-expect-error Args are Partial here */}
<Fieldset {...ErrorPropagation.args} />
</div>
<div>
<h2>Error</h2>
{/* @ts-expect-error Args are Partial here */}
<Fieldset {...Error.args} />
</div>
<div>
<h2>Error small</h2>
{/* @ts-expect-error Args are Partial here */}
<Fieldset {...Error.args} size="small" />
</div>
<div>
<h2>Disabled</h2>
{/* @ts-expect-error Args are Partial here */}
<Fieldset {...Disabled.args} />
</div>
<div>
<h2>HideLegend</h2>
{/* @ts-expect-error Args are Partial here */}
<Fieldset {...HideLegend.args} />
</div>
</VStack>
);
},
parameters: {
chromatic: { disable: false },
},
};
@@ -89,8 +89,8 @@ interface FileUploadComponent
* ```jsx
* <FileUpload.Item file={file} status="uploading" />
* <FileUpload.Item file={file} status="downloading" />
* <FileUpload.Item file={file} itemAction="retry" onRetry={...} />
* <FileUpload.Item file={file} itemAction="delete" onDelete={...} />
* <FileUpload.Item file={file} button={{ action:"retry", onClick:... }} />
* <FileUpload.Item file={file} button={{ action:"delete", onClick:... }} />
* ```
*/
Item: typeof Item;
@@ -57,51 +57,63 @@ export const States: StoryObj<typeof FileUpload.Item> = {
file={fileTxt}
error="Error og uploading"
status="uploading"
onRetry={() => onRetry(fileTxt)}
onDelete={() => onDelete(fileTxt)}
button={{
onClick: () => onDelete(fileTxt),
action: "delete",
}}
/>
<h2>Item Actions</h2>
<h3>status + delete</h3>
<FileUpload.Item
file={fileDocx}
onDelete={() => onDelete(fileDocx)}
onRetry={() => onRetry(fileDocx)}
status="uploading"
itemAction="delete"
button={{
onClick: () => onDelete(fileDocx),
action: "delete",
}}
/>
<h3>status + retry</h3>
<FileUpload.Item
file={fileDocx}
onDelete={() => onDelete(fileDocx)}
onRetry={() => onRetry(fileDocx)}
status="downloading"
itemAction="retry"
button={{
onClick: () => onRetry(fileDocx),
action: "retry",
}}
/>
<h3>retry</h3>
<FileUpload.Item
file={fileCsv}
onRetry={() => onRetry(fileCsv)}
itemAction="retry"
button={{
onClick: () => onRetry(fileCsv),
action: "retry",
}}
/>
<h3>delete</h3>
<FileUpload.Item
file={filePptx}
onDelete={() => onDelete(filePptx)}
itemAction="delete"
button={{
onClick: () => onDelete(filePptx),
action: "delete",
}}
/>
<h3>retry + error</h3>
<FileUpload.Item
file={fileCsv}
error="Error og onRetry"
onRetry={() => onRetry(fileCsv)}
itemAction="retry"
button={{
onClick: () => onRetry(fileCsv),
action: "retry",
}}
/>
<h3>delete + error</h3>
<FileUpload.Item
file={filePptx}
error="Error og onDelete"
onDelete={() => onDelete(filePptx)}
itemAction="delete"
button={{
onClick: () => onDelete(filePptx),
action: "delete",
}}
/>
</div>
),
@@ -25,6 +25,7 @@ const MAX_SIZE = MAX_SIZE_MB * 1024 * 1024;
const CustomItem = ({
index,
onDelete,
...props
}: FileUploadItemProps & {
index: number;
@@ -45,7 +46,10 @@ const CustomItem = ({
<FileUpload.Item
{...props}
status={loading ? "uploading" : "idle"}
itemAction="delete"
button={{
action: "delete",
onClick: onDelete,
}}
as="li"
/>
);
@@ -165,7 +169,10 @@ export const Single: StoryFn = () => {
key={file.file.name}
file={file.file}
error={file.error ? errors[file.reasons[0]] : undefined}
onDelete={removeFile}
button={{
action: "delete",
onClick: removeFile,
}}
/>
))}
</VStack>
@@ -194,12 +201,17 @@ export const Translation = () => (
<FileUpload.Dropzone label="Last opp bilder" onSelect={console.log} />
<FileUpload.Item
file={{ name: "eksempel.png", size: 200000 }}
onDelete={() => null}
button={{
action: "delete",
onClick: () => null,
}}
/>
<FileUpload.Item
file={{ name: "eksempel.png", size: 200000 }}
itemAction="retry"
onRetry={() => null}
button={{
action: "retry",
onClick: () => null,
}}
/>
<FileUpload.Item
file={{ name: "eksempel.png", size: 200000 }}
@@ -30,6 +30,7 @@ const Dropzone = forwardRef<HTMLInputElement, FileUploadDropzoneProps>(
disabled,
translations,
onClick,
id: buttonId,
...rest
} = props;
@@ -46,7 +47,10 @@ const Dropzone = forwardRef<HTMLInputElement, FileUploadDropzoneProps>(
const _disabled = disabled ?? fileLimitReached;
const { inputProps, errorId, showErrorMsg, hasError, inputDescriptionId } =
useFormField({ ...props, disabled: _disabled }, "fileUpload");
useFormField(
{ ...omit(props, ["id"]), disabled: _disabled },
"fileUpload",
);
const {
id: inputId,
"aria-describedby": ariaDescribedby,
@@ -131,6 +135,7 @@ const Dropzone = forwardRef<HTMLInputElement, FileUploadDropzoneProps>(
<Button
{...omit(rest, ["errorId"])}
{...inputPropsRest}
id={buttonId}
aria-describedby={cl(labelId, ariaDescribedby)}
className="navds-dropzone__area-button"
type="button"
@@ -12,12 +12,13 @@ import ItemIcon from "./ItemIcon";
import ItemName from "./ItemName";
import { formatFileSize } from "./utils/format-file-size";
export interface FileItemBaseProps {
export interface FileUploadItemProps
extends React.HTMLAttributes<HTMLDivElement> {
/**
* Overrides html-tag
* @default "div"
*/
as?: "div" | "li";
as?: ("div" | "li") & React.ElementType;
/**
* Either a native File or file metadata.
*/
@@ -43,37 +44,20 @@ export interface FileItemBaseProps {
* @default "idle"
*/
status?: "downloading" | "uploading" | "idle";
/**
* Props for the action button.
*/
button?: {
action: "delete" | "retry";
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
id?: string;
};
/**
* i18n-API for customizing texts and labels
*/
translations?: ComponentTranslation<"FileUpload">["item"];
onRetry?: (event: MouseEvent<HTMLButtonElement>) => void;
onDelete?: (event: MouseEvent<HTMLButtonElement>) => void;
}
type FileItemActionDelete = {
onDelete: (event: MouseEvent<HTMLButtonElement>) => void;
itemAction: "delete";
};
type FileItemActionRetry = {
onRetry: (event: MouseEvent<HTMLButtonElement>) => void;
itemAction: "retry";
};
type FileItemActionNone = {
itemAction?: "none";
};
type FileItemConditionalProps =
| FileItemActionDelete
| FileItemActionRetry
| FileItemActionNone;
export type FileUploadItemProps = FileItemBaseProps &
FileItemConditionalProps &
React.HTMLAttributes<HTMLDivElement>;
export const Item: OverridableComponent<FileUploadItemProps, HTMLDivElement> =
forwardRef(
(
@@ -81,16 +65,14 @@ export const Item: OverridableComponent<FileUploadItemProps, HTMLDivElement> =
as: Component = "div",
file,
status = "idle",
onDelete,
onRetry,
error,
className,
href,
onFileClick,
itemAction = "delete",
button,
translations,
...rest
},
}: FileUploadItemProps,
ref,
) => {
const context = useFileUploadTranslation(false);
@@ -148,14 +130,14 @@ export const Item: OverridableComponent<FileUploadItemProps, HTMLDivElement> =
</div>
</div>
{status === "idle" && (
{status === "idle" && button && (
<ItemButton
file={file}
onRetry={onRetry}
onDelete={onDelete}
action={itemAction}
retryTitle={translate("item.retryButtonTitle")}
deleteTitle={translate("item.deleteButtonTitle")}
{...button}
title={translate(
button.action === "retry"
? "item.retryButtonTitle"
: "item.deleteButtonTitle",
)}
/>
)}
</div>
@@ -1,52 +1,27 @@
import React from "react";
import { ArrowsCirclepathIcon, TrashIcon } from "@navikt/aksel-icons";
import { Button } from "../../../../button";
import { FileItem } from "./Item.types";
interface Props {
file: FileItem;
onRetry?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onDelete?: (event: React.MouseEvent<HTMLButtonElement>) => void;
action: "delete" | "retry" | "none";
retryTitle: string;
deleteTitle: string;
action: "delete" | "retry";
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
id?: string;
title: string;
}
const ItemButton = ({
onRetry,
onDelete,
action,
retryTitle,
deleteTitle,
}: Props) => {
if (action === "none") {
return null;
}
const ItemButton = ({ action, onClick, id, title }: Props) => {
const Icon = action === "delete" ? TrashIcon : ArrowsCirclepathIcon;
if (onRetry && action === "retry") {
return (
<Button
className="navds-file-item__button"
type="button"
variant="tertiary-neutral"
onClick={onRetry}
icon={<ArrowsCirclepathIcon title={retryTitle} />}
/>
);
}
if (onDelete && action === "delete") {
return (
<Button
className="navds-file-item__button"
type="button"
variant="tertiary-neutral"
onClick={onDelete}
icon={<TrashIcon title={deleteTitle} />}
/>
);
}
return null;
return (
<Button
id={id}
className="navds-file-item__button"
type="button"
variant="tertiary-neutral"
onClick={onClick}
icon={<Icon title={title} />}
/>
);
};
export default ItemButton;
@@ -18,7 +18,7 @@ export const RadioGroupContext =
export interface RadioGroupProps
extends Omit<
FieldsetProps,
"onChange" | "errorPropagation" | "defaultValue"
"onChange" | "errorPropagation" | "defaultValue" | "nativeReadOnly"
> {
/**
* Collection of `<Radio />`-elements
@@ -49,6 +49,7 @@ export const Default = (props) => {
</RadioGroup>
);
};
Default.args = {
controlled: false,
error: false,
+2 -2
View File
@@ -9,11 +9,11 @@ export interface RadioProps
*/
children: React.ReactNode;
/**
* The value of the HTML element.
* Radio value.
*/
value: any;
/**
* Adds a description to extend labeling of Radio.
* Adds a description to extend the labeling.
*/
description?: string;
}
@@ -85,7 +85,7 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
...rest
} = props;
const [checked, setChecked] = useState(
const [_checked, setChecked] = useState(
defaultChecked ?? checkedProp ?? false,
);
@@ -93,6 +93,8 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
checkedProp !== undefined && setChecked(checkedProp);
}, [checkedProp]);
const checked = checkedProp ?? _checked;
return (
<div
className={cl(
@@ -1,5 +1,6 @@
import { Meta } from "@storybook/react";
import { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { VStack } from "../../layout/stack";
import Switch from "./Switch";
export default {
@@ -31,70 +32,59 @@ export default {
type: "boolean",
},
},
} as Meta;
export const Default = {
render: (props) => {
return <Switch {...props}>Label text</Switch>;
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof Switch>;
type Story = StoryObj<typeof Switch>;
export const Default: Story = {
args: {
position: "right",
children: "Label text",
},
};
export const Small = () => {
return (
<div className="colgap">
<Switch size="small">Label text</Switch>
<Switch size="small" position="right">
Label text
</Switch>
</div>
);
export const Small: Story = {
args: {
children: "Label text",
size: "small",
},
};
export const Description = () => {
return (
<div className="colgap">
<Switch size="small" description="Cillum sint exercitation ut cillum.">
Label text
</Switch>
<Switch
size="small"
position="right"
description="Cillum sint exercitation ut cillum."
>
Label text
</Switch>
</div>
);
export const Description: Story = {
args: {
children: "Label text",
description: "Cillum sint exercitation ut cillum.",
},
};
export const Loading = () => {
return (
<div className="colgap">
export const Loading: Story = {
render: () => {
return (
<div className="colgap">
<Switch loading>Label text</Switch>
<div className="colgap">
<Switch loading>Label text</Switch>
<Switch checked loading>
Label text
</Switch>
<Switch checked loading>
Label text
</Switch>
</div>
<div className="colgap">
<Switch loading size="small">
Label text
</Switch>
<Switch checked loading size="small">
Label text
</Switch>
</div>
</div>
<div className="colgap">
<Switch loading size="small">
Label text
</Switch>
<Switch checked loading size="small">
Label text
</Switch>
</div>
</div>
);
);
},
};
export const Disabled = () => {
return (
export const Disabled: Story = {
render: () => (
<div className="colgap">
<Switch disabled>Label text</Switch>
@@ -102,11 +92,11 @@ export const Disabled = () => {
Label text
</Switch>
</div>
);
),
};
export const HideLabel = () => {
return (
export const HideLabel: Story = {
render: () => (
<div className="colgap">
<Switch hideLabel>Label text</Switch>
@@ -114,11 +104,11 @@ export const HideLabel = () => {
Label text
</Switch>
</div>
);
),
};
export const Readonly = () => {
return (
export const Readonly: Story = {
render: () => (
<div className="colgap">
<Switch description="Slår av alle notifikasjoner" readOnly>
Notifikasjoner
@@ -128,5 +118,54 @@ export const Readonly = () => {
Notifikasjoner
</Switch>
</div>
);
),
};
export const Chromatic: Story = {
render: () => (
<VStack gap="4" align="start">
<div>
<h2>Default</h2>
{/* @ts-expect-error Args are Partial here */}
<Switch {...Default.args} />
</div>
<div>
<h2>Small</h2>
{/* @ts-expect-error Args are Partial here */}
<Switch {...Small.args} position="left" />
{/* @ts-expect-error Args are Partial here */}
<Switch {...Small.args} position="right" />
</div>
<div>
<h2>Description</h2>
{/* @ts-expect-error Args are Partial here */}
<Switch {...Description.args} position="left" />
{/* @ts-expect-error Args are Partial here */}
<Switch {...Description.args} position="right" />
</div>
<div>
<h2>Loading</h2>
{/* @ts-expect-error Args are Partial here */}
<Loading.render />
</div>
<div>
<h2>Disabled</h2>
{/* @ts-expect-error Args are Partial here */}
<Disabled.render />
</div>
<div>
<h2>HideLabel</h2>
{/* @ts-expect-error Args are Partial here */}
<HideLabel.render />
</div>
<div>
<h2>Readonly</h2>
{/* @ts-expect-error Args are Partial here */}
<Readonly.render />
</div>
</VStack>
),
parameters: {
chromatic: { disable: false },
},
};
+2 -3
View File
@@ -5,8 +5,7 @@ import { FieldsetContext } from "./fieldset/context";
export interface FormFieldProps {
/**
* Adds a description to extend labeling of the field.
* Error message for element.
* Error message.
*/
error?: React.ReactNode;
/**
@@ -24,7 +23,7 @@ export interface FormFieldProps {
*/
disabled?: boolean;
/**
* Adds a description to extend labeling of a field.
* Adds a description to extend the labeling.
*/
description?: React.ReactNode;
/**
@@ -41,16 +41,14 @@ export const PosterVariants = {
render: () => (
<VStack gap="6" align="start">
<GuidePanel>
If you exclude the <code>poster</code> prop, you will get the poster
variant on mobile (&lt;480px) and the non-poster variant otherwise.
If you exclude the `poster` prop, you will get the poster variant on
mobile (&lt;480px) and the non-poster variant otherwise.
</GuidePanel>
<GuidePanel poster>
Use the <code>poster</code> prop to get the poster variant on all
viewports.
Use the `poster` prop to get the poster variant on all viewports.
</GuidePanel>
<GuidePanel poster={false}>
Set <code>poster=false</code> to get the non-poster variant on all
viewports.
Set `poster=false` to get the non-poster variant on all viewports.
</GuidePanel>
</VStack>
),
+5 -1
View File
@@ -238,7 +238,11 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
level="1"
id={ariaLabelId}
>
<span className="navds-modal__header-icon">{header.icon}</span>
{header.icon && (
<span className="navds-modal__header-icon">
{header.icon}
</span>
)}
{header.heading}
</Heading>
</ModalHeader>
@@ -146,6 +146,7 @@ export const WithUseState: StoryFn = () => {
);
};
WithUseState.storyName = "With useState";
WithUseState.parameters = { chromatic: { disable: true } };
export const EmptyHeader: StoryFn = () => (
<Modal open onClose={() => null} aria-label="Modal with empty header">
@@ -225,6 +226,7 @@ export const WithTooltip: StoryFn = () => {
</div>
);
};
WithTooltip.parameters = { chromatic: { disable: true } }; // The tooltip behaves unpredictably in Chromatic
export const WithSrOnlyElement: StoryFn = () => (
<Modal
@@ -330,12 +332,6 @@ ChromaticViewportTesting.parameters = {
height: 400,
},
},
desktop: {
viewport: {
width: 1280,
height: 960,
},
},
},
},
};
@@ -1,65 +1,91 @@
import { Meta, StoryFn } from "@storybook/react";
import React, { useState } from "react";
import { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import React from "react";
import { ReadMore } from ".";
import { VStack } from "../layout/stack";
const meta: Meta<typeof ReadMore> = {
export default {
title: "ds-react/ReadMore",
component: ReadMore,
};
export default meta;
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof ReadMore>;
export const Default: StoryFn<{
controlled: boolean;
size: "medium" | "small";
defaultOpen: boolean;
}> = (props) => {
const [state, setState] = useState(false);
type Story = StoryObj<typeof ReadMore>;
return (
<ReadMore
open={props.controlled ? state : undefined}
onClick={() => setState((x) => !x)}
header="Grunnen til at vi spør om dette og i tillegg ber om vedlegg"
size={props.size}
onOpenChange={console.log}
defaultOpen={props.defaultOpen}
>
<div style={{ maxWidth: 300 }}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Mollitia,
tempore corporis exercitationem minus dignissimos eius aspernatur fugiat
iusto.
</div>
</ReadMore>
);
};
Default.args = {
controlled: false,
size: "medium",
defaultOpen: false,
};
Default.argTypes = {
size: {
options: ["medium", "small"],
control: { type: "radio" },
const Content = (
<div style={{ maxWidth: 300 }}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Mollitia, tempore
corporis exercitationem minus dignissimos eius aspernatur fugiat iusto.
</div>
);
export const Default: Story = {
args: {
size: "medium",
defaultOpen: false,
onOpenChange: fn(),
children: Content,
header: "Grunnen til at vi spør om dette og i tillegg ber om vedlegg",
},
argTypes: {
size: {
options: ["medium", "small"],
control: { type: "radio" },
},
},
};
export const Small = () => (
<ReadMore
header="Grunnen til at vi spør om dette og i tillegg ber om vedlegg"
size="small"
>
Command station, this is ST 321. Code Clearance Blue. We&apos;re starting
our approach. Deactivate the security shield.
</ReadMore>
);
export const Small: Story = {
args: {
...Default.args,
size: "small",
},
};
export const Open = () => (
<ReadMore
header="Grunnen til at vi spør om dette og i tillegg ber om vedlegg"
defaultOpen
>
Command station, this is ST 321. Code Clearance Blue. We&apos;re starting
our approach. Deactivate the security shield.
</ReadMore>
);
export const DefaultOpen: Story = {
args: {
...Default.args,
defaultOpen: true,
},
};
export const Open: Story = {
args: {
...Default.args,
open: true,
},
};
export const Chromatic: Story = {
render: () => {
return (
<VStack gap="4">
<div>
<h2>Default</h2>
{/* @ts-expect-error Args are partial, leading to required prop mismatch */}
<ReadMore {...Default.args} />
</div>
<div>
<h2>Small</h2>
{/* @ts-expect-error Args are partial, leading to required prop mismatch */}
<ReadMore {...Small.args} />
</div>
<div>
<h2>DefaultOpen</h2>
{/* @ts-expect-error Args are partial, leading to required prop mismatch */}
<ReadMore {...DefaultOpen.args} />
</div>
<div>
<h2>Controlled open</h2>
{/* @ts-expect-error Args are partial, leading to required prop mismatch */}
<ReadMore {...Open.args} />
</div>
</VStack>
);
},
parameters: {
chromatic: { disable: false },
},
};
-66
View File
@@ -1,66 +0,0 @@
import * as RadixTabs from "@radix-ui/react-tabs";
import cl from "clsx";
import React, { forwardRef, useContext } from "react";
import { BodyShort } from "../typography";
import { OverridableComponent } from "../util/types";
import { TabsContext } from "./context";
export interface TabProps
extends Omit<React.HTMLAttributes<HTMLButtonElement>, "children"> {
/**
* Tab label
*/
label?: React.ReactNode;
/**
* Tab Icon
*/
icon?: React.ReactNode;
/**
* Value for state-handling
*/
value: string;
}
export const Tab: OverridableComponent<TabProps, HTMLButtonElement> =
forwardRef(
(
{ className, as: Component = "button", label, icon, value, ...rest },
ref,
) => {
const context = useContext(TabsContext);
if (!label && !icon) {
console.error("<Tabs.Tab/> needs label/icon");
return null;
}
return (
<RadixTabs.Trigger value={value} asChild>
<Component
ref={ref}
className={cl(
"navds-tabs__tab",
`navds-tabs__tab--${context?.size ?? "medium"}`,
`navds-tabs__tab-icon--${context?.iconPosition}`,
className,
{
"navds-tabs__tab--icon-only": icon && !label,
},
)}
{...rest}
>
<BodyShort
as="span"
className="navds-tabs__tab-inner"
size={context?.size}
>
{icon}
{label}
</BodyShort>
</Component>
</RadixTabs.Trigger>
);
},
);
export default Tab;
-128
View File
@@ -1,128 +0,0 @@
import * as RadixTabs from "@radix-ui/react-tabs";
import cl from "clsx";
import React, {
forwardRef,
useContext,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { ChevronLeftIcon, ChevronRightIcon } from "@navikt/aksel-icons";
import { debounce } from "../util";
import { useMergeRefs } from "../util/hooks/useMergeRefs";
import { TabsContext } from "./context";
export interface TabListProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* `<Tabs.Tab />` elements
*/
children: React.ReactNode;
}
export const TabList = forwardRef<HTMLDivElement, TabListProps>(
({ className, ...rest }, ref) => {
const context = useContext(TabsContext);
const listRef = useRef<HTMLDivElement | null>(null);
const mergedRef = useMergeRefs(listRef, ref);
const [displayScroll, setDisplayScroll] = useState({
start: false,
end: false,
});
const updateScrollButtonState = useMemo(
() =>
debounce(() => {
if (!listRef?.current) return;
const { scrollWidth, clientWidth } = listRef.current;
const scrollLeft = listRef.current.scrollLeft;
// use 1 for the potential rounding error with browser zooms.
const showStartScroll = scrollLeft > 1;
const showEndScroll = scrollLeft < scrollWidth - clientWidth - 1;
setDisplayScroll((oldDisplayScroll) =>
showStartScroll === oldDisplayScroll.start &&
showEndScroll === oldDisplayScroll.end
? oldDisplayScroll
: { start: showStartScroll, end: showEndScroll },
);
}),
[],
);
useEffect(() => {
const handleResize = () => updateScrollButtonState();
const win = listRef.current?.ownerDocument ?? document ?? window;
win.addEventListener("resize", handleResize);
let resizeObserver;
if (typeof ResizeObserver !== "undefined") {
resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(listRef.current);
}
return () => {
win.removeEventListener("resize", handleResize);
if (resizeObserver) {
resizeObserver.disconnect();
}
};
}, [updateScrollButtonState]);
useEffect(() => {
updateScrollButtonState();
});
useEffect(() => {
return () => {
updateScrollButtonState.clear();
};
}, [updateScrollButtonState]);
const ScrollButton = ({
dir,
hidden,
}: {
dir: 1 | -1;
hidden: boolean;
}) => (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
<div
className={cl("navds-tabs__scroll-button", {
"navds-tabs__scroll-button--hidden": hidden,
})}
onClick={() => {
if (!listRef.current) return;
listRef.current.scrollLeft += dir * 100;
}}
>
{dir === -1 ? (
<ChevronLeftIcon title="scroll tilbake" />
) : (
<ChevronRightIcon title="scroll neste" />
)}
</div>
);
const showSteppers = displayScroll.end || displayScroll.start;
return (
<div className="navds-tabs__tablist-wrapper">
{showSteppers && (
<ScrollButton dir={-1} hidden={!displayScroll.start} />
)}
<RadixTabs.TabsList
{...rest}
ref={mergedRef}
onScroll={updateScrollButtonState}
loop={context?.loop}
className={cl("navds-tabs__tablist", className)}
/>
{showSteppers && <ScrollButton dir={1} hidden={!displayScroll.end} />}
</div>
);
},
);
export default TabList;
-26
View File
@@ -1,26 +0,0 @@
import * as RadixTabs from "@radix-ui/react-tabs";
import cl from "clsx";
import React, { forwardRef } from "react";
export interface TabPanelProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Tab panel content
*/
children: React.ReactNode;
/**
* Value for state-handling
*/
value: string;
}
const TabPanel = forwardRef<HTMLDivElement, TabPanelProps>(
({ className, ...rest }, ref) => (
<RadixTabs.TabsContent
{...rest}
ref={ref}
className={cl("navds-tabs__tabpanel", className)}
/>
),
);
export default TabPanel;
@@ -0,0 +1,24 @@
import { createContext } from "../util/create-context";
import { createDescendantContext } from "../util/hooks/descendants/useDescendant";
import { TabsProps } from "./Tabs.types";
import { useTabs } from "./useTabs";
export const [
TabsDescendantsProvider,
useTabsDescendantsContext,
useTabsDescendants,
useTabsDescendant,
] = createDescendantContext<HTMLButtonElement, { value: string }>();
type TabsProviderProps = ReturnType<typeof useTabs> &
Pick<
TabsProps,
"selectionFollowsFocus" | "loop" | "size" | "iconPosition" | "fill"
>;
/* State context */
export const [TabsProvider, useTabsContext] = createContext<TabsProviderProps>({
name: "TabsContext",
hookName: "useTabsContext",
providerName: "TabsProvider",
});
+236 -116
View File
@@ -1,7 +1,8 @@
import { Meta } from "@storybook/react";
import { Meta, StoryObj } from "@storybook/react";
import React, { useState } from "react";
import { DishwasherIcon, FreezerIcon, MugIcon } from "@navikt/aksel-icons";
import { Tabs } from ".";
import { VStack } from "../layout/stack";
export default {
title: "ds-react/Tabs",
@@ -20,41 +21,44 @@ export default {
},
},
},
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof Tabs>;
const Panel = () => {
return (
<>
<Tabs.Panel
value="test1"
className="panel"
style={{ background: "var(--a-gray-50)", height: 100 }}
>
Innholdspanel for Skap-tab
</Tabs.Panel>
<Tabs.Panel
value="test2"
className="panel"
style={{
background: "var(--a-green-50)",
height: 100,
}}
>
Innholdspanel for Oppvaskmaskin-tab
</Tabs.Panel>
<Tabs.Panel
value="test3"
className="panel"
style={{ background: "var(--a-red-50)", height: 100 }}
>
Innholdspanel for Fryser-tab
</Tabs.Panel>
<style>{`.panel[data-state="active"] { display:grid; place-content: center;}`}</style>
</>
);
};
type Story = StoryObj<typeof Tabs>;
export const Default = {
const Panel = () => (
<>
<Tabs.Panel
value="test1"
className="panel"
style={{ background: "var(--a-gray-50)", height: 100 }}
>
Innholdspanel for Skap-tab
</Tabs.Panel>
<Tabs.Panel
value="test2"
className="panel"
style={{
background: "var(--a-green-50)",
height: 100,
}}
>
Innholdspanel for Oppvaskmaskin-tab
</Tabs.Panel>
<Tabs.Panel
value="test3"
className="panel"
style={{ background: "var(--a-red-50)", height: 100 }}
>
Innholdspanel for Fryser-tab
</Tabs.Panel>
<style>{`.panel[data-state="active"] { display:grid; place-content: center;}`}</style>
</>
);
export const Default: Story = {
render: (props) => {
return (
<Tabs
@@ -65,13 +69,17 @@ export const Default = {
iconPosition={props?.iconPosition}
>
<Tabs.List>
<Tabs.Tab value="test1" icon={<MugIcon />} label="Skap" />
<Tabs.Tab value="test1" icon={<MugIcon aria-hidden />} label="Skap" />
<Tabs.Tab
value="test2"
label="Oppvaskmaskin"
icon={<DishwasherIcon />}
icon={<DishwasherIcon aria-hidden />}
/>
<Tabs.Tab
value="test3"
icon={<FreezerIcon aria-hidden />}
label="Fryser"
/>
<Tabs.Tab value="test3" icon={<FreezerIcon />} label="Fryser" />
</Tabs.List>
<Panel />
</Tabs>
@@ -84,108 +92,220 @@ export const Default = {
},
};
export const Small = () => {
return (
<Tabs defaultValue="test2" size="small">
<Tabs.List>
<Tabs.Tab value="test1" icon={<MugIcon />} label="Skap" />
<Tabs.Tab
value="test2"
label="Oppvaskmaskin"
icon={<DishwasherIcon />}
/>
<Tabs.Tab value="test3" icon={<FreezerIcon />} label="Fryser" />
</Tabs.List>
<Panel />
</Tabs>
);
};
export const Small = () => (
<Tabs defaultValue="test2" size="small">
<Tabs.List>
<Tabs.Tab value="test1" icon={<MugIcon aria-hidden />} label="Skap" />
<Tabs.Tab
value="test2"
label="Oppvaskmaskin"
icon={<DishwasherIcon aria-hidden />}
/>
<Tabs.Tab
value="test3"
icon={<FreezerIcon aria-hidden />}
label="Fryser"
/>
</Tabs.List>
<Panel />
</Tabs>
);
export const Controlled = () => {
const [activeValue, setActiveValue] = useState("test1");
return (
<Tabs value={activeValue} onChange={setActiveValue}>
<Tabs.List>
<Tabs.Tab value="test1" icon={<MugIcon />} label="Skap" />
<Tabs.Tab value="test1" icon={<MugIcon aria-hidden />} label="Skap" />
<Tabs.Tab
value="test2"
label="Oppvaskmaskin"
icon={<DishwasherIcon />}
icon={<DishwasherIcon aria-hidden />}
/>
<Tabs.Tab
value="test3"
icon={<FreezerIcon aria-hidden />}
label="Fryser"
/>
<Tabs.Tab value="test3" icon={<FreezerIcon />} label="Fryser" />
</Tabs.List>
<Panel />
</Tabs>
);
};
export const IconPosition = () => {
return (
<div className="colgap">
<Tabs defaultValue="test2" size="small">
<Tabs.List>
<Tabs.Tab value="test1" icon={<MugIcon />} label="Skap" />
<Tabs.Tab
value="test2"
label="Oppvaskmaskin"
icon={<DishwasherIcon />}
/>
<Tabs.Tab value="test3" icon={<FreezerIcon />} label="Fryser" />
</Tabs.List>
<Panel />
</Tabs>
<Tabs defaultValue="test2" size="small" iconPosition="top">
<Tabs.List style={{ margin: "0 auto" }}>
<Tabs.Tab value="test1" icon={<MugIcon />} label="Skap" />
<Tabs.Tab
value="test2"
label="Oppvaskmaskin"
icon={<DishwasherIcon />}
/>
<Tabs.Tab value="test3" icon={<FreezerIcon />} label="Fryser" />
</Tabs.List>
<Panel />
</Tabs>
</div>
);
};
export const Icon = () => {
return (
<div className="colgap">
<Tabs defaultValue="test2">
<Tabs.List style={{ margin: "0 auto" }}>
<Tabs.Tab value="test1" icon={<MugIcon />} />
<Tabs.Tab value="test2" icon={<DishwasherIcon />} />
<Tabs.Tab value="test3" icon={<FreezerIcon />} />
</Tabs.List>
<Panel />
</Tabs>
<Tabs defaultValue="test2" size="small" iconPosition="top">
<Tabs.List style={{ margin: "0 auto" }}>
<Tabs.Tab value="test1" icon={<MugIcon />} />
<Tabs.Tab value="test2" icon={<DishwasherIcon />} />
<Tabs.Tab value="test3" icon={<FreezerIcon />} />
</Tabs.List>
<Panel />
</Tabs>
</div>
);
};
export const Overflow = () => {
return (
<Tabs defaultValue="test2" style={{ maxWidth: 300 }}>
export const IconPosition = () => (
<VStack gap="4">
<Tabs defaultValue="test2" size="small">
<Tabs.List>
<Tabs.Tab value="test1" icon={<MugIcon />} label="Skap" />
<Tabs.Tab value="test1" icon={<MugIcon aria-hidden />} label="Skap" />
<Tabs.Tab
value="test2"
label="Oppvaskmaskin"
icon={<DishwasherIcon />}
icon={<DishwasherIcon aria-hidden />}
/>
<Tabs.Tab
value="test3"
icon={<FreezerIcon aria-hidden />}
label="Fryser"
/>
<Tabs.Tab value="test3" icon={<FreezerIcon />} label="Fryser" />
</Tabs.List>
<Panel />
</Tabs>
);
<Tabs defaultValue="test2" size="small" iconPosition="top">
<Tabs.List style={{ margin: "0 auto" }}>
<Tabs.Tab value="test1" icon={<MugIcon aria-hidden />} label="Skap" />
<Tabs.Tab
value="test2"
label="Oppvaskmaskin"
icon={<DishwasherIcon aria-hidden />}
/>
<Tabs.Tab
value="test3"
icon={<FreezerIcon aria-hidden />}
label="Fryser"
/>
</Tabs.List>
<Panel />
</Tabs>
</VStack>
);
export const Icon = () => (
<VStack gap="4">
<Tabs defaultValue="test2">
<Tabs.List style={{ margin: "0 auto" }}>
<Tabs.Tab value="test1" icon={<MugIcon title="Skap" />} />
<Tabs.Tab
value="test2"
icon={<DishwasherIcon title="Oppvaskmaskin" />}
/>
<Tabs.Tab value="test3" icon={<FreezerIcon title="Fryser" />} />
</Tabs.List>
<Panel />
</Tabs>
<Tabs defaultValue="test2" size="small" iconPosition="top">
<Tabs.List style={{ margin: "0 auto" }}>
<Tabs.Tab value="test1" icon={<MugIcon title="Skap" />} />
<Tabs.Tab
value="test2"
icon={<DishwasherIcon title="Oppvaskmaskin" />}
/>
<Tabs.Tab value="test3" icon={<FreezerIcon title="Fryser" />} />
</Tabs.List>
<Panel />
</Tabs>
</VStack>
);
export const Overflow = () => (
<Tabs defaultValue="test2" style={{ maxWidth: 300 }}>
<Tabs.List>
<Tabs.Tab value="test1" icon={<MugIcon aria-hidden />} label="Skap" />
<Tabs.Tab
value="test2"
label="Oppvaskmaskin"
icon={<DishwasherIcon aria-hidden />}
/>
<Tabs.Tab
value="test3"
icon={<FreezerIcon aria-hidden />}
label="Fryser"
/>
</Tabs.List>
<Panel />
</Tabs>
);
export const Fill = () => (
<Tabs defaultValue="test2" fill>
<Tabs.List>
<Tabs.Tab value="test1" icon={<MugIcon aria-hidden />} label="Skap" />
<Tabs.Tab
value="test2"
icon={<DishwasherIcon aria-hidden />}
label="Oppvaskmaskin"
/>
<Tabs.Tab
value="test3"
icon={<FreezerIcon aria-hidden />}
label="Fryser"
/>
</Tabs.List>
<Panel />
</Tabs>
);
Fill.parameters = {
layout: "fullscreen",
};
export const CustomIds = () => (
<Tabs defaultValue="test2">
<Tabs.List>
<Tabs.Tab
value="test1"
label="Skap"
id="custom-tabid-1"
aria-controls="custom-tabpanelid-1"
/>
<Tabs.Tab
value="test2"
label="Oppvaskmaskin"
id="custom-tabid-2"
aria-controls="custom-tabpanelid-2"
/>
</Tabs.List>
<Tabs.Panel
value="test1"
className="panel"
id="custom-tabpanelid-1"
aria-labelledby="custom-tabid-1"
>
Innholdspanel for Skap-tab
</Tabs.Panel>
<Tabs.Panel
value="test2"
className="panel"
id="custom-tabpanelid-2"
aria-labelledby="custom-tabid-2"
>
Innholdspanel for Oppvaskmaskin-tab
</Tabs.Panel>
</Tabs>
);
export const Chromatic = {
render: () => (
<VStack gap="6" align="center">
<div>
<h2>Small</h2>
<Small />
</div>
<div>
<h2>Controlled</h2>
<Controlled />
</div>
<div>
<h2>IconPosition</h2>
<IconPosition />
</div>
<div>
<h2>Icon</h2>
<Icon />
</div>
<div>
<h2>Overflow</h2>
<Overflow />
</div>
<div>
<h2>Fill</h2>
<div style={{ minWidth: 600 }}>
<Fill />
</div>
</div>
</VStack>
),
parameters: {
chromatic: { disable: false, delay: 300 },
},
};
+99 -37
View File
@@ -1,4 +1,5 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { act, fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { describe, expect, test } from "vitest";
import { Tabs } from "./Tabs";
@@ -39,72 +40,133 @@ const TestTabs = ({
describe("Tabs", () => {
test("sets default value correctly", () => {
render(<TestTabs defaultValue="tab2" />);
const tab = screen.getByTestId("tab2");
const panel = screen.getByTestId("tabpanel2");
const tab2 = screen.getByTestId("tab2");
const panel2 = screen.getByTestId("tabpanel2");
expect(tab).toHaveAttribute("aria-selected", "true");
expect(panel).toHaveTextContent("Tabpanel 2");
expect(tab2).toHaveAttribute("aria-selected", "true");
expect(panel2).toHaveTextContent("Tabpanel 2");
});
test("label-connection between tab and tabpanel is correct", async () => {
render(<TestTabs defaultValue="tab2" />);
const tab = screen.getByTestId("tab2");
const panel = screen.getByTestId("tabpanel2");
const tab2 = screen.getByTestId("tab2");
const panel2 = screen.getByTestId("tabpanel2");
const controlsId = tab.getAttribute("aria-controls");
const panelLabelledBy = panel.getAttribute("aria-labelledby");
const controlsId = tab2.getAttribute("aria-controls");
const panelLabelledBy = panel2.getAttribute("aria-labelledby");
expect(controlsId).toEqual(panel.id);
expect(tab.id).toEqual(panelLabelledBy);
expect(controlsId).toEqual(panel2.id);
expect(tab2.id).toEqual(panelLabelledBy);
});
test("sets correct attributes on active tab", () => {
render(<TestTabs defaultValue="tab2" />);
const tab = screen.getByTestId("tab2");
const tab2 = screen.getByTestId("tab2");
expect(tab).toHaveAttribute("aria-selected", "true");
expect(tab).toHaveAttribute("role", "tab");
expect(tab).toHaveAttribute("aria-controls");
expect(tab).toHaveAttribute("tabindex", "-1");
expect(tab2).toHaveAttribute("aria-selected", "true");
expect(tab2).toHaveAttribute("role", "tab");
expect(tab2).toHaveAttribute("aria-controls");
expect(tab2).toHaveAttribute("tabindex", "0");
});
test("sets correct attributes on idle tab", () => {
render(<TestTabs defaultValue="tab2" />);
const tab = screen.getByTestId("tab1");
const tab1 = screen.getByTestId("tab1");
expect(tab).toHaveAttribute("aria-selected", "false");
expect(tab).toHaveAttribute("role", "tab");
expect(tab).toHaveAttribute("aria-controls");
expect(tab).toHaveAttribute("tabindex", "-1");
expect(tab1).toHaveAttribute("aria-selected", "false");
expect(tab1).toHaveAttribute("role", "tab");
expect(tab1).toHaveAttribute("aria-controls");
expect(tab1).toHaveAttribute("tabindex", "-1");
});
test("sets correct attributes on active tabpanel", () => {
render(<TestTabs defaultValue="tab2" />);
const panel = screen.getByTestId("tabpanel2");
const panel2 = screen.getByTestId("tabpanel2");
expect(panel).toHaveAttribute("aria-labelledby");
expect(panel).toHaveAttribute("role", "tabpanel");
expect(panel).toHaveAttribute("tabindex", "0");
expect(panel).toHaveTextContent("Tabpanel 2");
expect(panel).toHaveStyle({ display: "block" });
expect(panel2).toHaveAttribute("aria-labelledby");
expect(panel2).toHaveAttribute("role", "tabpanel");
expect(panel2).toHaveAttribute("tabindex", "0");
expect(panel2).toHaveTextContent("Tabpanel 2");
expect(panel2).toHaveStyle({ display: "block" });
});
test("sets correct attributes on idle tabpanel", () => {
render(<TestTabs defaultValue="tab1" />);
const panel = screen.getByTestId("tabpanel2");
const panel2 = screen.getByTestId("tabpanel2");
expect(panel).toHaveAttribute("aria-labelledby");
expect(panel).toHaveAttribute("role", "tabpanel");
expect(panel).toHaveAttribute("tabindex", "0");
expect(panel).toBeEmptyDOMElement();
expect(panel).toHaveStyle({ display: "none" });
expect(panel2).toHaveAttribute("aria-labelledby");
expect(panel2).toHaveAttribute("role", "tabpanel");
expect(panel2).toHaveAttribute("tabindex", "0");
expect(panel2).toBeEmptyDOMElement();
expect(panel2).toHaveStyle({ display: "none" });
});
test("sets tabindex to 0 when focused", () => {
render(<TestTabs defaultValue="tab2" />);
const tab = screen.getByTestId("tab2");
render(<TestTabs defaultValue="tab1" />);
const tab2 = screen.getByTestId("tab2");
fireEvent.focus(tab);
expect(tab).toHaveAttribute("tabindex", "0");
fireEvent.focus(tab2);
expect(tab2).toHaveAttribute("tabindex", "0");
});
test("rowing tabindex keydown moves focus", () => {
render(<TestTabs defaultValue="tab1" />);
const tab1 = screen.getByTestId("tab1");
expect(tab1).toHaveAttribute("tabindex", "0");
fireEvent.keyDown(tab1, { key: "ArrowRight" });
expect(tab1).toHaveAttribute("tabindex", "-1");
expect(screen.getByTestId("tab2")).toHaveAttribute("tabindex", "0");
expect(screen.getByTestId("tab2")).toHaveAttribute(
"aria-selected",
"false",
);
});
test("selectionFollowsFocus moves active tabs", () => {
render(<TestTabs defaultValue="tab1" selectionFollowsFocus />);
const tab1 = screen.getByTestId("tab1");
expect(tab1).toHaveAttribute("tabindex", "0");
fireEvent.keyDown(tab1, { key: "ArrowRight" });
expect(screen.getByTestId("tab2")).toHaveAttribute("aria-selected", "true");
expect(screen.getByTestId("tab2")).toHaveAttribute("tabindex", "0");
});
test("tabbing from tabs moves focus to tabPanel", async () => {
render(<TestTabs defaultValue="tab1" />);
const tab1 = screen.getByTestId("tab1");
tab1.focus();
await userEvent.tab();
expect(screen.getByTestId("tabpanel1")).toHaveFocus();
});
test("shift+tab back to tablist should focus selected tab", async () => {
render(<TestTabs defaultValue="tab1" />);
const tab1 = screen.getByTestId("tab1");
/* Move focus to tab2 */
fireEvent.keyDown(tab1, { key: "ArrowRight" });
expect(screen.getByTestId("tab2")).toHaveFocus();
/* Move focus to tabPanel */
// eslint-disable-next-line testing-library/no-unnecessary-act
await act(async () => {
/* Tablist handles tabbing with setTimeout, so we need to use act */
await userEvent.tab();
});
expect(screen.getByTestId("tabpanel1")).toHaveFocus();
/* Move focus back to tablist, now tab1 should have focus */
// eslint-disable-next-line testing-library/no-unnecessary-act
await act(async () => {
await userEvent.tab({ shift: true });
});
expect(screen.getByTestId("tab1")).toHaveFocus();
});
});
+48 -70
View File
@@ -1,48 +1,15 @@
import * as RadixTabs from "@radix-ui/react-tabs";
import cl from "clsx";
import React, { HTMLAttributes, forwardRef } from "react";
import { OverridableComponent } from "../util/types";
import Tab, { TabProps } from "./Tab";
import TabList, { TabListProps } from "./TabList";
import TabPanel, { TabPanelProps } from "./TabPanel";
import { TabsContext } from "./context";
export interface TabsProps
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "dir"> {
children: React.ReactNode;
/**
* Changes padding and font-size.
* @default "medium"
*/
size?: "medium" | "small";
/**
* onChange callback for selected Tab.
*/
onChange?: (value: string) => void;
/**
* Controlled selected value.
*/
value?: string;
/**
* If not controlled, a default-value needs to be set.
*/
defaultValue?: string;
/**
* Automatically activates tab on focus/navigation.
* @default false
*/
selectionFollowsFocus?: boolean;
/**
* Loops back to start when navigating past last item.
* @default false
*/
loop?: boolean;
/**
* Icon position in Tab.
* @default "left"
*/
iconPosition?: "left" | "top";
}
import React, { forwardRef } from "react";
import {
TabsDescendantsProvider,
TabsProvider,
useTabsDescendants,
} from "./Tabs.context";
import { TabsProps } from "./Tabs.types";
import Tab from "./parts/tab/Tab";
import TabList from "./parts/tablist/TabList";
import TabPanel from "./parts/tabpanel/TabPanel";
import { useTabs } from "./useTabs";
interface TabsComponent
extends React.ForwardRefExoticComponent<
@@ -52,19 +19,15 @@ interface TabsComponent
* @see 🏷️ {@link TabProps}
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
*/
Tab: OverridableComponent<TabProps, HTMLButtonElement>;
Tab: typeof Tab;
/**
* @see 🏷️ {@link TabListProps}
*/
List: React.ForwardRefExoticComponent<
TabListProps & React.RefAttributes<HTMLDivElement>
>;
List: typeof TabList;
/**
* @see 🏷️ {@link TabPanelProps}
*/
Panel: React.ForwardRefExoticComponent<
TabPanelProps & React.RefAttributes<HTMLDivElement>
>;
Panel: typeof TabPanel;
}
/**
@@ -98,33 +61,48 @@ export const Tabs = forwardRef<HTMLDivElement, TabsProps>(
{
className,
children,
onChange,
size = "medium",
defaultValue = "",
value,
onChange,
id,
selectionFollowsFocus = false,
loop = false,
loop = true,
iconPosition = "left",
fill = false,
...rest
},
ref,
) => {
const descendants = useTabsDescendants();
const tabsContext = useTabs({ defaultValue, value, onChange, id });
/**
* TabsProvider handles memoization of context values, so we can safely skip it here.
*/
const context = {
...tabsContext,
selectionFollowsFocus,
loop,
size,
iconPosition,
fill,
};
return (
<RadixTabs.Root
{...rest}
ref={ref}
className={cl("navds-tabs", className, `navds-tabs--${size}`)}
activationMode={selectionFollowsFocus ? "automatic" : "manual"}
onValueChange={onChange}
>
<TabsContext.Provider
value={{
size,
loop,
iconPosition,
}}
>
{children}
</TabsContext.Provider>
</RadixTabs.Root>
<TabsDescendantsProvider value={descendants}>
<TabsProvider {...context}>
<div
ref={ref}
{...rest}
id={id}
className={cl("navds-tabs", className, `navds-tabs--${size}`)}
>
{children}
</div>
</TabsProvider>
</TabsDescendantsProvider>
);
},
) as TabsComponent;
+43
View File
@@ -0,0 +1,43 @@
import { HTMLAttributes } from "react";
export interface TabsProps
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "dir"> {
children: React.ReactNode;
/**
* Changes padding and font-size.
* @default "medium"
*/
size?: "medium" | "small";
/**
* onChange callback for selected Tab.
*/
onChange?: (value: string) => void;
/**
* Controlled selected value.
*/
value?: string;
/**
* If not controlled, a default-value needs to be set.
*/
defaultValue?: string;
/**
* Automatically activates tab on focus/navigation.
* @default false
*/
selectionFollowsFocus?: boolean;
/**
* Loops back to start when navigating past last item.
* @default true
*/
loop?: boolean;
/**
* Icon position in Tab.
* @default "left"
*/
iconPosition?: "left" | "top";
/**
* Stretches each tab to fill avaliable space in container.
* @default false
*/
fill?: boolean;
}
-9
View File
@@ -1,9 +0,0 @@
import { createContext } from "react";
interface TabsContextProps {
size: "medium" | "small";
loop: boolean;
iconPosition: "left" | "top";
}
export const TabsContext = createContext<TabsContextProps | null>(null);
+11 -4
View File
@@ -1,5 +1,12 @@
"use client";
export { default as TabsTab, type TabProps } from "./Tab";
export { default as TabsList, type TabListProps } from "./TabList";
export { default as TabsPanel, type TabPanelProps } from "./TabPanel";
export { default as Tabs, type TabsProps } from "./Tabs";
export { default as Tabs } from "./Tabs";
export { type TabsProps } from "./Tabs.types";
export { default as TabsTab, type TabProps } from "./parts/tab/Tab";
export {
default as TabsList,
type TabListProps,
} from "./parts/tablist/TabList";
export {
default as TabsPanel,
type TabPanelProps,
} from "./parts/tabpanel/TabPanel";
@@ -0,0 +1,93 @@
import cl from "clsx";
import React, { forwardRef } from "react";
import { BodyShort } from "../../../typography";
import { OverridableComponent } from "../../../util";
import { useTabsContext } from "../../Tabs.context";
import { useTab } from "./useTab";
export interface TabProps
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
/**
* Tab label.
*/
label?: React.ReactNode;
/**
* Tab Icon.
*/
icon?: React.ReactNode;
/**
* Value for state-handling.
*/
value: string;
/**
* Overrides auto-generated id.
*
* **Warning**: Tab generates an id if not provided. If you need to override it,
* make sure to also include the correct `aria-controls` id for the TabPanel it controls.
*/
id?: string;
}
export const Tab: OverridableComponent<TabProps, HTMLButtonElement> =
forwardRef(
(
{
className,
as: Component = "button",
label,
icon,
value,
onClick,
onFocus,
disabled,
id,
...rest
},
ref: React.ForwardedRef<HTMLButtonElement>,
) => {
const tabCtx = useTab({ value, onClick, onFocus, disabled }, ref);
const ctx = useTabsContext();
if (!label && !icon) {
console.error("<Tabs.Tab/> needs label and/or icon");
return null;
}
return (
<Component
ref={tabCtx.ref}
{...rest}
className={cl(
"navds-tabs__tab",
`navds-tabs__tab--${ctx?.size ?? "medium"}`,
`navds-tabs__tab-icon--${ctx?.iconPosition}`,
className,
{
"navds-tabs__tab--icon-only": icon && !label,
"navds-tabs__tab--fill": ctx.fill,
},
)}
role="tab"
type="button"
aria-selected={tabCtx.isSelected}
data-state={tabCtx.isSelected ? "active" : "inactive"}
tabIndex={tabCtx.isFocused ? 0 : -1}
aria-controls={rest["aria-controls"] ?? tabCtx.controlsId}
id={id ?? tabCtx.id}
onFocus={tabCtx.onFocus}
onClick={tabCtx.onClick}
>
<BodyShort
as="span"
className="navds-tabs__tab-inner"
size={ctx?.size}
>
<span aria-hidden={!!label}>{icon}</span>
<span>{label}</span>
</BodyShort>
</Component>
);
},
);
export default Tab;
@@ -0,0 +1,52 @@
import { composeEventHandlers } from "../../../util/composeEventHandlers";
import { mergeRefs } from "../../../util/hooks/useMergeRefs";
import { useTabsContext, useTabsDescendant } from "../../Tabs.context";
export interface UseTabProps {
/**
* If `true`, the `Tab` won't be toggleable
* @default false
*/
disabled?: boolean;
onClick?: React.MouseEventHandler;
onFocus?: React.FocusEventHandler;
value: string;
}
export function useTab<P extends UseTabProps>(
{ value, disabled = false, onFocus: _onFocus, onClick }: P,
ref: React.ForwardedRef<HTMLButtonElement>,
) {
const {
id,
setSelectedValue,
selectionFollowsFocus,
focusedValue,
setFocusedValue,
selectedValue,
makeTabId,
makeTabPanelId,
} = useTabsContext();
const { register } = useTabsDescendant({
disabled,
value,
});
const isSelected = value === selectedValue;
const onFocus = () => {
setFocusedValue(value);
selectionFollowsFocus && setSelectedValue(value);
};
return {
ref: mergeRefs([register, ref]),
isSelected,
isFocused: focusedValue === value,
id: makeTabId(id, value),
controlsId: makeTabPanelId(id, value),
onClick: composeEventHandlers(onClick, () => setSelectedValue(value)),
onFocus: disabled ? undefined : composeEventHandlers(_onFocus, onFocus),
};
}
@@ -0,0 +1,29 @@
import cl from "clsx";
import React from "react";
import { ChevronLeftIcon, ChevronRightIcon } from "@navikt/aksel-icons";
interface ScrollButtonProps {
hidden: boolean;
onClick: () => void;
dir: "left" | "right";
}
function ScrollButton({ hidden, onClick, dir }: ScrollButtonProps) {
return (
<div
className={cl("navds-tabs__scroll-button", {
"navds-tabs__scroll-button--hidden": hidden,
})}
onClick={onClick}
aria-hidden
>
{dir === "left" ? (
<ChevronLeftIcon title="scroll tilbake" />
) : (
<ChevronRightIcon title="scroll neste" />
)}
</div>
);
}
export default ScrollButton;
@@ -0,0 +1,56 @@
/* eslint-disable jsx-a11y/interactive-supports-focus */
import cl from "clsx";
import React, { forwardRef, useRef } from "react";
import { composeEventHandlers } from "../../../util/composeEventHandlers";
import { useMergeRefs } from "../../../util/hooks/useMergeRefs";
import ScrollButton from "./ScrollButtons";
import { useScrollButtons } from "./useScrollButtons";
import { useTabList } from "./useTabList";
export interface TabListProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* <Tabs.Tab /> elements.
*/
children: React.ReactNode;
}
export const TabList = forwardRef<HTMLDivElement, TabListProps>(
({ className, onKeyDown, ...rest }, ref) => {
const { onKeyDown: _onKeyDown } = useTabList();
const listRef = useRef<HTMLDivElement>(null);
const mergedRef = useMergeRefs(listRef, ref);
const scrollCtx = useScrollButtons(listRef);
return (
<div className="navds-tabs__tablist-wrapper">
{scrollCtx.show && (
<ScrollButton
dir="left"
hidden={!scrollCtx.start}
onClick={scrollCtx.scrollLeft}
/>
)}
<div
ref={mergedRef}
{...rest}
onScroll={scrollCtx.update}
className={cl("navds-tabs__tablist", className)}
role="tablist"
aria-orientation="horizontal"
onKeyDown={composeEventHandlers(onKeyDown, _onKeyDown)}
/>
{scrollCtx.show && (
<ScrollButton
dir="right"
hidden={!scrollCtx.end}
onClick={scrollCtx.scrollRight}
/>
)}
</div>
);
},
);
export default TabList;
@@ -0,0 +1,69 @@
import { useEffect, useMemo, useState } from "react";
import { debounce } from "../../../util";
export function useScrollButtons(listRef: React.RefObject<HTMLDivElement>) {
const [displayScroll, setDisplayScroll] = useState({
start: false,
end: false,
});
const updateScrollButtonState = useMemo(
() =>
debounce(() => {
if (!listRef?.current) return;
const { scrollWidth, clientWidth } = listRef.current;
const scrollLeft = listRef.current.scrollLeft;
// use 1 for the potential rounding error with browser zooms.
const showStartScroll = scrollLeft > 1;
const showEndScroll = scrollLeft < scrollWidth - clientWidth - 1;
setDisplayScroll((oldDisplayScroll) =>
showStartScroll === oldDisplayScroll.start &&
showEndScroll === oldDisplayScroll.end
? oldDisplayScroll
: { start: showStartScroll, end: showEndScroll },
);
}),
[listRef],
);
useEffect(() => {
const handleResize = () => updateScrollButtonState();
const win = listRef.current?.ownerDocument ?? document ?? window;
win.addEventListener("resize", handleResize);
let resizeObserver;
if (typeof ResizeObserver !== "undefined") {
resizeObserver = new ResizeObserver(handleResize);
resizeObserver.observe(listRef.current);
}
return () => {
win.removeEventListener("resize", handleResize);
resizeObserver && resizeObserver.disconnect();
updateScrollButtonState.clear();
};
}, [listRef, updateScrollButtonState]);
useEffect(() => {
updateScrollButtonState();
});
return {
update: updateScrollButtonState,
start: displayScroll.start,
end: displayScroll.end,
show: displayScroll.end || displayScroll.start,
scrollLeft: () => {
if (listRef.current) {
listRef.current.scrollLeft -= 100;
}
},
scrollRight: () => {
if (listRef.current) {
listRef.current.scrollLeft += 100;
}
},
};
}
@@ -0,0 +1,68 @@
import { useCallback } from "react";
import { useTabsContext, useTabsDescendantsContext } from "../../Tabs.context";
/**
* TabList hook to manage multiple tab buttons,
* and ensures only one tab is selected at a time.
*/
export function useTabList() {
const { focusedValue, loop, selectedValue, setFocusedValue } =
useTabsContext();
const descendants = useTabsDescendantsContext();
/**
* Implements rowing-tabindex for horizontal tabs
*/
const onKeyDown = useCallback(
(event: React.KeyboardEvent) => {
/**
* Tabs.Tab is registered with its prop 'value'.
* We can then use it to find the current focuses descendant
*/
const idx = descendants
.values()
.findIndex((x) => x.value === focusedValue);
const nextTab = () => {
const next = descendants.nextEnabled(idx, loop);
next && next.node?.focus();
};
const prevTab = () => {
const prev = descendants.prevEnabled(idx, loop);
prev && prev.node?.focus();
};
const firstTab = () => {
const first = descendants.firstEnabled();
first && first.node?.focus();
};
const lastTab = () => {
const last = descendants.lastEnabled();
last && last.node?.focus();
};
const keyMap: Record<string, React.KeyboardEventHandler> = {
ArrowLeft: prevTab,
ArrowRight: nextTab,
Home: firstTab,
End: lastTab,
};
const action = keyMap[event.key];
if (action) {
event.preventDefault();
action(event);
} else if (event.key === "Tab") {
/**
* Imperative focus during keydown is risky so we prevent React's batching updates
* to avoid potential bugs. See: https://github.com/facebook/react/issues/20332
*/
selectedValue && setTimeout(() => setFocusedValue(selectedValue));
}
},
[descendants, focusedValue, loop, selectedValue, setFocusedValue],
);
return { onKeyDown };
}
@@ -0,0 +1,50 @@
import cl from "clsx";
import React, { forwardRef } from "react";
import { useTabPanel } from "./useTabPanel";
export interface TabPanelProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Tab panel content.
*/
children: React.ReactNode;
/**
* Value for state-handling.
*/
value: string;
/**
* If true, will only render children when selected.
* @default true
*/
lazy?: boolean;
/**
* Overrides auto-generated id.
*
* **Warning**: TabPanel generates an id if not provided. If you need to override it,
* make sure to also include the correct `aria-labelledby` id for the Tab that labels it.
*/
id?: string;
}
const TabPanel = forwardRef<HTMLDivElement, TabPanelProps>(
({ className, value, children, lazy = true, id, ...rest }, ref) => {
const ctx = useTabPanel({ value });
return (
<div
ref={ref}
{...rest}
className={cl("navds-tabs__tabpanel", className)}
role="tabpanel"
tabIndex={0}
aria-labelledby={rest["aria-labelledby"] ?? ctx.labelledbyId}
id={id ?? ctx.id}
hidden={ctx.hidden}
data-state={!ctx.hidden ? "active" : "inactive"}
>
{lazy && ctx.hidden ? null : children}
</div>
);
},
);
export default TabPanel;
@@ -0,0 +1,18 @@
import { useTabsContext } from "../../Tabs.context";
type TabPanelProps = {
value: string;
};
/**
* Tabs hook for managing the visible/hidden state of Tabs.Panel
*/
export function useTabPanel({ value }: TabPanelProps) {
const { id, selectedValue, makeTabId, makeTabPanelId } = useTabsContext();
return {
labelledbyId: makeTabId(id, value),
hidden: selectedValue !== value,
id: makeTabPanelId(id, value),
};
}
+51
View File
@@ -0,0 +1,51 @@
import { useEffect, useState } from "react";
import { useId } from "../util";
import { useControllableState } from "../util/hooks/useControllableState";
import { TabsProps } from "./Tabs.types";
export function useTabs({
onChange,
value,
defaultValue = "",
id,
}: Pick<TabsProps, "onChange" | "value" | "defaultValue" | "id">) {
const [focusedValue, setFocusedValue] = useState(defaultValue);
const [selectedValue, setSelectedValue] = useControllableState({
defaultValue,
value,
onChange,
});
/**
* Sync focused `value` with controlled `selectedValue`
*/
useEffect(() => {
if (value != null) {
setFocusedValue(value);
}
}, [value]);
/**
* Scope ids for better tracking
*/
const uuid = useId();
return {
id: `tabs-${id ?? uuid}`,
selectedValue,
setSelectedValue,
focusedValue,
setFocusedValue,
makeTabId,
makeTabPanelId,
};
}
function makeTabId(id: string, value: string) {
return `${id}--tab-${value}`;
}
function makeTabPanelId(id: string, value: string) {
return `${id}--tabpanel-${value}`;
}
@@ -0,0 +1,31 @@
import { createContext as ReactCreateContext } from "react";
import { createContext } from "../util/create-context";
import { createDescendantContext } from "../util/hooks/descendants/useDescendant";
import { ToggleGroupProps } from "./ToggleGroup.types";
import { useToggleGroup } from "./useToggleGroup";
interface ToggleContextProps {
size: "medium" | "small";
}
export const ToggleGroupContext = ReactCreateContext<ToggleContextProps | null>(
null,
);
export const [
ToggleGroupDescendantsProvider,
useToggleGroupDescendantsContext,
useToggleGroupDescendants,
useToggleGroupDescendant,
] = createDescendantContext<HTMLButtonElement, { value: string }>();
type ToggleGroupProviderProps = ReturnType<typeof useToggleGroup> &
Pick<ToggleGroupProps, "size">;
/* State context */
export const [ToggleGroupProvider, useToggleGroupContext] =
createContext<ToggleGroupProviderProps>({
name: "ToggleGroupContext",
hookName: "useToggleGroupContext",
providerName: "ToggleGroupProvider",
});
@@ -5,6 +5,7 @@ import {
EnvelopeOpenIcon,
InboxUpIcon,
} from "@navikt/aksel-icons";
import { VStack } from "../layout/stack";
import ToggleGroup from "./ToggleGroup";
const meta: Meta<typeof ToggleGroup> = {
@@ -22,7 +23,11 @@ const meta: Meta<typeof ToggleGroup> = {
control: { type: "radio" },
},
},
parameters: {
chromatic: { disable: true },
},
};
export default meta;
const Items = (icon?: boolean, both?: boolean) => (
@@ -71,6 +76,7 @@ export const Default = (props) => {
</ToggleGroup>
);
};
Default.args = {
icon: true,
text: true,
@@ -81,7 +87,7 @@ export const Compositions = () => {
const [activeValue, setActiveValue] = useState("ulest");
return (
<div className="colgap">
<VStack gap="6">
<ToggleGroup value={activeValue} onChange={setActiveValue}>
{Items()}
</ToggleGroup>
@@ -91,7 +97,10 @@ export const Compositions = () => {
<ToggleGroup value={activeValue} onChange={setActiveValue}>
{Items(true)}
</ToggleGroup>
</div>
<ToggleGroup fill value={activeValue} onChange={setActiveValue}>
{Items(true)}
</ToggleGroup>
</VStack>
);
};
@@ -99,7 +108,7 @@ export const Variants = () => {
const [activeValue, setActiveValue] = useState("ulest");
return (
<div className="colgap">
<VStack gap="6">
<ToggleGroup
variant="action"
value={activeValue}
@@ -114,7 +123,7 @@ export const Variants = () => {
>
{Items(true, true)}
</ToggleGroup>
</div>
</VStack>
);
};
@@ -122,7 +131,7 @@ export const Small = () => {
const [activeValue, setActiveValue] = useState("ulest");
return (
<div className="colgap">
<VStack gap="6">
<ToggleGroup size="small" value={activeValue} onChange={setActiveValue}>
{Items()}
</ToggleGroup>
@@ -132,6 +141,58 @@ export const Small = () => {
<ToggleGroup size="small" value={activeValue} onChange={setActiveValue}>
{Items(true)}
</ToggleGroup>
</div>
</VStack>
);
};
export const Chromatic = {
render: () => (
<VStack gap="6">
<div>
<h2>Text</h2>
<ToggleGroup value="ulest" onChange={console.log}>
{Items()}
</ToggleGroup>
</div>
<div>
<h2>Icon</h2>
<ToggleGroup value="ulest" onChange={console.log}>
{Items(true)}
</ToggleGroup>
</div>
<div>
<h2>Text + icon</h2>
<ToggleGroup value="ulest" onChange={console.log}>
{Items(true, true)}
</ToggleGroup>
</div>
<div style={{ minWidth: 600 }}>
<h2>Fill</h2>
<ToggleGroup value="ulest" onChange={console.log} fill>
{Items(true, true)}
</ToggleGroup>
</div>
<div>
<h2>Small</h2>
<ToggleGroup value="ulest" onChange={console.log} size="small">
{Items(true, true)}
</ToggleGroup>
</div>
<div>
<h2>Small + fill</h2>
<ToggleGroup value="ulest" onChange={console.log} size="small" fill>
{Items(true, true)}
</ToggleGroup>
</div>
<div>
<h2>Neutral</h2>
<ToggleGroup value="ulest" onChange={console.log} variant="neutral">
{Items(true, true)}
</ToggleGroup>
</div>
</VStack>
),
parameters: {
chromatic: { disable: false },
},
};
@@ -1,4 +1,5 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { act, fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React from "react";
import { describe, expect, test } from "vitest";
import { ToggleGroup } from "./ToggleGroup";
@@ -20,36 +21,76 @@ const TestToggleGroup = ({ value, onChange, defaultValue }: any) => (
describe("ToggleGroup", () => {
test("sets default value correctly", () => {
render(<TestToggleGroup defaultValue="toggle2" />);
const toggle = screen.getByTestId("toggle2");
const toggle2 = screen.getByTestId("toggle2");
expect(toggle).toHaveAttribute("aria-checked", "true");
expect(toggle2).toHaveAttribute("aria-checked", "true");
});
test("sets correct attributes on active toggle", () => {
render(<TestToggleGroup defaultValue="toggle2" />);
const toggle = screen.getByTestId("toggle2");
const toggle2 = screen.getByTestId("toggle2");
expect(toggle).toHaveAttribute("aria-checked", "true");
expect(toggle).toHaveAttribute("role", "radio");
expect(toggle).toHaveAttribute("type", "button");
expect(toggle).toHaveAttribute("tabindex", "-1");
expect(toggle2).toHaveAttribute("aria-checked", "true");
expect(toggle2).toHaveAttribute("role", "radio");
expect(toggle2).toHaveAttribute("type", "button");
expect(toggle2).toHaveAttribute("tabindex", "0");
});
test("sets correct attributes on idle toggle", () => {
render(<TestToggleGroup defaultValue="toggle1" />);
const toggle = screen.getByTestId("toggle2");
const toggle2 = screen.getByTestId("toggle2");
expect(toggle).toHaveAttribute("aria-checked", "false");
expect(toggle).toHaveAttribute("role", "radio");
expect(toggle).toHaveAttribute("type", "button");
expect(toggle).toHaveAttribute("tabindex", "-1");
expect(toggle2).toHaveAttribute("aria-checked", "false");
expect(toggle2).toHaveAttribute("role", "radio");
expect(toggle2).toHaveAttribute("type", "button");
expect(toggle2).toHaveAttribute("tabindex", "-1");
});
test("sets tabindex to 0 when focused", () => {
render(<TestToggleGroup defaultValue="toggle2" />);
const toggle = screen.getByTestId("toggle2");
const toggle2 = screen.getByTestId("toggle2");
fireEvent.focus(toggle);
expect(toggle).toHaveAttribute("tabindex", "0");
fireEvent.focus(toggle2);
expect(toggle2).toHaveAttribute("tabindex", "0");
});
test("roving tabindex keydown moves focus", () => {
render(<TestToggleGroup defaultValue="toggle1" />);
const toggle1 = screen.getByTestId("toggle1");
expect(toggle1).toHaveAttribute("tabindex", "0");
fireEvent.keyDown(toggle1, { key: "ArrowRight" });
expect(toggle1).toHaveAttribute("tabindex", "-1");
expect(screen.getByTestId("toggle2")).toHaveAttribute("tabindex", "0");
expect(screen.getByTestId("toggle2")).toHaveAttribute(
"aria-checked",
"false",
);
});
test("Space selects focused toggle-item", async () => {
render(<TestToggleGroup defaultValue="toggle1" />);
const toggle1 = screen.getByTestId("toggle1");
expect(toggle1).toHaveAttribute("tabindex", "0");
fireEvent.keyDown(toggle1, { key: "ArrowRight" });
expect(toggle1).toHaveAttribute("tabindex", "-1");
expect(screen.getByTestId("toggle2")).toHaveAttribute("tabindex", "0");
expect(screen.getByTestId("toggle2")).toHaveAttribute(
"aria-checked",
"false",
);
// eslint-disable-next-line testing-library/no-unnecessary-act
await act(async () => {
await userEvent.keyboard(" ");
});
expect(screen.getByTestId("toggle2")).toHaveAttribute(
"aria-checked",
"true",
);
});
});
@@ -1,55 +1,24 @@
import * as RadixToggleGroup from "@radix-ui/react-toggle-group";
import cl from "clsx";
import React, { HTMLAttributes, forwardRef, useState } from "react";
import React, { forwardRef } from "react";
import { Label } from "../typography";
import { useId } from "../util/hooks";
import ToggleItem, { ToggleGroupItemProps } from "./ToggleItem";
import { ToggleGroupContext } from "./context";
export interface ToggleGroupProps
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "dir"> {
/**
* Toggles.Item elements
*/
children: React.ReactNode;
/**
* Changes padding and font-size
* @default "medium"
*/
size?: "medium" | "small";
/**
* Controlled selected value
*/
value?: string;
/**
* If not controlled, a default-value needs to be set
*/
defaultValue?: string;
/**
* Callback for selected toggle
*/
onChange: (value: string) => void;
/**
* Label describing ToggleGroup
*/
label?: React.ReactNode;
/**
* Changes design and interaction-visuals
* @default "action"
*/
variant?: "action" | "neutral";
}
import { useId } from "../util";
import {
ToggleGroupDescendantsProvider,
ToggleGroupProvider,
useToggleGroupDescendants,
} from "./ToggleGroup.context";
import { ToggleGroupProps } from "./ToggleGroup.types";
import ToggleItem from "./parts/ToggleItem";
import { useToggleGroup } from "./useToggleGroup";
interface ToggleGroupComponent
extends React.ForwardRefExoticComponent<
ToggleGroupProps & React.RefAttributes<HTMLDivElement>
> {
/**
* @see 🏷️ {@link ToggleGroupItemProps}
* @see 🏷️ {@link ToggleItem}
*/
Item: React.ForwardRefExoticComponent<
ToggleGroupItemProps & React.RefAttributes<HTMLButtonElement>
>;
Item: typeof ToggleItem;
}
/**
@@ -77,70 +46,74 @@ export const ToggleGroup = forwardRef<HTMLDivElement, ToggleGroupProps>(
label,
value,
defaultValue,
"aria-describedby": desc,
"aria-describedby": userDescribedby,
variant = "action",
fill = false,
...rest
},
ref,
) => {
const [groupValue, setGroupValue] = useState(defaultValue);
const labelId = useId();
const descendants = useToggleGroupDescendants();
const handleValueChange = (v: string) => {
if (v !== "") {
setGroupValue(v);
onChange?.(v);
}
const toggleGroupContext = useToggleGroup({
defaultValue,
value,
onChange,
});
/**
* ToggleGroupProvider handles memoization.
*/
const context = {
...toggleGroupContext,
size,
};
if (!value && !defaultValue) {
console.error("ToggleGroup without value/defaultvalue is not allowed");
}
const labelId = useId();
const describeBy = cl({
[desc ?? ""]: !!desc,
[labelId ?? ""]: !!label,
});
if (!value && !defaultValue) {
console.error("ToggleGroup without value or defaultvalue is not allowed");
}
if (!value && !defaultValue) {
console.error("ToggleGroup needs either a value or defaultValue");
}
return (
<ToggleGroupContext.Provider
value={{
size,
}}
>
<div className={cl("navds-toggle-group__wrapper", className)}>
{label && (
<Label
size={size}
className="navds-toggle-group__label"
id={labelId}
>
{label}
</Label>
)}
<RadixToggleGroup.Root
{...rest}
onValueChange={handleValueChange}
value={value ?? groupValue}
defaultValue={defaultValue}
ref={ref}
className={cl(
"navds-toggle-group",
`navds-toggle-group--${size}`,
`navds-toggle-group--${variant}`,
)}
{...(describeBy && { "aria-describedby": describeBy })}
role="radiogroup"
type="single"
<ToggleGroupDescendantsProvider value={descendants}>
<ToggleGroupProvider {...context}>
<div
className={cl("navds-toggle-group__wrapper", className, {
"navds-toggle-group__wrapper--fill": fill,
})}
>
{children}
</RadixToggleGroup.Root>
</div>
</ToggleGroupContext.Provider>
{label && (
<Label
size={size}
className="navds-toggle-group__label"
id={labelId}
>
{label}
</Label>
)}
<div
{...rest}
ref={ref}
className={cl(
"navds-toggle-group",
`navds-toggle-group--${size}`,
`navds-toggle-group--${variant}`,
)}
aria-describedby={
cl(userDescribedby, !!label && labelId) || undefined
}
role="radiogroup"
>
{children}
</div>
</div>
</ToggleGroupProvider>
</ToggleGroupDescendantsProvider>
);
},
) as ToggleGroupComponent;
@@ -0,0 +1,40 @@
import { HTMLAttributes } from "react";
export interface ToggleGroupProps
extends Omit<HTMLAttributes<HTMLDivElement>, "onChange" | "dir"> {
/**
* Toggles.Item elements.
*/
children: React.ReactNode;
/**
* Changes padding and font-size.
* @default "medium"
*/
size?: "medium" | "small";
/**
* Controlled selected value.
*/
value?: string;
/**
* If not controlled, a default-value needs to be set.
*/
defaultValue?: string;
/**
* Callback for selected toggle.
*/
onChange: (value: string) => void;
/**
* Label describing ToggleGroup.
*/
label?: React.ReactNode;
/**
* Changes design and interaction-visuals.
* @default "action"
*/
variant?: "action" | "neutral";
/**
* Stretch each button to fill avaliable space in container.
* @default false
*/
fill?: boolean;
}
@@ -1,41 +0,0 @@
import * as RadixToggleGroup from "@radix-ui/react-toggle-group";
import cl from "clsx";
import React, { forwardRef, useContext } from "react";
import { BodyShort } from "../typography/BodyShort";
import { ToggleGroupContext } from "./context";
export interface ToggleGroupItemProps
extends React.HTMLAttributes<HTMLButtonElement> {
/**
* Content
*/
children: React.ReactNode;
/**
* Value for state-handling
*/
value: string;
}
const ToggleItem = forwardRef<HTMLButtonElement, ToggleGroupItemProps>(
({ className, children, ...rest }, ref) => {
const context = useContext(ToggleGroupContext);
return (
<RadixToggleGroup.Item
{...rest}
ref={ref}
className={cl("navds-toggle-group__button", className)}
>
<BodyShort
as="span"
className="navds-toggle-group__button-inner"
size={context?.size}
>
{children}
</BodyShort>
</RadixToggleGroup.Item>
);
},
);
export default ToggleItem;
@@ -1,9 +0,0 @@
import { createContext } from "react";
interface ToggleContextProps {
size: "medium" | "small";
}
export const ToggleGroupContext = createContext<ToggleContextProps | null>(
null,
);
+3 -2
View File
@@ -1,6 +1,7 @@
"use client";
export { default as ToggleGroup, type ToggleGroupProps } from "./ToggleGroup";
export { default as ToggleGroup } from "./ToggleGroup";
export { type ToggleGroupProps } from "./ToggleGroup.types";
export {
default as ToggleGroupItem,
type ToggleGroupItemProps,
} from "./ToggleItem";
} from "./parts/ToggleItem";
@@ -0,0 +1,55 @@
import cl from "clsx";
import React, { forwardRef } from "react";
import { BodyShort } from "../../typography/BodyShort";
import { useToggleGroupContext } from "../ToggleGroup.context";
import { useToggleItem } from "./useToggleItem";
export interface ToggleGroupItemProps
extends React.HTMLAttributes<HTMLButtonElement> {
/**
* Content.
*/
children: React.ReactNode;
/**
* Value for state-handling.
*/
value: string;
}
const ToggleItem = forwardRef<HTMLButtonElement, ToggleGroupItemProps>(
(
{ className, children, value, onClick, onFocus, onKeyDown, ...rest },
forwardedRef,
) => {
const itemCtx = useToggleItem(
{ value, onClick, onFocus, disabled: false, onKeyDown },
forwardedRef,
);
const ctx = useToggleGroupContext();
return (
<button
{...rest}
ref={itemCtx.ref}
className={cl("navds-toggle-group__button", className)}
type="button"
role="radio"
aria-checked={itemCtx.isSelected}
tabIndex={itemCtx.isFocused ? 0 : -1}
onClick={itemCtx.onClick}
onFocus={itemCtx.onFocus}
onKeyDown={itemCtx.onKeyDown}
>
<BodyShort
as="span"
className="navds-toggle-group__button-inner"
size={ctx?.size}
>
{children}
</BodyShort>
</button>
);
},
);
export default ToggleItem;
@@ -0,0 +1,104 @@
import { useCallback } from "react";
import { composeEventHandlers } from "../../util/composeEventHandlers";
import { mergeRefs } from "../../util/hooks/useMergeRefs";
import {
useToggleGroupContext,
useToggleGroupDescendant,
} from "../ToggleGroup.context";
export interface UseToggleItemProps {
/**
* If `true`, the `ToggleItem` won't be toggleable
* @default false
*/
disabled?: boolean;
onClick?: React.MouseEventHandler;
onFocus?: React.FocusEventHandler;
onKeyDown?: React.KeyboardEventHandler;
value: string;
}
export function useToggleItem<P extends UseToggleItemProps>(
{
value,
disabled = false,
onFocus: _onFocus,
onClick,
onKeyDown: _onKeyDown,
}: P,
ref: React.ForwardedRef<HTMLButtonElement>,
) {
const { setSelectedValue, setFocusedValue, selectedValue, focusedValue } =
useToggleGroupContext();
const { register, descendants } = useToggleGroupDescendant({
disabled,
value,
});
const isSelected = value === selectedValue;
const onFocus = () => setFocusedValue(value);
/**
* Implements roving-tabindex for horizontal tabs
*/
const onKeyDown = useCallback(
(event: React.KeyboardEvent) => {
/**
* ToggleGroup.Item is registered with its prop 'value'.
* We can then use it to find the current focuses descendant
*/
const idx = descendants
.values()
.findIndex((x) => x.value === focusedValue);
const nextTab = () => {
const next = descendants.nextEnabled(idx, false);
next && next.node?.focus();
};
const prevTab = () => {
const prev = descendants.prevEnabled(idx, false);
prev && prev.node?.focus();
};
const firstTab = () => {
const first = descendants.firstEnabled();
first && first.node?.focus();
};
const lastTab = () => {
const last = descendants.lastEnabled();
last && last.node?.focus();
};
const keyMap: Record<string, React.KeyboardEventHandler> = {
ArrowLeft: prevTab,
ArrowRight: nextTab,
Home: firstTab,
End: lastTab,
};
const action = keyMap[event.key];
if (action) {
event.preventDefault();
action(event);
} else if (event.key === "Tab") {
/**
* Imperative focus during keydown is risky so we prevent React's batching updates
* to avoid potential bugs. See: https://github.com/facebook/react/issues/20332
*/
selectedValue && setTimeout(() => setFocusedValue(selectedValue));
}
},
[descendants, focusedValue, selectedValue, setFocusedValue],
);
return {
ref: mergeRefs([register, ref]),
isSelected,
isFocused: focusedValue === value,
onClick: composeEventHandlers(onClick, () => setSelectedValue(value)),
onFocus: disabled ? undefined : composeEventHandlers(_onFocus, onFocus),
onKeyDown: composeEventHandlers(_onKeyDown, onKeyDown),
};
}
@@ -0,0 +1,33 @@
import { useEffect, useState } from "react";
import { useControllableState } from "../util/hooks/useControllableState";
import { ToggleGroupProps } from "./ToggleGroup.types";
export function useToggleGroup({
onChange,
value,
defaultValue = "",
}: Pick<ToggleGroupProps, "onChange" | "value" | "defaultValue">) {
const [focusedValue, setFocusedValue] = useState(defaultValue);
const [selectedValue, setSelectedValue] = useControllableState({
defaultValue,
value,
onChange,
});
/**
* Sync focused `value` with controlled `selectedValue`
*/
useEffect(() => {
if (value != null) {
setFocusedValue(value);
}
}, [value]);
return {
selectedValue,
setSelectedValue,
focusedValue,
setFocusedValue,
};
}
@@ -4,18 +4,17 @@ import React from "react";
import { VStack } from "../../layout/stack";
import BodyLong from "../BodyLong";
const meta = {
const meta: Meta<typeof BodyLong> = {
title: "ds-react/Typography/BodyLong",
component: BodyLong,
decorators: [(story) => <div style={{ maxWidth: "700px" }}>{story()}</div>],
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof BodyLong>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof BodyLong>;
const lorem =
"Hvis du ikke bor sammen med begge foreldrene dine, kan du ha rett til barnebidrag fra en eller begge foreldre mens du fullfører videregående skole eller tilsvarende.";
@@ -4,18 +4,17 @@ import React from "react";
import { VStack } from "../../layout/stack";
import { BodyShort } from "../BodyShort";
const meta = {
const meta: Meta<typeof BodyShort> = {
title: "ds-react/Typography/BodyShort",
component: BodyShort,
decorators: [(story) => <div style={{ maxWidth: "250px" }}>{story()}</div>],
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof BodyShort>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof BodyShort>;
const lorem = "Du må gjøre en filtrering for å se brukere i listen.";
@@ -4,18 +4,17 @@ import React from "react";
import { VStack } from "../../layout/stack";
import Detail from "../Detail";
const meta = {
const meta: Meta<typeof Detail> = {
title: "ds-react/Typography/Detail",
component: Detail,
decorators: [(story) => <div style={{ maxWidth: "200px" }}>{story()}</div>],
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof Detail>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof Detail>;
const lorem = "Du må gjøre en filtrering for å se brukere i listen.";
@@ -4,17 +4,16 @@ import React from "react";
import { VStack } from "../../layout/stack";
import ErrorMessage from "../ErrorMessage";
const meta = {
const meta: Meta<typeof ErrorMessage> = {
title: "ds-react/Typography/ErrorMessage",
component: ErrorMessage,
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof ErrorMessage>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof ErrorMessage>;
const lorem =
"Du må fylle ut: Oppgi årsaken til at du har ventet mer enn 6 måneder med å søke om refusjon";
@@ -4,18 +4,17 @@ import { default as React } from "react";
import { VStack } from "../../layout/stack";
import Heading from "../Heading";
const meta = {
const meta: Meta<typeof Heading> = {
title: "ds-react/Typography/Heading",
component: Heading,
decorators: [(story) => <div style={{ maxWidth: "300px" }}>{story()}</div>],
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof Heading>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof Heading>;
const lorem = "Hva kan vi hjelpe deg med?";
@@ -4,18 +4,17 @@ import React from "react";
import { VStack } from "../../layout/stack";
import Label from "../Label";
const meta = {
const meta: Meta<typeof Label> = {
title: "ds-react/Typography/Label",
component: Label,
decorators: [(story) => <div style={{ maxWidth: "400px" }}>{story()}</div>],
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof Label>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof Label>;
const lorem =
"Oppgi årsaken til at du har ventet mer enn 6 måneder med å søke om refusjon";
+14
View File
@@ -1,5 +1,19 @@
# @navikt/ds-tailwind
## 6.5.0
## 6.4.1
## 6.4.0
## 6.3.6
## 6.3.5
## 6.3.4
## 6.3.3
## 6.3.2
## 6.3.1
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tailwind",
"version": "6.3.2",
"version": "6.5.0",
"description": "Tailwind config based on @navikt/ds-tokens",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -25,7 +25,7 @@
"directory": "@navikt/core/tailwind"
},
"devDependencies": {
"@navikt/ds-tokens": "^6.3.2",
"@navikt/ds-tokens": "^6.5.0",
"color": "4.2.3",
"lodash": "^4.17.21",
"tailwindcss": "^3.3.3",
+14
View File
@@ -1,5 +1,19 @@
# @navikt/ds-tokens
## 6.5.0
## 6.4.1
## 6.4.0
## 6.3.6
## 6.3.5
## 6.3.4
## 6.3.3
## 6.3.2
## 6.3.1
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tokens",
"version": "6.3.2",
"version": "6.5.0",
"description": "Design-tokens for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
+138 -96
View File
@@ -1,13 +1,55 @@
# Changelog
## 6.5
### @navikt/ds-react
- Tabs: Ny prop `lazy` som rendrer innhold i TabPanel selv når panel er skjult (har fortsatt display:none) ([#2621](https://github.com/navikt/aksel/pull/2621))
- ToggleGroup: Erstattet bruk av dependency `@radix-ui/react-toggle-group` med egen implementasjon. ([#2620](https://github.com/navikt/aksel/pull/2620))
- Tabs: La til ny prop `fill` som lar Tabs.Tab-elementer strekke seg over tilgjengelig bredde. ([#2621](https://github.com/navikt/aksel/pull/2621))
- Tabs: Erstattet bruk av dependency `@radix-ui/react-tabs` med egen implementasjon. ([#2621](https://github.com/navikt/aksel/pull/2621))
* DatePicker/MonthPicker: Valgte datoer får nå `aria-pressed` for å bedre indikere valg for skjermleser. ([#2838](https://github.com/navikt/aksel/pull/2838))
* DatePicker/MonthPicker: `required`-prop stoppet ikke de-select av allerede valgt dato. ([#2838](https://github.com/navikt/aksel/pull/2838))
* ToggleGroup: La til ny prop `fill` som strekker ToggleGroup til å ta opp all tilgjengelig bredde. ([#2620](https://github.com/navikt/aksel/pull/2620))
## 6.4
### @navikt/ds-css
- FileUpload: Item-illustrasjon er nå radius-full ([#2834](https://github.com/navikt/aksel/pull/2834))
### @navikt/ds-react
- FileUpload.Item: :boom: Endret API og støtte for å sette ID på knappen ([#2824](https://github.com/navikt/aksel/pull/2824))
## 6.3
### @navikt/ds-css
- FileUpload: Ny komponent `FileUpload` for å håndtere opplasting av filer, Dropzone og fil-visning ([#2504](https://github.com/navikt/aksel/pull/2504))
- ReadMore: Padding-right på button er bumpet noen px ([#2806](https://github.com/navikt/aksel/pull/2806))
* FileUpload: Ny komponent `FileUpload` for å håndtere opplasting av filer, Dropzone og fil-visning ([#2504](https://github.com/navikt/aksel/pull/2504))
### @navikt/ds-react
- DatePicker: Fjernet role gridcell fra dato-knapper. ([#2826](https://github.com/navikt/aksel/pull/2826))
* Modal: Fjern tomt element ([#2819](https://github.com/navikt/aksel/pull/2819))
- Switch: Fikset edgecase der checked Switch var visuelt unchecked ([#2810](https://github.com/navikt/aksel/pull/2810))
- FileUpload.Dropzone: id prop settes på button ([#2817](https://github.com/navikt/aksel/pull/2817))
* FileUpload.Dropzone: Fiks feil med duplisert id ([#2805](https://github.com/navikt/aksel/pull/2805))
* useDatePicker: `isInvalid`-key i onValidate var hadde flipper boolean i noen tilfeller ([#2809](https://github.com/navikt/aksel/pull/2809))
- FileUpload: Fiks feil ved import av json ([#2803](https://github.com/navikt/aksel/pull/2803))
* FileUpload: Disabled dropzone stopper opplasting ([#2798](https://github.com/navikt/aksel/pull/2798))
@@ -18,16 +60,16 @@
## 6.2
### @navikt/ds-react
- Select: Fjern 'multiple' fra SelectProps ([#2782](https://github.com/navikt/aksel/pull/2782))
### @navikt/aksel-icons
- Ikoner: Nytt ikon `archive` ([#2772](https://github.com/navikt/aksel/pull/2772))
- Ikoner: Nytt ikon `opensource` ([#2787](https://github.com/navikt/aksel/pull/2787))
### @navikt/ds-react
- Select: Fjern 'multiple' fra SelectProps ([#2782](https://github.com/navikt/aksel/pull/2782))
## 6.1
### @navikt/ds-css
@@ -50,9 +92,11 @@
## 6.0
### @navikt/ds-tailwind
### @navikt/aksel-icons
- Tokens: La til nytt brekkpunkt `2xl` for `1440px`. Alle primitives er oppdatert for å reflektere oppdateringen. Brukere av Tailwind vil måtte sjekke at overskriving av `screen: "2xl"` ikke brekker app. ([#2623](https://github.com/navikt/aksel/pull/2623))
- ESM: ds-react og aksel-icons støtter nå native ESM 🎉 ([#2623](https://github.com/navikt/aksel/pull/2623))
* SSR: ds-react og aksel-icons støtter nå `use client` og kan brukes i nextjs app-router uten lokal re-eksportering. ([#2623](https://github.com/navikt/aksel/pull/2623))
### @navikt/ds-css
@@ -66,6 +110,10 @@
- Textarea: Fjernet div med klassen navds-textarea\_\_wrapper ([#2623](https://github.com/navikt/aksel/pull/2623))
### @navikt/ds-tailwind
- Tokens: La til nytt brekkpunkt `2xl` for `1440px`. Alle primitives er oppdatert for å reflektere oppdateringen. Brukere av Tailwind vil måtte sjekke at overskriving av `screen: "2xl"` ikke brekker app. ([#2623](https://github.com/navikt/aksel/pull/2623))
### @navikt/ds-tokens
- Tokens: La til nytt brekkpunkt `2xl` for `1440px`. Alle primitives er oppdatert for å reflektere oppdateringen. Brukere av Tailwind vil måtte sjekke at overskriving av `screen: "2xl"` ikke brekker app. ([#2623](https://github.com/navikt/aksel/pull/2623))
@@ -94,19 +142,13 @@
* SSR: ds-react og aksel-icons støtter nå `use client` og kan brukes i nextjs app-router uten lokal re-eksportering. ([#2623](https://github.com/navikt/aksel/pull/2623))
### @navikt/aksel-stylelint
- Textarea: Fjernet div med klassen navds-textarea\_\_wrapper ([#2623](https://github.com/navikt/aksel/pull/2623))
### @navikt/aksel
- Chat: Fjernet deprecated props `backgroundColor` og `avatarBgColor`. Bruk prop `variant` som erstatning. ([#2623](https://github.com/navikt/aksel/pull/2623))
### @navikt/aksel-icons
### @navikt/aksel-stylelint
- ESM: ds-react og aksel-icons støtter nå native ESM 🎉 ([#2623](https://github.com/navikt/aksel/pull/2623))
* SSR: ds-react og aksel-icons støtter nå `use client` og kan brukes i nextjs app-router uten lokal re-eksportering. ([#2623](https://github.com/navikt/aksel/pull/2623))
- Textarea: Fjernet div med klassen navds-textarea\_\_wrapper ([#2623](https://github.com/navikt/aksel/pull/2623))
## 5.18
@@ -148,32 +190,32 @@
## 5.16
### @navikt/ds-tailwind
### @navikt/aksel-icons
- Tailwind: La til ny `maxWidth` for text ([#2652](https://github.com/navikt/aksel/pull/2652))
- Ikoner: Nytt ikon `LocationPin` ✨ ([#2672](https://github.com/navikt/aksel/pull/2672))
### @navikt/ds-css
- :sparkles: Combobox: Mulighet for å begrense hvor mange valg bruker kan ta ([#2260](https://github.com/navikt/aksel/pull/2260))
### @navikt/ds-tailwind
- Tailwind: La til ny `maxWidth` for text ([#2652](https://github.com/navikt/aksel/pull/2652))
### @navikt/ds-react
- :sparkles: Combobox: Mulighet for å begrense hvor mange valg bruker kan ta ([#2260](https://github.com/navikt/aksel/pull/2260))
### @navikt/aksel-icons
- Ikoner: Nytt ikon `LocationPin` ✨ ([#2672](https://github.com/navikt/aksel/pull/2672))
## 5.15
### @navikt/ds-react
- HelpText: HelpText-state ble ikke riktig oppdatert ved klikk ([#2643](https://github.com/navikt/aksel/pull/2643))
### @navikt/aksel-icons
- Ikoner: Synket ikoner med Figma. Lagt til nytt ikon `FigureChild` ([#2629](https://github.com/navikt/aksel/pull/2629))
### @navikt/ds-react
- HelpText: HelpText-state ble ikke riktig oppdatert ved klikk ([#2643](https://github.com/navikt/aksel/pull/2643))
## 5.14
### @navikt/ds-css
@@ -214,6 +256,10 @@
## 5.12
### @navikt/aksel-icons
- Ikoner: Oppdatert ikonpakke med diverse bugdfixes og oppdateringer av stroke ([#2538](https://github.com/navikt/aksel/pull/2538))
### @navikt/ds-css
- ExpansionCard: Divider mellom header og innhold ([#2566](https://github.com/navikt/aksel/pull/2566))
@@ -234,11 +280,11 @@
- Stylelint: Støtter nå stylelint v16 ([#2591](https://github.com/navikt/aksel/pull/2591))
## 5.11
### @navikt/aksel-icons
- Ikoner: Oppdatert ikonpakke med diverse bugdfixes og oppdateringer av stroke ([#2538](https://github.com/navikt/aksel/pull/2538))
## 5.11
- Ikoner: Nytt ikon `PushPin` ([#2503](https://github.com/navikt/aksel/pull/2503))
### @navikt/ds-css
@@ -280,16 +326,8 @@
- :wheelchair: Textarea: Forsinkelse ved live-opplesning av gjenstående tegn ([#2502](https://github.com/navikt/aksel/pull/2502))
### @navikt/aksel-icons
- Ikoner: Nytt ikon `PushPin` ([#2503](https://github.com/navikt/aksel/pull/2503))
## 5.10
### @navikt/ds-tailwind
- typescript types for tailwind preset ([#2486](https://github.com/navikt/aksel/pull/2486))
### @navikt/ds-css
- :bug: Textarea: Teller flyttet ut av tekstfeltet for å unngå overlapp og misforståelser ([#2483](https://github.com/navikt/aksel/pull/2483))
@@ -298,6 +336,10 @@
- Primitives: Ny komponent `Page` og `Page.Block` ([#2456](https://github.com/navikt/aksel/pull/2456))
### @navikt/ds-tailwind
- typescript types for tailwind preset ([#2486](https://github.com/navikt/aksel/pull/2486))
### @navikt/ds-react
- Gjør lukknapp i Alert til type button ([#2497](https://github.com/navikt/aksel/pull/2497))
@@ -360,6 +402,10 @@
## 5.7
### @navikt/aksel-icons
- Ikoner: Nye ikoner `ChevronLeftFirstCircle` og `ChevronRightLastCircle` ([#2365](https://github.com/navikt/aksel/pull/2365))
### @navikt/ds-css
- :lipstick: Chips.Toggle: Ikon i uvalgt tilstand ([#2395](https://github.com/navikt/aksel/pull/2395))
@@ -400,11 +446,15 @@
- :lipstick: CopyButton: Justert padding, gap og animasjon ([#2355](https://github.com/navikt/aksel/pull/2355))
## 5.6
### @navikt/aksel-icons
- Ikoner: Nye ikoner `ChevronLeftFirstCircle` og `ChevronRightLastCircle` ([#2365](https://github.com/navikt/aksel/pull/2365))
- Ikoner: SVG export setter nå `height="1em"`, `width="1em"` og `fill="currentColor"`. ([#2300](https://github.com/navikt/aksel/pull/2300))
## 5.6
- Ikoner: Nye ikoner `book` og `books` ([#2313](https://github.com/navikt/aksel/pull/2313))
* Ikoner: Nye ikoner `ClipboardCheckmark` og `ClipboardLink` ([#2307](https://github.com/navikt/aksel/pull/2307))
### @navikt/ds-css
@@ -456,14 +506,6 @@
- Remove tabIndex="-1" on Popover to fix VoiceOver navigation issue inside Popover ([#2303](https://github.com/navikt/aksel/pull/2303))
### @navikt/aksel-icons
- Ikoner: SVG export setter nå `height="1em"`, `width="1em"` og `fill="currentColor"`. ([#2300](https://github.com/navikt/aksel/pull/2300))
- Ikoner: Nye ikoner `book` og `books` ([#2313](https://github.com/navikt/aksel/pull/2313))
* Ikoner: Nye ikoner `ClipboardCheckmark` og `ClipboardLink` ([#2307](https://github.com/navikt/aksel/pull/2307))
## 5.5
### @navikt/ds-css
@@ -504,6 +546,10 @@
## 5.3
### @navikt/aksel-icons
- :bug: SVG import map er nå riktig path ([#2250](https://github.com/navikt/aksel/pull/2250))
### @navikt/ds-css
- Alert: 12px -> 8px gap mellom tekst og ikon ved `size="small"` ([#2240](https://github.com/navikt/aksel/pull/2240))
@@ -540,11 +586,11 @@
* :wheelchair: Textarea: Skjermleser-spesifikk tekst leses opp sammenhengende ([#2216](https://github.com/navikt/aksel/pull/2216))
## 5.2
### @navikt/aksel-icons
- :bug: SVG import map er nå riktig path ([#2250](https://github.com/navikt/aksel/pull/2250))
## 5.2
- Ikoner: Oppdatert ikonpakke ([#2197](https://github.com/navikt/aksel/pull/2197))
### @navikt/ds-css
@@ -562,10 +608,6 @@
- Hovering over combobox dropdown will move selection/focus in the list, so we don't end up with a split focus, and reversely when moving focus while hovering ([#2193](https://github.com/navikt/aksel/pull/2193))
### @navikt/aksel-icons
- Ikoner: Oppdatert ikonpakke ([#2197](https://github.com/navikt/aksel/pull/2197))
## 5.1
### @navikt/ds-css
@@ -775,18 +817,18 @@
## 4.4
### @navikt/ds-tailwind
- Shadow-tokens er oppdatert ([PR](https://github.com/navikt/aksel/pull/2041))
- Oppdatert text-subtle og icon-subtle tokens til 700-skala (tidligere 600). ([PR](https://github.com/navikt/aksel/pull/2036))
### @navikt/ds-css
- :lipstick: Chat: fjernet border, satt avatar svg til 24x24px, byttet om "subtle" og "neutral" ([#2077](https://github.com/navikt/aksel/pull/2077))
* Shadow-tokens er oppdatert: Datepicker har nå ikke border, Modal bruker shadow-xlarge, LinkCard bruker shadow-xsmall ([PR](https://github.com/navikt/aksel/pull/2041))
### @navikt/ds-tailwind
- Shadow-tokens er oppdatert ([PR](https://github.com/navikt/aksel/pull/2041))
- Oppdatert text-subtle og icon-subtle tokens til 700-skala (tidligere 600). ([PR](https://github.com/navikt/aksel/pull/2036))
### @navikt/ds-tokens
- Shadow-tokens er oppdatert til mer tydeligere varianter ([PR](https://github.com/navikt/aksel/pull/2041))
@@ -821,9 +863,9 @@
## 4.1
### @navikt/ds-tailwind
### @navikt/aksel-icons
- :tada: Fargetokens for datavisualisering. ([PR](https://github.com/navikt/aksel/pull/2032))
- Nye ikoner `ChevronRightLast` og `ChevronLeftLast` ([PR](https://github.com/navikt/aksel/pull/2054))
### @navikt/ds-css
@@ -835,6 +877,10 @@
- Alle description-felter på fieldsets har nå `text-subtle` som farge. ([PR](https://github.com/navikt/aksel/pull/2036))
### @navikt/ds-tailwind
- :tada: Fargetokens for datavisualisering. ([PR](https://github.com/navikt/aksel/pull/2032))
### @navikt/ds-tokens
- :tada: Fargetokens for datavisualisering. ([PR](https://github.com/navikt/aksel/pull/2032))
@@ -869,10 +915,6 @@
- Deprecated klassenavn `navds-chips--icon-left` ([PR](https://github.com/navikt/aksel/pull/2035))
### @navikt/aksel-icons
- Nye ikoner `ChevronRightLast` og `ChevronLeftLast` ([PR](https://github.com/navikt/aksel/pull/2054))
## 4.0
### @navikt/ds-css
@@ -925,6 +967,10 @@
## 3.2
### @navikt/aksel-icons
- Nye ikoner `FileParagraph` og `FilePlus` ([PR](https://github.com/navikt/aksel/pull/1998))
### @navikt/ds-css
- Datepicker: Fikset small-variant av datepicker-input. ([PR](https://github.com/navikt/aksel/pull/1993))
@@ -943,10 +989,6 @@
* Ny komponent CopyButton! Erstatter `CopyToClipboard` som nå er tagget som deprecated ([PR](https://github.com/navikt/aksel/pull/1982))
### @navikt/aksel-icons
- Nye ikoner `FileParagraph` og `FilePlus` ([PR](https://github.com/navikt/aksel/pull/1998))
## 3.1
### @navikt/ds-css
@@ -979,6 +1021,16 @@
## 2.9
### @navikt/aksel-icons
- Oppdatert Statusikoner til å være tydeligere og mer konsistente. ([PR](https://github.com/navikt/aksel/pull/1959))
* Fikset `CheckmarkIcon`-bug
- Fikset `CheckmarkCircleIcon`-bug ([PR](https://github.com/navikt/aksel/pull/1956))
* Nye ikoner `BulletList` og `NumberList` ([PR](https://github.com/navikt/aksel/pull/1950))
### @navikt/ds-css
- CSS nå tilgjengelig som separate filer: Kan lastest fra CDN, minified-versjoner tilgjengelig ([PR](https://github.com/navikt/aksel/pull/1941))
@@ -991,17 +1043,19 @@
- :truck: Flyttet Codemods fra `@navikt/ds-codemods` -> `@navikt/aksel`. `@navikt/ds-codemods` regnes nå som deprecated ([PR](https://github.com/navikt/aksel/pull/1952))
## 2.8
### @navikt/aksel-icons
- Oppdatert Statusikoner til å være tydeligere og mer konsistente. ([PR](https://github.com/navikt/aksel/pull/1959))
- :tada: Nye ikoner `HandKnot` og `HandKnotFilled` ([PR](https://github.com/navikt/aksel/pull/1928))
* Fikset `CheckmarkIcon`-bug
* Oppdatert `InformationSquare`-ikon ([PR](https://github.com/navikt/aksel/pull/1921))
- Fikset `CheckmarkCircleIcon`-bug ([PR](https://github.com/navikt/aksel/pull/1956))
- Oppdatert `VideoSlack`, `Questionmark` og `Information`-ikoner ([PR](https://github.com/navikt/aksel/pull/1909))
* Nye ikoner `BulletList` og `NumberList` ([PR](https://github.com/navikt/aksel/pull/1950))
* Nye ikoner `VideoSlash` og `VideoSlashFill`
## 2.8
- Fikset Sourcemap-bug
### @navikt/ds-css
@@ -1037,26 +1091,8 @@
* Ny komponent ExpansionCard! ([PR](https://github.com/navikt/aksel/pull/1820))
### @navikt/aksel-icons
- :tada: Nye ikoner `HandKnot` og `HandKnotFilled` ([PR](https://github.com/navikt/aksel/pull/1928))
* Oppdatert `InformationSquare`-ikon ([PR](https://github.com/navikt/aksel/pull/1921))
- Oppdatert `VideoSlack`, `Questionmark` og `Information`-ikoner ([PR](https://github.com/navikt/aksel/pull/1909))
* Nye ikoner `VideoSlash` og `VideoSlashFill`
- Fikset Sourcemap-bug
## 2.7
### @navikt/ds-css
- Tabs: La til focus-markering for Tabs.Panel ([PR](https://github.com/navikt/aksel/pull/1863))
* ToggleGroup: Fikset token-bug
### @navikt/aksel-icons
- Nye ikoner `CaretUpDownFilledDown`, `CaretUpDownFilledUp`, `CloudDown`, `CloudDownFill`, `CloudUp`, `CloudUpFill`, `HddDown`, `HddDownFill`, `HddUp`, `HddUpFill`, `Inbox`, `InboxDown`, `InboxDownFill`, `InboxFill`, `InboxUp`, `InboxUpFill`, `ShoppingBasket`, `ShoppingBasketFill`
@@ -1065,6 +1101,12 @@
- Ny ikonpakke med for core icons 3! `@navikt/aksel-icons` ([PR](https://github.com/navikt/aksel/pull/1847))
### @navikt/ds-css
- Tabs: La til focus-markering for Tabs.Panel ([PR](https://github.com/navikt/aksel/pull/1863))
* ToggleGroup: Fikset token-bug
## 2.6
### @navikt/ds-react
@@ -1161,10 +1203,6 @@
## 2.0
### @navikt/ds-tailwind
- Alle token er oppdatert til nytt format. ([Guide](https://aksel.nav.no/grunnleggende/kode/migrering#h76f47744d112))
### @navikt/ds-css
- Radio: Fikset default visuell error-state ([PR](https://github.com/navikt/aksel/pull/1737))
@@ -1177,6 +1215,10 @@
* Tokens: Alle komponenter bruker nå semantiske tokens for som standard, med innebygd støtte for komponent-spesifikke tokens.
### @navikt/ds-tailwind
- Alle token er oppdatert til nytt format. ([Guide](https://aksel.nav.no/grunnleggende/kode/migrering#h76f47744d112))
### @navikt/ds-tokens
- Alle token er oppdatert til nytt format. ([Guide](https://aksel.nav.no/grunnleggende/kode/migrering#h76f47744d112))
+2 -1
View File
@@ -4,8 +4,9 @@ ENV NODE_ENV production
WORKDIR /app
COPY aksel.nav.no/website/.next/standalone ./
COPY aksel.nav.no/website/config/next-logger.config.js ./
COPY aksel.nav.no/website/next-logger.config.js ./
ENV NODE_ENV=production
ENV NODE_OPTIONS '-r next-logger'
EXPOSE 3000
+3
View File
@@ -21,6 +21,9 @@ spec:
outbound:
external:
- host: api.sanity.io
- host: api.slack.com
- host: slack.com
- host: wss-primary.slack.com
- host: hnbe3yhs.api.sanity.io
- host: apicdn.sanity.io
- host: hnbe3yhs.apicdn.sanity.io
+3
View File
@@ -25,6 +25,9 @@ spec:
outbound:
external:
- host: api.sanity.io
- host: api.slack.com
- host: slack.com
- host: wss-primary.slack.com
- host: hnbe3yhs.api.sanity.io
- host: apicdn.sanity.io
- host: hnbe3yhs.apicdn.sanity.io
+5 -5
View File
@@ -15,10 +15,10 @@
"playroom"
],
"dependencies": {
"@navikt/aksel-icons": "^6.3.2",
"@navikt/ds-css": "^6.3.2",
"@navikt/ds-react": "^6.3.2",
"@navikt/ds-tailwind": "^6.3.2",
"@navikt/ds-tokens": "^6.3.2"
"@navikt/aksel-icons": "^6.5.0",
"@navikt/ds-css": "^6.5.0",
"@navikt/ds-react": "^6.5.0",
"@navikt/ds-tailwind": "^6.5.0",
"@navikt/ds-tokens": "^6.5.0"
}
}
+1 -1
View File
@@ -11,7 +11,7 @@
"babel-loader": "^9.1.3",
"babel-plugin-preval": "^5.1.0",
"css-loader": "^6.8.1",
"playroom": "^0.32.1",
"playroom": "^0.37.0",
"style-loader": "^3.3.3"
},
"dependencies": {
+1 -1
View File
@@ -13,7 +13,7 @@ module.exports = {
title: "Aksel Sandbox",
snippets: path.resolve("./src/snippets.ts"),
widths: [320, 480, 768, 1024, 1280, 1440],
defaultWidths: [320, 1280], // will be supported if this PR is merged: https://github.com/seek-oss/playroom/pull/302
defaultVisibleWidths: [320, 1280],
port: 9000,
frameComponent: path.resolve("./src/FrameComponent.tsx"),
useScope: path.resolve("./src/useScope.tsx"),
+13 -5
View File
@@ -12,10 +12,10 @@ function getAbsolutePath(value: string): any {
}
const config: StorybookConfig = {
storyIndexers: (indexers) => {
experimental_indexers: (indexers) => {
const csfIndexer = async (fileName: string, opts) => {
const code = readFileSync(fileName, "utf-8").toString();
return loadCsf(code, { ...opts, fileName }).parse();
return loadCsf(code, { ...opts, fileName }).parse().indexInputs;
};
const exampleIndexer = async (fileName: string, opts) => {
@@ -32,18 +32,18 @@ const config: StorybookConfig = {
.split("pages/eksempler/")[1]
.replace(".tsx", "")}" };\n`;
return loadCsf(code, { ...opts, fileName }).parse();
return loadCsf(code, { ...opts, fileName }).parse().indexInputs;
};
return [
...(indexers || []),
{
test: /(stories|story)\.[tj]sx?$/,
indexer: csfIndexer,
createIndex: csfIndexer,
},
{
test: /pages\/eksempler\/|(".+")|(.+.tsx)$/,
indexer: exampleIndexer,
createIndex: exampleIndexer,
},
];
},
@@ -99,6 +99,14 @@ const config: StorybookConfig = {
__dirname,
"../components/layout/",
);
sbConfig.resolve.alias["@/auth"] = resolve(
__dirname,
"../components/auth/",
);
sbConfig.resolve.alias["@/slack"] = resolve(
__dirname,
"../components/utils/slack/index.ts",
);
sbConfig.resolve.alias["@/assets"] = resolve(
__dirname,
"../components/assets/",
@@ -0,0 +1,32 @@
import { NextApiRequest, NextApiResponse } from "next/types";
export type ApiHandler = (
req: NextApiRequest,
res: NextApiResponse,
) => Promise<unknown> | unknown;
export type AuthUser = {
name: "name";
email: "email";
};
export const AuthApiErrorReturn = {
"Access denied": "Access denied",
} as const;
export type AuthApiErrorReturn = {
ok: false;
error: (typeof AuthApiErrorReturn)[keyof typeof AuthApiErrorReturn];
};
type LoggedInT = {
signedIn: true;
user: AuthUser;
};
type NotLoggedInT = {
signedIn: false;
user: null;
};
export type UserStateT = LoggedInT | NotLoggedInT;
@@ -1,11 +1,6 @@
import { NextApiRequest, NextApiResponse } from "next/types";
import { logger } from "@navikt/next-logger";
import { ApiHandler } from "@/auth/auth.types";
import { validateWonderwallToken } from "@/auth/validateWonderwall";
import { logger } from "../../config/logger";
type ApiHandler = (
req: NextApiRequest,
res: NextApiResponse,
) => Promise<unknown> | unknown;
/**
* Used to authenticate Next.JS pages. Assumes application is behind
@@ -1,15 +1,13 @@
import { NextApiRequest } from "next/types";
import { logger } from "@navikt/next-logger";
import { AuthUser } from "@/auth/auth.types";
export function getAuthUser(headers: NextApiRequest["headers"]): {
name: string | null;
email: string | null;
} {
export function getAuthUser(
headers: NextApiRequest["headers"],
): AuthUser | null {
const token = getToken(headers);
if (!token) {
return {
name: null,
email: null,
};
return null;
}
const jwt = JSON.parse(
@@ -26,7 +24,12 @@ export const fakeToken =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjlHbW55RlBraGMzaE91UjIybXZTdmduTG83WSJ9.eyJhdWQiOiJmYWtlLWF1ZCIsImlzcyI6Imh0dHBzOi8vbG9naW4ubWljcm9zb2Z0b25saW5lLmNvbS85NjZhYzU3Mi1mNWI3LTRiYmUtYWE4OC1jNzY0MTljMGY4NTEvdjIuMCIsImlhdCI6MTY5ODQxMTU0NiwibmJmIjoxNjk4NDExNTQ2LCJleHAiOjE2OTg0MTY1MjYsImFpbyI6ImZha2UtYWlvIiwiYXpwIjoiZmFrZS1henAiLCJhenBhY3IiOiIyIiwiZ3JvdXBzIjpbImZha2UtZ3JvdXAiXSwibmFtZSI6IkZha2UgVG9rZW5lbnNzb24iLCJvaWQiOiJmYWtlLW9pZCIsInByZWZlcnJlZF91c2VybmFtZSI6ImZha2UtbG9uZy1lbWFpbC1hdEBleGFtcGxlLm5vIiwicmgiOiJmYWtlLXJoLiIsInNjcCI6ImRlZmF1bHRhY2Nlc3MiLCJzdWIiOiJmYWtlLXN1YiIsInRpZCI6ImZha2UtdGlkIiwidXRpIjoiekY4R0VGemhfRTZya3gtYzNnNFNBQSIsInZlciI6IjIuMCIsIk5BVmlkZW50IjoiWjk5MjM4OSIsImF6cF9uYW1lIjoiZGV2LWdjcDpmYWtlLXRlYW06aGVsc2VzamVray1ib3QifQ.OOP_Q0ZcUI6jQzM1UON9mA8d8BU_GhioQ5OLuepwebg";
export function getToken(headers: NextApiRequest["headers"]) {
if (process.env.NODE_ENV !== "production") return fakeToken;
if (process.env.NODE_ENV !== "production") {
logger.info(
"Is running locally or in demo, using local fake token for Authenticated user-requests",
);
return fakeToken;
}
return headers.authorization?.replace("Bearer ", "") ?? null;
}
@@ -0,0 +1,35 @@
import { NextApiRequest } from "next/types";
import { logger } from "@navikt/next-logger";
import { UserStateT } from "@/auth/auth.types";
import { getAuthUser } from "@/auth/getAuthUser";
import { validateWonderwallToken } from "@/auth/validateWonderwall";
export async function getAuthUserState(
headers: NextApiRequest["headers"],
): Promise<UserStateT> {
const isSignedIn = await validateWonderwallToken(headers);
if (!isSignedIn) {
return {
signedIn: false,
user: null,
};
}
const user = getAuthUser(headers);
if (!user) {
logger.error(
"User is signed in, but no user was found in the token. This should be impossible!",
);
return {
signedIn: false,
user: null,
};
}
return {
signedIn: true,
user,
};
}
Loaded 100 of 247 files, more files were not shown because too many files have changed in this diff. Show more