Compare commits

...
Author SHA1 Message Date
github-actions[bot]andgithub-actions[bot] c8c3fc4ec5 Version Packages (#2896)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-10 14:37:43 +02:00
Andreas Nordahl 834ba0cbae Decouple read-more icon hover offset and rotation style (#2912) 2024-05-10 13:53:03 +02:00
KenandHalvor Haugan be653cc67a [Aksel.nav.no] Mindre justeringer på Søk (#2899)
* refactor: Fjernet tags fra søk

* design: Oppdatert padding for søk og la tilbake border på focus

* refactor: Fjernet lenketags fra komponent-artikler

* design: Oppdatert fokusmarkering og hover for search-hit

* bug: Mobiltilpasset søk-headings

* bug: e2e-tester fant ikke søkefelt

* bug: Seksjoner i søk har nå riktig heading-level h2

* Update aksel.nav.no/website/components/website-modules/search/parts/Hit.tsx

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

* refactor: Quotes i søk får samme farge

* Update aksel.nav.no/website/components/website-modules/search/parts/Results.tsx

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

* refactor: Unødvendig filtrering basert på dokumenttype er fjernet

* bug: Bruker section-element for gruppering av søkeresultater

* refactor: Unødvendig filtrering basert på dokumenttype er fjernet

* bug: Border forsvant ved pointerdown

* refactor: Hides image-frame from serch-hit

* Revert "refactor: Hides image-frame from serch-hit"

This reverts commit 764c82ebdb.

* refactor: Truncate title

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-05-08 10:08:57 +02:00
Halvor Haugan 054835d1dd 🆙 Oppgradert deps (#2908) 2024-05-08 09:53:14 +02:00
Ken 7f99d407c0 Combobox refactor: Flyttet Input controls ut i egen komponent (#2900)
* refactor: Flyttet Input controls ut i egen komponent

* refactor: Inlinet clearbutton implementasjon, fikset typer

* 🚚 Flyttet ToggleListButton til input-mappe

* Combobox refactor: Bruker intern `createContext` for context-creation i Combobox (#2902)

* refactor: Bruker egen creactContext for selectedOptionsContext

* refactor: Bruker egen creactContext for filteredOptionsContext

* refactor: Bruker egen creactContext for InputContext

* refactor: Bruker egen creactContext for selectedOptionsContext

* refactor: Rename InputContext hook

* 🚚 Flyttet testfiler inn i egen mappe (#2905)
2024-05-08 09:05:02 +02:00
Ken 1202e0a6f7 bug: InitialValues for god-praksis struktur i admin ga ikke riktige tilganger 2024-05-07 18:22:56 +02:00
0c06fa12d0 Progressbar tweaks (etter docs-møtet) (#2892)
Co-authored-by: Julian Nymark <julian.nymark@nav.no>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-05-07 14:36:11 +02:00
Lars Eirik Korsgaard Hansen 4797ccae9d Combobox: Remove 'Ingen søketreff' when custom options allowed (#2895) 2024-05-07 11:41:01 +02:00
Ken ac06d882a7 refactor: Forenklet mergeRefs i floating-util 2024-05-07 09:57:20 +02:00
Ken 8f5c956bad Floating utility (#2872)
* ✨ Initial Popper wrapper

* ✨ Floating popper element

* 🏷️ Rename popper -> floating

* 🐛 Fikset imports

* ♻️ FLoating-fil splittet opp i moduler

* 🎨 Satt opp exports og story

* 🎨 Arrow er nå del av content

* 🐛 display block på arrow

* 🎉 Egen Portal-komponent

* 🎨 Portal og Overlay exportert i root

* ♻️ Tooltip bruker egen Portal

* 📝 Changeset

* 🔀 Merge Portal

* 📝 Story with portal

* 📝 Props story

* 🚚 Temp rename

* 🚚 Temp rename

* 🎨 La til Slot på Floating.Anchor

* ✨ Fungerende nesting av dismissable items

* ✨ Fungerende outsidepointerclick handler

* 🚚 Ryddet opp i struktur

* ✨ bedre event-handling

* 🎉 Popover bruker dismissableLayer

* 🔥 Fjernet console.logs

* ♻️ Extender nå ikke htmldivelement

* 📝 Commented on dimsissablelayer

* 📝 Kommentarer

* 🎨 Disable pointer-events on body

* 🎨 Setup content

* 🎨 Fungerende popover med story

* 🎨 useDescendant-API kan bruke uten context-error på nesting

* 🐛 Fikset tilfeller der oppdatering av descendant-options ikke ble reflektert i descendants-map

* 🐛 Satte enabled til optional

* bug: import av Portal brakk stories for Popover og Floating

* bug: DescenadantContext er nå riktig typet som undefined

* label: scopet navn for bruk av descendant-provider i dismissablelayer

* memo: Bedre kommentarer

* feat: Sender ikke callbacks hvis layer er disabled

* memo: Formatering og kommentarer

* memo: Kommentarer

* feat: Støtter forwardRef

* merge: Merge branch with main

* 🔥 Fjernet ny popover implementasjon

* 🔥 Fjernet mergeconflict fra main i tabs

* 🔥 Fjernet mergeconflict fra main i tabs

* refactor: Cleaniup codestructure

* refactor: Fjernet unødvendige props

* refactor: Synket dependencies og changeset
2024-05-07 09:56:06 +02:00
Ken 6b40a0ddd8 refactor: Oppdatert stories til chromatic format
refactor: Oppdatert HGrid stories til chromatic format

refactor: Oppdatert InternalHeader stories til chromatic format

refactor: Oppdatert skeleton stories til chromatic format

refactor: Oppdatert box stories til chromatic format

refactor: Oppdatert loader stories til chromatic format

refactor: Oppdatert list stories til chromatic format

refactor: Oppdatert tag stories til chromatic format
2024-05-06 16:28:12 +02:00
Halvor Haugan 60a6cba329 Justering av stories på Chat (#2903) 2024-05-06 14:24:23 +02:00
Ken 7fd62f3b79 logging: Fjernet logging av query i søk (#2897) 2024-05-06 11:50:02 +02:00
Ken 0944bbc256 DismissableLayer-API (#2814)
* ✨ Fungerende nesting av dismissable items

* ✨ Fungerende outsidepointerclick handler

* 🚚 Ryddet opp i struktur

* ✨ bedre event-handling

* 🎉 Popover bruker dismissableLayer

* 🔥 Fjernet console.logs

* ♻️ Extender nå ikke htmldivelement

* 📝 Commented on dimsissablelayer

* 📝 Kommentarer

* 🎨 Disable pointer-events on body

* 🎨 useDescendant-API kan bruke uten context-error på nesting

* 🐛 Fikset tilfeller der oppdatering av descendant-options ikke ble reflektert i descendants-map

* 🐛 Satte enabled til optional

* bug: DescenadantContext er nå riktig typet som undefined

* label: scopet navn for bruk av descendant-provider i dismissablelayer

* memo: Bedre kommentarer

* feat: Sender ikke callbacks hvis layer er disabled

* memo: Formatering og kommentarer

* memo: Kommentarer

* feat: Støtter forwardRef

* ♻️ Ryddet i DismissableLayer filer

* ♻️ Fikset edgecase der 'disabled'-noder kunne stoppe pointer-events

* refactor: Cleanup PR før release

* bug: useDismissableDescendantsContext ikke required
2024-05-04 15:15:15 +02:00
Ken 16087a458a refactor: DescendantContext er nå scopet (#2890)
* refactor: DescendantContext er nå scopet

* ♻️ Fjernet unødvendig wrapping
2024-05-04 13:34:34 +02:00
KenandHalvor Haugan c47c372d0f [Datepicker|MonthPicker] Flippet visning av år. (#2882)
* feat: DropdownCaption viser år i motsatt rekkefølge for select

* feat: Year-select for MonthPicker er viser nyeste år først

* memo: Changeset

* Update .changeset/thick-poems-design.md

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

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-05-04 13:33:59 +02:00
Ken 0c479831df bug: Nettsidebygg feilet ved bruk av fjernet prop 2024-05-02 13:27:50 +02:00
github-actions[bot]andgithub-actions[bot] 41e9fbd2a5 Version Packages (#2878)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-05-02 12:35:49 +02:00
Ken 7b25f30e60 🏷️ Fjernet referanse til duration i Progressbar for pre-release 2024-05-02 12:32:31 +02:00
Ken db9c858153 refactor: Radio og Checkbox bruker :focus for bedre synlighet når de blir lenket til fra ErrorSummary (#2893) 2024-05-02 12:18:49 +02:00
Julian Nymark b21015bf54 💄 fix for broken pin style (#2889)
* 💄 fix for broken pin style

As content (Rows) grows, the pin would get pushed down (and under) the content.

* changeset

* 💄 make css for timeline pin even more compact
2024-04-29 19:13:57 +02:00
Lars Eirik Korsgaard Hansen bac48d9057 Progressbar aria hidden (#2888) 2024-04-29 14:47:29 +02:00
Lars Eirik Korsgaard Hansen e1a85dfbeb Progressbar demo-fixes (#2887) 2024-04-29 12:40:09 +02:00
Lars Eirik Korsgaard Hansen 3f65eee9e9 Progressbar demos (#2885) 2024-04-29 10:53:02 +02:00
Halvor Haugan cd77198f1c [aksel.nav.no] Fiks anker i søketreff-lenker (#2884) 2024-04-29 09:02:58 +02:00
Halvor Haugan 82827ec712 Justering av eksempler på Label, Detail og ErrorMessage (#2883) 2024-04-29 09:02:34 +02:00
Ken 710ed83661 [Aksel.nav.no] Ny Meny for sidebar og TOC (#2873)
* 🎉 WIP for ny sidebar og TOC meny

* 💫 Animert farger

* 🎨 Støtte for nested menuitems

* 🎨 Støtter nå sidebar og toc-variant for farger

* ♻️ Migrert TOC til å bruke ny meny

* ♻️ Migrert Sidebar til å bruke ny meny

* 🐛 Fikset story

* ♿ Klikkhøyde nå minst 32px

* 💄 Justert bakgrunnsanimasjon fra 200ms -> 100ms

* Types: variant bruker semantiske navn

* bug: MenuLink ble brukt utenfor MenuLi
2024-04-26 07:58:47 +02:00
Ken 3b44a97725 design: Fallback/Alle tema piktogram er nå Aksel-logo (#2877) 2024-04-25 11:55:46 +02:00
Ken edcbd6e198 bug: Error-handling for oppretting av nye brukerprofiler i sanity (#2881) 2024-04-25 11:54:08 +02:00
Halvor Haugan bd3f9355eb storybook:aksel viser og sorterer nå på index på kodeeksempler (#2880) 2024-04-25 10:49:04 +02:00
Halvor Haugan f11bce1f8c Oppdater JSDOC og eksempler på Heading, BodyLong og BodyShort (#2879) 2024-04-25 10:16:00 +02:00
Lars Eirik Korsgaard HansenandHalvor Haugan 277525f39f ✨ Ny komponent: ProgressBar (#2845)
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-04-24 14:18:55 +02:00
Ken 210496c2cc bug: Validering av SEO-felt i sanity brakk ved undefined asset 2024-04-24 08:03:27 +02:00
github-actions[bot]andgithub-actions[bot] 8f317878fc Version Packages (#2875)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-22 15:48:59 +02:00
KenandHalvor Haugan c3064a01f7 🎨 Spacer er nå span (tidligere div) (#2874)
* 🎨 Spacer er nå span (tidligere div)

* 📝 Changeset

* Update .changeset/thick-humans-smile.md

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

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-04-22 15:32:57 +02:00
Ken 02f0a911b0 🔥 Fjernet gammel kode for god praksis og sanity (#2866)
* 🔥 Fjernet filtrering av aksel_tema

* 🔥 Fjernet gamle migreringer

* ⚰️ Fjernet aksel_tema og gammelt feedback-dokument

* ⚰️ Fjernet gammelt god praksis card

* ♻️ Gjenbruker nytt gp-card for relaterte artikler

* ♻️ Fjernet gamle typer

* 🔥 Fjernet referanser til gammelt feedback-dokument
2024-04-22 15:12:56 +02:00
Ken ee8524229f 🎨 Temakontakter får slack-melding gjennom tilbakemeldinger (#2869) 2024-04-19 13:36:16 +02:00
Ken d9932e203a [Sanity] Temakontakter visning (#2868)
* 🎨 Lagt til Temakontakter-felt i for gp.tema

* 🔥 Fjernet requirement for temakontakter

* 🎨 Visning for temakontakter
2024-04-19 12:58:14 +02:00
KenandHalvor Haugan b14fc23ecb [react] Collapsible component utility (#2859)
* ✨ Boilerplate for collapsible-component

* 🎨 Collapsible wrapper interface

* 🎨 Collapsible trigger interface

* 🎨 Collapsible content interface

* 🧪 Stories

* ♻️ Satt opp storybook for chromatic

* 📝 Satt opp exports

* 🧪 Open/defaultOpen tester for chromatic

* 📝 Kommentert props

* ♻️ Definerer data-state verdi på root

* 📝 JSDOC

* 📝 Disabled-demo

* ♻️ aria-controls blir bare satt når open

* ♿ Content controls button

* Update @navikt/core/react/src/collapsible/Collapsible.types.ts

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

* ♻️ props -> rest

* ♻️ props -> rest

* ♻️ Omit unreachable props

* 📝 _id rename

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-04-19 12:27:55 +02:00
Ken c16f75d6f8 [Sanity] Temakontakter (#2867)
* 🎨 Lagt til Temakontakter-felt i for gp.tema

* 🔥 Fjernet requirement for temakontakter
2024-04-19 08:42:22 +02:00
Halvor Haugan a01be91608 Rekkefølge på eksempler på FormSummary 2024-04-18 13:05:21 +02:00
github-actions[bot]andgithub-actions[bot] 8826a79021 Version Packages (#2850)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-18 11:59:36 +02:00
Vegard HaugstvedtandKen c2f3da84cc Bugfix/prevent submit on enter in combobox (#2861)
* Prevent "Enter" key in Combobox to...

* Added test for Enter not submitting form

* Added changeset

* Add test for un-selecting a selected option using "enter" on a Chip not submitting the form

* Do not prevent enter from submitting form if it should not perform another action (like selecting an option)

* Do not submit when the input field has a value

* Replace lots of sleep-calls with awaiting userEvents

* Create util function for looking up an option

* Fix bug where we couldn't un-select a custom option using "enter"

* Update test to account for the dropdown list closing when focus moves to the chips in the previous step

* Add possibility to slow down tests for debugging

* Fix issue with onFocus -> onToggleSelected(false), where clicking the open/close button didn't work

* Fix test

* Closing the FilteredOptionsList when focusing a Chip caused too many side effects, but we need to remove virtual focus at least. That way we won't double-click on enter.

* When no longer closing FilteredOptions on focusing a chip, we don't need to click the toggleIsOpen button to check that the option is un-selected

* Update .changeset/rotten-windows-cover.md

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

---------

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2024-04-18 10:00:16 +02:00
Ken 2c1476022d 🐛 Descendant-provider feilet ved bruk av cloneElement (#2832)
* 🐛 Når Descendant-provider ble brukt med react cloneElement feilet child-re-render

* ⚡ Synker options for allerede registrert node

* ⚡ Unngår flere map-lookups for samme funksjon
2024-04-18 09:55:34 +02:00
Halvor Haugan dacf648c47 FormSummary: Link til dokumentasjonen i JSDOC (#2865) 2024-04-18 09:06:29 +02:00
Ken 5ec986771b [God praksis] Nye sider erstatter gamle (#2858)
* 🚚 Byttet ut gamle god-praksis sider med nye

* ♻️ Sitemap bruker ny gp-struktur

* ♻️ Lister med tema tittler bruker nå nye tema

* ♻️ Flyttet gammel godpraksiscardsimple

* ⚰️ Markert gamle tema-felt i sanity som deprecated

* ♻️ Bedre håndtering av editor/profil visning

* ♻️ Velger nå ikke en default tab for sanity visning

* 🔒 ErrorBoundary på alle gp-cards

* 🧪 Storybook for frontpage-cards

* 🐛 Dialog var ikke riktig lablet

* 🐛 Chips-liste wrappet ikke button i li, cube-illustrasjon var ikke gjemt for skjermleser

* 🎉 Viser publiserings og oppdateringsdatoer i toppen av artikkel

* 📝 Bidragsytere -> Medvirkende

* 🐛 Playwright testet gamle god praksis sider

* 🐛 Preview brakk på forside og god-praksis

* 💄 HeroCards har større illustrasjon

* ♻️ Forenklet size på herocard

* ♻️ Innhold i herocard er alltid sentrert

* 🔍 Artikler har nå riktig fallback OG-image

* Update aksel.nav.no/website/e2e/test-urls.json

* 🏷️ bruker SanityImageSource for piktogram

* 🏷️ Tagget title i staticprops som deprecated

* 🔥 Fjernet testkode

* Update aksel.nav.no/website/sanity/plugins/structure/god-praksis.ts

* 📝 Skrivefeil i structure

* 💄 Oppdatert gap mellom tagger i cards
2024-04-18 07:28:08 +02:00
ca6af8a5f7 index & process templates -> stories (#2856)
* index & process templates -> stories

* ✨ add duplication of templates to component storybook

* 🎨 fix story

* ♻️ refactor indexers for storybook

* ♻️ layout fullscreen for templates

* 🔥 Fjernet meta.json filer

* ♻️ Trukket dekorator-kode ut av alle templates

* 🐛 Fjerner storybook stories fra npm output

* 🔥 Fjernet templates fra core/react

* 🔥 Fjernet console.log

---------

Co-authored-by: Ken <ken.aleksander@gmail.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2024-04-17 13:46:58 +02:00
Ken 9410840f74 🐛 Fikset lenke i sanity (#2863) 2024-04-17 13:22:42 +02:00
Ken d88884f785 🔥 Fjernet content-container.css og grid.css fra _mappings (#2860)
* 🔥 Fjernet content-container og grid css fra _mappings

* Update .changeset/wet-books-end.md
2024-04-17 09:46:27 +02:00
Ken 0c8239ee00 🐛 EditorPreview knapp Sanity admin 2024-04-16 12:48:06 +02:00
Halvor Haugan 987118fd47 FormSummary.Heading: Fjern overridable, level oblig. og css-justeringer på <sm (#2857) 2024-04-16 10:34:27 +02:00
8d13f11689 Summary component (docs tweaks) (#2853)
* 🚧 wip on Summary

* ✨ new HR component

* 👔 fancy utility function to inject components into children

This is to be used in 2 places where we iterate over children and add <hr /> components between them all.

It also wraps the children in a <Box>, which is used here and not really reflected in the name of the function 🤔

* 🎨 improved version of Summary component

* Remove (some) magic

* Fix children type in injectHRBetween

* css

* header gap and bg

* responsive story + example with long texts

* 🚚 Summary -> FormSummary

* 🎨 update stories

* 🎨 update story + tweak CSS for EditButton

* 📝 add JSDoc

* export FormSummary in core/react/src/index.ts

* FormSummaryValue bruker BodyLong

* 📝 update JSDoc

* 📝 add demo

* 📝 update README.md for eksempler

* forwardRef på Answers

* forwardRef på Header

* forwardRef på Label og Value

* forwardRef på Heading og Edit

* Answer export

* spread props

* 🔥 remove HR component

* 📝 update JSDoc

* 🎨 export more FormSummary

* 🔥 duplicate dependency of _mapping (same as main)

* 📝 update JSDoc for FormSummary

* changeset

* 🔖 changeset

* 🔥 duplicate changeset

* Rename EditButton -> Edit

* oops

* Link er vel valgfritt

* 🐛 add correct CSS dependency for FormSummary

* 💄 FormSummary

* 💄 FormSummary

* className navds-form-summary

* className navds-form-summary__answers

* className navds-form-summary__header

* Update @navikt/core/css/form/form-summary.css

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

* Update @navikt/core/react/src/form-summary/FormSummaryLabel.tsx

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

* Update @navikt/core/react/src/form-summary/FormSummaryValue.tsx

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

* Header wrapper på små skjermer

* Rekkefølgen på selectors samme som DOM

* 🎨 structure consistency (ref, then ...rest, then className, then other things)

- also convert a props -> {not_the_rest, ...rest}

* update mappings

* Flytt border slik at du kan ha flere dd etter hverandre

* 🎨 update stories

* 🎨 update stories (more empty values)

* 🚚 story ComplexData -> RealisticUsage

* Mindre padding på små skjermer

* 🎨 update stories

* Answer forwardRef

* 🎨 update stories

* Eksempel på linjeskift i value i story

* 🎨 update story

* 🎨 update story

* 🚚 FormSummary -> Form/FormSummary

* 🎨 update Demo and story

* 🏷️ make href required for FormSummaryEdit

* 🏷️ + 🚧 typing woes

* 💄 tweak CSS to make it look just right

* 🏷️ correct typing using Exclude > Omit

* 🎨 use div over section without label

* 💄 FormSummary, last-child padding removal

* Fjernet kommentar

* typo i readme

* 🚚 FormSummary.Edit -> FormSummary.EditLink

* 🏷️ make href for EditLink optional

* 🚚 rename FormSummaryEdit -> FormSummaryEditLink

* FormSummaryEdit -> FormSummaryEditLink

* ✨ add example to FormSummary

* 🎨 update examples

* 🎨 update examples

* 💄 FormSummary

* 🎨 types + jsdoc tweaks (FormSummary)

* 🔥 remove aria-label example

* 🎨 separate example for grouped data

* 🎨 update desc for example

* 🔥 unused example code

* 🚚 rename examples

---------

Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2024-04-15 13:54:08 +02:00
Ken 158e18454e 🚚 gp-sider lenker nå til god-praksis (#2852) 2024-04-15 13:53:40 +02:00
Ken be26bf22c9 🔥 Fjernet redirect (#2851) 2024-04-15 13:35:36 +02:00
Ken c2968a0e6d 🎨 Artikler med contributors får automatisk editor (#2849)
* 🎨 Artikler med contributors får automatisk editor lagt til ved creation

* 🔒 Lager bare profil automatisk for SSO-inlogging
2024-04-15 11:16:27 +02:00
KenandHalvor Haugan 3f92773273 [Sanity] Oppdatert deskStructure (#2844)
* 🚚 Flyttet bloggvisning til egen fil

* 🚚 Flyttet grunnleggende visning

* 🎨 Sorterer 'trenger oppdatering' etter eldst først

* 🎨 Sorterer 'trenger oppdatering' etter eldst først i god-praksis

* 🚚 Flyttet komponenter visning

* 🚚 Flyttet admin visning

* 💄 Endret ikon for utdaterte artikler. Dynamisk treshold for utdatert

* 📝 Skrivefeil

* 🐛 Skrivehjelp-tittel hadde for lav kontrast på darkmode i sanity

* 🔥 Fjernet unødvendig currenUser check

* ♻️ Fikset PR-kommentarer

* Update aksel.nav.no/website/sanity/plugins/structure/structure.util.ts

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

* 🐛 Sjekker mail for lowercase

* 🐛 email kan være undefined

* Update aksel.nav.no/website/sanity/plugins/structure/structure.util.ts

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

* ♻️ Bruker innebygd showIcon-funksjon

* 📝 Bedre gruppenavn for seksjoner i sanity

* 💄 Ikoner på landigssider

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-04-15 11:15:17 +02:00
Ken 0c3ab1d0e7 [Sanity] Oppdatert utdatertvarsel (#2846)
* 🎨 innholdstype og undertema er nå required

* ♻️ Bruker eget panel for utdatert-warning i sanity

* ⚡ Bruker sanity useCurrentUser for å vise tilganger til bruker på profilside

* Update aksel.nav.no/website/sanity/schema/custom-components/EditorPage.tsx

* 🔥 Fjernet rest-spread
2024-04-15 11:12:32 +02:00
3a73a029f6 ✨ new FormSummary component (#2802)
* 🚧 wip on Summary

* ✨ new HR component

* 👔 fancy utility function to inject components into children

This is to be used in 2 places where we iterate over children and add <hr /> components between them all.

It also wraps the children in a <Box>, which is used here and not really reflected in the name of the function 🤔

* 🎨 improved version of Summary component

* Remove (some) magic

* Fix children type in injectHRBetween

* css

* header gap and bg

* responsive story + example with long texts

* 🚚 Summary -> FormSummary

* 🎨 update stories

* 🎨 update story + tweak CSS for EditButton

* 📝 add JSDoc

* export FormSummary in core/react/src/index.ts

* FormSummaryValue bruker BodyLong

* 📝 update JSDoc

* 📝 add demo

* 📝 update README.md for eksempler

* forwardRef på Answers

* forwardRef på Header

* forwardRef på Label og Value

* forwardRef på Heading og Edit

* Answer export

* spread props

* 🔥 remove HR component

* 📝 update JSDoc

* 🎨 export more FormSummary

* 🔥 duplicate dependency of _mapping (same as main)

* 📝 update JSDoc for FormSummary

* changeset

* 🔖 changeset

* 🔥 duplicate changeset

* Rename EditButton -> Edit

* oops

* Link er vel valgfritt

* 🐛 add correct CSS dependency for FormSummary

* 💄 FormSummary

* 💄 FormSummary

* className navds-form-summary

* className navds-form-summary__answers

* className navds-form-summary__header

* Update @navikt/core/css/form/form-summary.css

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

* Update @navikt/core/react/src/form-summary/FormSummaryLabel.tsx

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

* Update @navikt/core/react/src/form-summary/FormSummaryValue.tsx

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

* Header wrapper på små skjermer

* Rekkefølgen på selectors samme som DOM

* 🎨 structure consistency (ref, then ...rest, then className, then other things)

- also convert a props -> {not_the_rest, ...rest}

* update mappings

* Flytt border slik at du kan ha flere dd etter hverandre

* 🎨 update stories

* 🎨 update stories (more empty values)

* 🚚 story ComplexData -> RealisticUsage

* Mindre padding på små skjermer

* 🎨 update stories

* Answer forwardRef

* 🎨 update stories

* Eksempel på linjeskift i value i story

* 🎨 update story

* 🎨 update story

* 🚚 FormSummary -> Form/FormSummary

* 🎨 update Demo and story

* 🏷️ make href required for FormSummaryEdit

* 🏷️ + 🚧 typing woes

* 💄 tweak CSS to make it look just right

* 🏷️ correct typing using Exclude > Omit

* 🎨 use div over section without label

* 💄 FormSummary, last-child padding removal

* Fjernet kommentar

* typo i readme

* 🚚 FormSummary.Edit -> FormSummary.EditLink

* 🏷️ make href for EditLink optional

* 🚚 rename FormSummaryEdit -> FormSummaryEditLink

* FormSummaryEdit -> FormSummaryEditLink

* ✨ add example to FormSummary

* 🎨 update examples

* 🎨 update examples

* 💄 FormSummary

* 🎨 types + jsdoc tweaks (FormSummary)

---------

Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2024-04-15 09:53:42 +02:00
Halvor Haugan 6fd0f501f5 Story med forced-colors på Button (#2847) 2024-04-15 09:24:07 +02:00
Ken a11b8ddf4e 🐛 Dokumentert bruk av full og overflow-x av bleed 2024-04-12 14:51:45 +02:00
Ken 171db6a813 [Sanity] Flyttet dev-område for ny god-praksis inn i "prod" (#2843)
* 🚚 Flyttet gp-taxonomy over til prod

* 🎉 Satt opp ny sanity-struktur for god-praksis

* 🔥 Fjernet gammel editor-visning

* 🎉 Oppdatert templates for ny sanity-visning av gp

* 🔥 Fjernet god-praksis-taxonomy struktur

Struktur er nå flyttet inn i main
2024-04-12 08:25:00 +02:00
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
417 changed files with 14766 additions and 12453 deletions

No files matched your search

+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"
+18
View File
@@ -1,5 +1,23 @@
# @navikt/aksel-icons
## 6.7.1
## 6.7.0
## 6.6.1
## 6.6.0
## 6.5.0
## 6.4.1
## 6.4.0
## 6.3.6
## 6.3.5
## 6.3.4
## 6.3.3
@@ -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.4",
"version": "6.7.1",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
+18
View File
@@ -1,5 +1,23 @@
# @navikt/aksel-stylelint
## 6.7.1
## 6.7.0
## 6.6.1
## 6.6.0
## 6.5.0
## 6.4.1
## 6.4.0
## 6.3.6
## 6.3.5
## 6.3.4
## 6.3.3
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "6.3.4",
"version": "6.7.1",
"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.4",
"@navikt/ds-tokens": "^6.3.4",
"@navikt/ds-css": "^6.7.1",
"@navikt/ds-tokens": "^6.7.1",
"concurrently": "7.2.1",
"postcss-selector-parser": "^6.0.13",
"postcss-value-parser": "^4.2.0",
+65
View File
@@ -1,5 +1,70 @@
# @navikt/aksel
## 6.7.1
### Patch Changes
- Updated dependencies [[`0c06fa12d`](https://github.com/navikt/aksel/commit/0c06fa12d0de4f3015be045d4e65e149ebd2c141)]:
- @navikt/ds-css@6.7.1
## 6.7.0
### Patch Changes
- Updated dependencies [[`db9c85815`](https://github.com/navikt/aksel/commit/db9c85815392528e59ef5082397ec14b1aa36e7a), [`277525f39`](https://github.com/navikt/aksel/commit/277525f39ff94c7ded1accf6031957efa3bdeb8b), [`b21015bf5`](https://github.com/navikt/aksel/commit/b21015bf54b4e19673b2849756c608ae323a9f70)]:
- @navikt/ds-css@6.7.0
## 6.6.1
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@6.6.1
## 6.6.0
### Patch Changes
- Aksel CLI: Fjernet deprecated komponenter fra css oversikt. ([#2860](https://github.com/navikt/aksel/pull/2860))
- Updated dependencies [[`3a73a029f`](https://github.com/navikt/aksel/commit/3a73a029f6272f76e10cb6baefbaa2040feaef6b)]:
- @navikt/ds-css@6.6.0
## 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
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "6.3.4",
"version": "6.7.1",
"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.4",
"@navikt/ds-css": "6.7.1",
"axios": "1.6.0",
"chalk": "4.1.0",
"clipboardy": "^2.3.0",
+40
View File
@@ -1,5 +1,45 @@
# @navikt/ds-css
## 6.7.1
### Patch Changes
- Progressbar: Tweak API, examples, stories and css ([#2892](https://github.com/navikt/aksel/pull/2892))
## 6.7.0
### Minor Changes
- :sparkles: Ny komponent: ProgressBar ([#2845](https://github.com/navikt/aksel/pull/2845))
### Patch Changes
- Checkbox, Radio: Erstatter `:focus-visible` med `:focus` for bedre UX brukt sammen med ErrorSummary. ([#2893](https://github.com/navikt/aksel/pull/2893))
- :lipstick: tweak CSS for timeline pins. ([#2889](https://github.com/navikt/aksel/pull/2889))
## 6.6.1
## 6.6.0
### Minor Changes
- :sparkles: Ny komponent FormSummary ([#2802](https://github.com/navikt/aksel/pull/2802))
## 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
+1 -1
View File
@@ -1,6 +1,6 @@
/* https://web.dev/prefers-reduced-motion/ */
@media (prefers-reduced-motion: reduce) {
*:not(.navds-loader *):not(.navds-loader),
*:not(.navds-loader *):not(.navds-loader):not(.navds-progress-bar *):not(.navds-progress-bar),
::before,
::after {
animation-delay: -1ms !important;
+6 -5
View File
@@ -97,10 +97,6 @@ const StyleMappings = {
main: formCss,
dependencies: [typoCss],
},
{
component: "ContentContainer",
main: "content-container.css",
},
{
component: "CopyButton",
main: "copybutton.css",
@@ -125,7 +121,6 @@ const StyleMappings = {
main: formCss,
dependencies: [typoCss, "button.css", "loader.css", "link.css"],
},
{ component: "Grid", main: "grid.css", dependencies: [typoCss] },
{
component: "GuidePanel",
main: "guide-panel.css",
@@ -164,6 +159,7 @@ const StyleMappings = {
},
{ component: "Panel", main: "panel.css" },
{ component: "Popover", main: "popover.css" },
{ component: "ProgressBar", main: "progress-bar.css" },
{ component: "Provider", main: "" },
{ component: "Radio", main: formCss, dependencies: [typoCss] },
{ component: "RadioGroup", main: formCss, dependencies: [typoCss] },
@@ -178,6 +174,11 @@ const StyleMappings = {
{ component: "Tag", main: "tag.css", dependencies: [typoCss] },
{ component: "TextField", main: formCss, dependencies: [typoCss] },
{ component: "Textarea", main: formCss, dependencies: [typoCss] },
{
component: "FormSummary",
main: formCss,
dependencies: [typoCss, "link.css"],
},
{
component: "ToggleGroup",
main: "toggle-group.css",
+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;
+77
View File
@@ -0,0 +1,77 @@
.navds-form-summary {
overflow: hidden;
border: 1px solid var(--a-border-subtle);
border-radius: var(--a-border-radius-large);
}
.navds-form-summary__header {
background-color: var(--a-surface-subtle);
padding: var(--a-spacing-4) var(--a-spacing-6);
display: flex;
justify-content: space-between;
gap: 0 var(--a-spacing-2);
}
@media (max-width: 479px) {
.navds-form-summary__header {
padding: var(--a-spacing-3) var(--a-spacing-4);
flex-direction: column;
}
}
.navds-form-summary__edit {
flex-shrink: 0;
margin-top: var(--a-spacing-1);
align-self: flex-start;
}
.navds-form-summary > .navds-form-summary__answers {
border-top: 1px solid var(--a-border-subtle);
}
.navds-form-summary__answers {
padding: var(--a-spacing-5) var(--a-spacing-6) var(--a-spacing-6);
margin: 0;
}
@media (max-width: 479px) {
.navds-form-summary__answers {
padding: var(--a-spacing-4);
}
}
.navds-form-summary__answer:not(:last-child) {
margin-bottom: var(--a-spacing-4);
padding-bottom: var(--a-spacing-4);
border-bottom: 1px solid var(--a-border-divider);
}
.navds-form-summary__answer:has(.navds-form-summary__answer) {
padding-bottom: var(--a-spacing-6);
}
.navds-form-summary__answer:has(.navds-form-summary__answer):last-child {
padding-bottom: 0;
}
@media (max-width: 479px) {
.navds-form-summary__answer:not(:last-child) {
margin-bottom: var(--a-spacing-3);
padding-bottom: var(--a-spacing-3);
}
.navds-form-summary__answer:has(.navds-form-summary__answer) {
padding-bottom: var(--a-spacing-5);
}
}
.navds-form-summary__value:first-of-type {
margin-top: var(--a-spacing-1);
}
.navds-form-summary__value :where(.navds-form-summary__answers) {
margin-top: var(--a-spacing-2);
padding: var(--a-spacing-4);
background: var(--a-surface-selected);
border-radius: var(--a-border-radius-large);
}
+1
View File
@@ -13,3 +13,4 @@
@import "textarea.css";
@import "search.css";
@import "combobox.css";
@import "form-summary.css";
+23 -101
View File
@@ -70,8 +70,8 @@
height: calc(1.25rem - 0.25rem);
}
.navds-checkbox__input:focus-visible + .navds-checkbox__label::before,
.navds-radio__input:focus-visible + .navds-radio__label::before {
.navds-checkbox__input:focus + .navds-checkbox__label::before,
.navds-radio__input:focus + .navds-radio__label::before {
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow:
@@ -80,33 +80,13 @@
0 0 0 4px var(--a-border-focus);
}
@supports not selector(:focus-visible) {
.navds-checkbox__input:focus + .navds-checkbox__label::before,
.navds-radio__input:focus + .navds-radio__label::before {
outline: 2px solid transparent;
outline-offset: 2px;
box-shadow:
0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default)),
0 0 0 4px var(--a-border-focus);
}
}
.navds-checkbox__input:hover:focus-visible + .navds-checkbox__label::before,
.navds-radio__input:hover:focus-visible + .navds-radio__label::before {
.navds-checkbox__input:hover:focus + .navds-checkbox__label::before,
.navds-radio__input:hover:focus + .navds-radio__label::before {
box-shadow:
0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action-hover)),
0 0 0 4px var(--a-border-focus);
}
@supports not selector(:focus-visible) {
.navds-checkbox__input:hover:focus + .navds-checkbox__label::before,
.navds-radio__input:hover:focus + .navds-radio__label::before {
box-shadow:
0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action-hover)),
0 0 0 4px var(--a-border-focus);
}
}
.navds-checkbox__input:indeterminate + .navds-checkbox__label::after {
content: "";
position: absolute;
@@ -163,24 +143,14 @@
background-position: 0.25rem center;
}
.navds-checkbox__input:indeterminate:focus-visible + .navds-checkbox__label::before,
.navds-checkbox__input:checked:focus-visible + .navds-checkbox__label::before {
.navds-checkbox__input:indeterminate:focus + .navds-checkbox__label::before,
.navds-checkbox__input:checked:focus + .navds-checkbox__label::before {
box-shadow:
0 0 0 1px var(--ac-radio-checkbox-action, var(--a-surface-action-selected)),
0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)),
0 0 0 4px var(--a-border-focus);
}
@supports not selector(:focus-visible) {
.navds-checkbox__input:indeterminate:focus + .navds-checkbox__label::before,
.navds-checkbox__input:checked:focus + .navds-checkbox__label::before {
box-shadow:
0 0 0 1px var(--ac-radio-checkbox-action, var(--a-surface-action-selected)),
0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)),
0 0 0 4px var(--a-border-focus);
}
}
.navds-radio__input:checked + .navds-radio__label::before {
box-shadow:
0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action-selected)),
@@ -188,40 +158,23 @@
background-color: var(--ac-radio-checkbox-action, var(--a-surface-action-selected));
}
.navds-radio__input:checked:focus-visible + .navds-radio__label::before {
.navds-radio__input:checked:focus + .navds-radio__label::before {
box-shadow:
0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action-selected)),
inset 0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)),
0 0 0 4px var(--a-border-focus);
}
@supports not selector(:focus-visible) {
.navds-radio__input:checked:focus + .navds-radio__label::before {
box-shadow:
0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
inset 0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)),
0 0 0 4px var(--a-border-focus);
}
}
.navds-checkbox__input:hover:not(:disabled) + .navds-checkbox__label,
.navds-radio__input:hover:not(:disabled) + .navds-radio__label {
color: var(--ac-radio-checkbox-action, var(--a-surface-action-hover));
}
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus-visible)
+ .navds-checkbox__label::before,
.navds-radio__input:hover:not(:disabled):not(:checked):not(:focus-visible) + .navds-radio__label::before {
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus) + .navds-checkbox__label::before,
.navds-radio__input:hover:not(:disabled):not(:checked):not(:focus) + .navds-radio__label::before {
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action-hover));
}
@supports not selector(:focus-visible) {
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus) + .navds-checkbox__label::before,
.navds-radio__input:hover:not(:disabled):not(:checked):not(:focus) + .navds-radio__label::before {
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action-hover));
}
}
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate) + .navds-checkbox__label::before,
.navds-radio__input:hover:not(:disabled):not(:checked) + .navds-radio__label::before {
background-color: var(--ac-radio-checkbox-action-hover-bg, var(--a-surface-action-subtle));
@@ -235,63 +188,32 @@
}
.navds-checkbox--error
> .navds-checkbox__input:focus-visible:not(:hover):not(:disabled):not(:checked):not(:indeterminate)
> .navds-checkbox__input:focus:not(:hover):not(:disabled):not(:checked):not(:indeterminate)
+ .navds-checkbox__label::before,
.navds-radio--error > .navds-radio__input:focus-visible:not(:hover):not(:disabled):not(:checked) + .navds-radio__label::before {
.navds-radio--error > .navds-radio__input:focus:not(:hover):not(:disabled):not(:checked) + .navds-radio__label::before {
box-shadow:
0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)),
0 0 0 4px var(--a-border-focus);
}
@supports not selector(:focus-visible) {
.navds-checkbox--error
> .navds-checkbox__input:focus:not(:hover):not(:disabled):not(:checked):not(:indeterminate)
+ .navds-checkbox__label::before,
.navds-radio--error > .navds-radio__input:focus:not(:hover):not(:disabled):not(:checked) + .navds-radio__label::before {
box-shadow:
0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)),
0 0 0 4px var(--a-border-focus);
}
}
.navds-checkbox--error
> .navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus-visible)
> .navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus)
+ .navds-checkbox__label::before,
.navds-radio--error > .navds-radio__input:hover:not(:disabled):not(:checked):not(:focus-visible) + .navds-radio__label::before {
.navds-radio--error > .navds-radio__input:hover:not(:disabled):not(:checked):not(:focus) + .navds-radio__label::before {
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
}
.navds-checkbox--error
> .navds-checkbox__input:focus-visible:hover:not(:disabled):not(:checked):not(:indeterminate)
> .navds-checkbox__input:focus:hover:not(:disabled):not(:checked):not(:indeterminate)
+ .navds-checkbox__label::before,
.navds-radio--error > .navds-radio__input:focus-visible:hover:not(:disabled):not(:checked) + .navds-radio__label::before {
.navds-radio--error > .navds-radio__input:focus:hover:not(:disabled):not(:checked) + .navds-radio__label::before {
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
box-shadow:
0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)),
0 0 0 4px var(--a-border-focus);
}
@supports not selector(:focus-visible) {
.navds-checkbox--error
> .navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus)
+ .navds-checkbox__label::before,
.navds-radio--error > .navds-radio__input:hover:not(:disabled):not(:checked):not(:focus) + .navds-radio__label::before {
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
}
.navds-checkbox--error
> .navds-checkbox__input:focus:hover:not(:disabled):not(:checked):not(:indeterminate)
+ .navds-checkbox__label::before,
.navds-radio--error > .navds-radio__input:focus:hover:not(:disabled):not(:checked) + .navds-radio__label::before {
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
box-shadow:
0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)),
0 0 0 4px var(--a-border-focus);
}
}
.navds-checkbox--disabled,
.navds-radio--disabled {
opacity: 0.3;
@@ -323,16 +245,16 @@
> .navds-checkbox__input:not(:disabled):not(:checked):not(:indeterminate)
+ .navds-checkbox__label::before,
.navds-checkbox--readonly
> .navds-checkbox__input:hover:not(:checked):not(:indeterminate):not(:focus-visible)
> .navds-checkbox__input:hover:not(:checked):not(:indeterminate):not(:focus)
+ .navds-checkbox__label::before,
.navds-radio--readonly > .navds-radio__input:not(:disabled):not(:checked) + .navds-radio__label::before,
.navds-radio--readonly > .navds-radio__input:hover:not(:checked):not(:focus-visible) + .navds-radio__label::before {
.navds-radio--readonly > .navds-radio__input:hover:not(:checked):not(:focus) + .navds-radio__label::before {
background-color: var(--__ac-radio-checkbox-readonly-bg);
box-shadow: 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
}
.navds-checkbox--readonly > .navds-checkbox__input:focus-visible + .navds-checkbox__label::before,
.navds-radio--readonly > .navds-radio__input:focus-visible + .navds-radio__label::before {
.navds-checkbox--readonly > .navds-checkbox__input:focus + .navds-checkbox__label::before,
.navds-radio--readonly > .navds-radio__input:focus + .navds-radio__label::before {
--__ac-radio-checkbox-readonly-border: var(--a-border-subtle), var(--a-shadow-focus);
}
@@ -362,8 +284,8 @@
}
@media (forced-colors: active) {
.navds-checkbox__input:focus-visible + .navds-checkbox__label::before,
.navds-radio__input:focus-visible + .navds-radio__label::before {
.navds-checkbox__input:focus + .navds-checkbox__label::before,
.navds-radio__input:focus + .navds-radio__label::before {
outline-color: highlight;
}
@@ -395,7 +317,7 @@
background-color: var(--__ac-radio-checkbox-high-contrast-text);
}
.navds-radio__input:checked:focus-visible + .navds-radio__label::before {
.navds-radio__input:checked:focus + .navds-radio__label::before {
border: 1px solid var(--__ac-radio-checkbox-high-contrast-bg);
outline: 2px solid highlight;
outline-offset: 2px;
@@ -406,7 +328,7 @@
color: highlight;
}
.navds-checkbox__input:focus-visible + .navds-checkbox__label::before {
.navds-checkbox__input:focus + .navds-checkbox__label::before {
outline: 2px solid var(--__ac-radio-checkbox-high-contrast-highlight);
}
+1
View File
@@ -26,6 +26,7 @@
@import "panel.css";
@import "link-panel.css";
@import "read-more.css";
@import "progress-bar.css";
@import "skeleton.css";
@import "stepper.css";
@import "table.css";
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "6.3.4",
"version": "6.7.1",
"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.4",
"@navikt/ds-tokens": "^6.7.1",
"cssnano": "6.0.0",
"fast-glob": "3.2.11",
"lodash": "4.17.21",
+108
View File
@@ -0,0 +1,108 @@
.navds-progress-bar {
background: var(--a-surface-neutral-subtle);
position: relative;
border-radius: var(--a-border-radius-full);
overflow: hidden;
box-shadow:
inset 0 1px 3px 0 rgba(0 0 0 / 0.15),
inset 0 0 1px 0 rgba(0 0 0 / 0.2);
}
.navds-progress-bar--small {
height: 12px;
min-width: 12px;
}
.navds-progress-bar--medium {
height: 16px;
min-width: 16px;
}
.navds-progress-bar--large {
height: 24px;
min-width: 24px;
}
.navds-progress-bar__foreground {
--__ac-progress-bar-translate: initial;
transform: translateX(var(--__ac-progress-bar-translate));
background: var(--ac-progress-bar-fg, var(--a-surface-alt-3));
transform-origin: left;
position: absolute;
top: 0;
left: 0;
bottom: 0;
width: 100%;
border-radius: inherit;
transition: transform 0.2s ease;
}
.navds-progress-bar__foreground--indeterminate {
--__ac-progress-bar-simulated: initial;
animation-name: navds-progress-bar-indeterminate-grow, navds-progress-bar-indeterminate;
animation-timing-function: ease-in-out, ease-in-out;
animation-duration: var(--__ac-progress-bar-simulated), 2500ms;
animation-fill-mode: forwards, none;
animation-iteration-count: 1, infinite;
animation-delay: 0s, var(--__ac-progress-bar-simulated);
}
/* navds-progress-bar-indeterminate wave animation */
@keyframes navds-progress-bar-indeterminate {
0% {
left: -50%;
width: 50%;
transform: translateX(0%);
}
50% {
animation-direction: reverse;
left: 100%;
transform: translateX(0%);
width: 50%;
}
100% {
left: -50%;
transform: translateX(0%);
width: 50%;
}
}
@keyframes navds-progress-bar-indeterminate-grow {
0% {
transform: translateX(-100%);
}
20% {
transform: translateX(-80%);
}
30% {
transform: translateX(-40%);
}
50% {
transform: translateX(-20%);
}
75% {
transform: translateX(-10%);
}
100% {
transform: translateX(-10%);
}
}
@media (forced-colors: active) {
.navds-progress-bar__foreground {
background: Highlight;
}
.navds-progress-bar {
outline: 1px solid transparent;
}
}
+3 -2
View File
@@ -79,7 +79,8 @@
}
.navds-read-more__button:hover > .navds-read-more__expand-icon {
transform: translateY(1px);
position: relative;
top: 1px;
}
.navds-read-more--open > .navds-read-more__button > .navds-read-more__expand-icon {
@@ -87,5 +88,5 @@
}
.navds-read-more--open > .navds-read-more__button:hover > .navds-read-more__expand-icon {
transform: translateY(-1px) rotate(-180deg);
top: -1px;
}
+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);
+1 -1
View File
@@ -245,7 +245,7 @@
.navds-timeline__pin-wrapper::before {
content: "";
top: var(--navdsc-timeline-pin-size);
height: calc(88% - var(--navdsc-timeline-pin-size));
height: calc(100% - var(--navdsc-timeline-pin-size) * 2);
width: 1px;
margin: 0 auto;
background: var(--a-surface-inverted);
+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));
+3
View File
@@ -217,6 +217,9 @@
"--ac-popover-bg": "--a-surface-default",
"--ac-popover-border": "--a-border-default"
},
"progress-bar": {
"--ac-progress-bar-fg": "--a-surface-alt-3"
},
"readmore": {
"--ac-read-more-text": "--a-text-action",
"--ac-read-more-hover-bg": "--a-surface-hover",
+114
View File
@@ -1,5 +1,119 @@
# @navikt/ds-react
## 6.7.1
### Patch Changes
- Progressbar: Tweak API, examples, stories and css ([#2892](https://github.com/navikt/aksel/pull/2892))
- Combobox: Remove 'Ingen søketreff' when custom options allowed ([#2895](https://github.com/navikt/aksel/pull/2895))
- Datepicker, MonthPicker: Rekkefølgen på årstall i Select er reversert slik at siste år er øverst. Dette er endret for å være bedre tilpasset ekspertsystemer der de mest relevante årene ble vist lengst unna musepeker ved klikk. ([#2882](https://github.com/navikt/aksel/pull/2882))
- Updated dependencies []:
- @navikt/ds-tokens@6.7.1
- @navikt/aksel-icons@6.7.1
## 6.7.0
### Minor Changes
- :sparkles: Ny komponent: ProgressBar ([#2845](https://github.com/navikt/aksel/pull/2845))
### Patch Changes
- Updated dependencies []:
- @navikt/ds-tokens@6.7.0
- @navikt/aksel-icons@6.7.0
## 6.6.1
### Patch Changes
- Spacer: Er nå `span` (tidligere `div`). ([#2874](https://github.com/navikt/aksel/pull/2874))
- Updated dependencies []:
- @navikt/ds-tokens@6.6.1
- @navikt/aksel-icons@6.6.1
## 6.6.0
### Minor Changes
- :sparkles: Ny komponent FormSummary ([#2802](https://github.com/navikt/aksel/pull/2802))
### Patch Changes
- Combobox: Prevents "Enter" while Combobox is focused from submitting form. ([#2861](https://github.com/navikt/aksel/pull/2861))
- Updated dependencies []:
- @navikt/ds-tokens@6.6.0
- @navikt/aksel-icons@6.6.0
## 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
+28 -8
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "6.3.4",
"version": "6.7.1",
"description": "React components from the Norwegian Labour and Welfare Administration.",
"author": "Aksel, a team part of the Norwegian Labour and Welfare Administration.",
"license": "MIT",
@@ -24,7 +24,8 @@
"/cjs",
"/esm",
"/src",
"!**/*.tsbuildinfo"
"!**/*.tsbuildinfo",
"!**/*.stories.*"
],
"main": "./cjs/index.js",
"module": "./esm/index.js",
@@ -309,6 +310,16 @@
"default": "./cjs/popover/index.js"
}
},
"./ProgressBar": {
"import": {
"types": "./esm/progress-bar/index.d.ts",
"default": "./esm/progress-bar/index.js"
},
"require": {
"types": "./cjs/progress-bar/index.d.ts",
"default": "./cjs/progress-bar/index.js"
}
},
"./Provider": {
"import": {
"types": "./esm/provider/index.d.ts",
@@ -549,6 +560,16 @@
"default": "./cjs/form/file-upload/index.js"
}
},
"./FormSummary": {
"import": {
"types": "./esm/form/form-summary/index.d.ts",
"default": "./esm/form/form-summary/index.js"
},
"require": {
"types": "./cjs/form/form-summary/index.d.ts",
"default": "./cjs/form/form-summary/index.js"
}
},
"./package.json": "./package.json"
},
"scripts": {
@@ -562,18 +583,17 @@
},
"dependencies": {
"@floating-ui/react": "0.25.4",
"@navikt/aksel-icons": "^6.3.4",
"@navikt/ds-tokens": "^6.3.4",
"@radix-ui/react-tabs": "1.0.0",
"@radix-ui/react-toggle-group": "1.0.0",
"@floating-ui/react-dom": "^2.0.9",
"@navikt/aksel-icons": "^6.7.1",
"@navikt/ds-tokens": "^6.7.1",
"clsx": "^2.1.0",
"date-fns": "^3.0.0",
"react-day-picker": "8.10.0"
},
"devDependencies": {
"@testing-library/dom": "8.13.0",
"@testing-library/dom": "9.3.4",
"@testing-library/jest-dom": "^5.16.0",
"@testing-library/react": "^13.3.0",
"@testing-library/react": "^15.0.7",
"@testing-library/user-event": "^14.2.0",
"@types/faker": "5.5.8",
"concurrently": "7.2.1",
@@ -183,3 +183,10 @@ export const Chromatic: Story = {
chromatic: { disable: false },
},
};
export const ChromaticForcedColors: Story = {
render: Chromatic.render,
parameters: {
chromatic: { disable: false, forcedColors: "active" },
},
};
+2 -1
View File
@@ -3,6 +3,7 @@ import React, { HTMLAttributes, forwardRef } from "react";
import { BodyLong } from "../typography";
import Bubble from "./Bubble";
export const VARIANTS = ["subtle", "info", "neutral"] as const;
export const POSITIONS = ["left", "right"] as const;
export const SIZES = ["medium", "small"] as const;
@@ -28,7 +29,7 @@ export interface ChatProps extends HTMLAttributes<HTMLDivElement> {
* Avoid using the same background as the surface behind Chat.
* @default "neutral"
*/
variant?: "subtle" | "info" | "neutral";
variant?: (typeof VARIANTS)[number];
/**
* Positions avatar and bubbles.
* @default "left"
+79 -113
View File
@@ -1,25 +1,11 @@
import { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { VStack } from "../layout/stack";
import Chat, { POSITIONS, SIZES } from "./Chat";
import Chat, { POSITIONS, SIZES, VARIANTS } from "./Chat";
export default {
title: "ds-react/Chat",
component: Chat,
argTypes: {
position: {
control: { type: "radio" },
options: POSITIONS,
},
toptextPosition: {
control: { type: "radio" },
options: POSITIONS,
},
size: {
control: { type: "radio" },
options: SIZES,
},
},
parameters: {
chromatic: { disable: true },
},
@@ -48,43 +34,49 @@ export const Controls: Story = {
timestamp: "01.01.21 14:00",
avatar: "ON",
},
argTypes: {
position: {
control: { type: "radio" },
options: [...POSITIONS], // TypeScript does not like using readonly typed arrays here
},
toptextPosition: {
control: { type: "radio" },
options: [...POSITIONS],
},
size: {
control: { type: "radio" },
options: [...SIZES],
},
variant: {
control: { type: "radio" },
options: [...VARIANTS],
},
},
};
export const Size: Story = {
render: () => (
<VStack gap="4">
<h3>Medium</h3>
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 14:00"
size="medium"
>
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint
laboris labore nisi ut.
</Chat.Bubble>
</Chat>
<h3>Small</h3>
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 14:00"
size="small"
>
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint
laboris labore nisi ut.
</Chat.Bubble>
</Chat>
{SIZES.map((size) => (
<>
<h3>{size}</h3>
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 14:00"
size={size}
>
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint
laboris labore nisi ut.
</Chat.Bubble>
</Chat>
</>
))}
</VStack>
),
};
@@ -92,35 +84,21 @@ export const Size: Story = {
export const Variants: Story = {
render: () => (
<VStack gap="4">
<h3>Info</h3>
<Chat avatar="NAV" name="NAV" timestamp="01.01.21 14:00" variant="info">
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
</Chat>
<h3>Neutral</h3>
<Chat
avatar="KN"
name="Kari Nordmann"
timestamp="01.01.21 14:03"
variant="neutral"
position="right"
>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
</Chat>
<h3>Subtle</h3>
<Chat
avatar="ON"
name="Ola Nordmann"
timestamp="01.01.21 14:07"
variant="subtle"
position="right"
>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint
laboris labore nisi ut.
</Chat.Bubble>
</Chat>
{VARIANTS.map((variant) => (
<>
<h3>{variant}</h3>
<Chat
avatar="NAV"
name="NAV"
timestamp="01.01.21 14:00"
variant={variant}
>
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
</Chat>
</>
))}
</VStack>
),
};
@@ -131,8 +109,7 @@ export const Position: Story = {
<h3>Default</h3>
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 14:00">
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint
laboris labore nisi ut.
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
</Chat>
<h3>Right</h3>
@@ -159,10 +136,7 @@ export const Position: Story = {
timestamp="01.01.21 15:00"
>
<Chat.Bubble>
Exercitation irure Lorem cupidatat culpa anim cillum esse ullamco qui
dolore laborum et Lorem. Labore sunt duis id Lorem voluptate commodo
ea esse. Dolore esse aliqua proident ea ad commodo ut dolore voluptate
labore sunt aute.
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
</Chat>
</VStack>
@@ -171,8 +145,8 @@ export const Position: Story = {
export const ToptextPosition: Story = {
render: () => (
<VStack gap="4">
<h3>Right</h3>
<VStack gap="4" style={{ width: 500 }}>
<h3>All right</h3>
<Chat
avatar="ON"
name="Ola Normann"
@@ -180,20 +154,31 @@ export const ToptextPosition: Story = {
toptextPosition="right"
>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint
laboris labore nisi ut.
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
</Chat>
<h3>Left</h3>
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 15:00">
<Chat.Bubble toptextPosition="left">
Exercitation irure Lorem cupidatat culpa anim cillum esse ullamco qui
dolore laborum et Lorem. Labore sunt duis id Lorem voluptate commodo
ea esse. Dolore esse aliqua proident ea ad commodo ut dolore voluptate
labore sunt aute.
<h3>Single bubble right</h3>
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 14:00">
<Chat.Bubble>
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
<Chat.Bubble timestamp="14:02" toptextPosition="right">
Enim velit deserunt do quis.
</Chat.Bubble>
</Chat>
<h3>Left</h3>
<h3>All left</h3>
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 15:00"
toptextPosition="left"
>
<Chat.Bubble>
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
</Chat>
<h3>All left, position right</h3>
<Chat
name="Kari Høyli"
timestamp="01.01.21 14:32"
@@ -203,25 +188,6 @@ export const ToptextPosition: Story = {
<Chat.Bubble>
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
<Chat.Bubble>
Enim velit deserunt do quis. Eu fugiat magna esse dolore ad sunt sit
est dolore incididunt. Occaecat cupidatat magna.
</Chat.Bubble>
</Chat>
<h3>Right</h3>
<Chat
name="Kari Høyli"
timestamp="01.01.21 14:32"
position="right"
toptextPosition="right"
>
<Chat.Bubble>
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
<Chat.Bubble>
Enim velit deserunt do quis. Eu fugiat magna esse dolore ad sunt sit
est dolore incididunt. Occaecat cupidatat magna.
</Chat.Bubble>
</Chat>
</VStack>
),
@@ -0,0 +1,32 @@
import { createContext } from "../util/create-context";
import { CollapsibleBaseProps } from "./Collapsible.types";
export type CollapsibleContextProps = Pick<CollapsibleBaseProps, "open"> & {
/**
* Allows connecting trigger to content
*/
contentId: string;
/**
* Allows connecting content to trigger
*/
triggerId: string;
/**
* Callback for toggling open state
*/
onOpenToggle: () => void;
/**
* Allows always rendering children in content when closed
*/
lazy: boolean;
/**
* Utility for getting state as string. Usefull for data-attributes.
*/
state: "open" | "closed";
};
export const [CollapsibleContextProvider, useCollapsibleContext] =
createContext<CollapsibleContextProps>({
name: "CollapsibleContext",
errorMessage:
"<Collapsible.Trigger> and <Collapsible.Content> must be used within a <Collapsible>",
});
@@ -0,0 +1,222 @@
import { Meta, StoryFn, StoryObj } from "@storybook/react";
import React, { useState } from "react";
import { ChevronDownIcon, ChevronUpIcon } from "@navikt/aksel-icons";
import { Button } from "../button";
import { Box } from "../layout/box";
import { HStack, VStack } from "../layout/stack";
import { Collapsible } from "./Collapsible";
const meta: Meta<typeof Collapsible> = {
title: "Utilities/Collapsible",
component: Collapsible,
parameters: {
chromatic: { disable: true },
},
};
export default meta;
type Story = StoryObj<typeof Collapsible>;
export const Default: Story = {
render: (...props) => (
<Collapsible {...props}>
<Collapsible.Trigger>Trigger</Collapsible.Trigger>
<Collapsible.Content>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae
corporis maxime aliquam, voluptates nobis numquam, non odit optio
architecto iure laborum possimus! Quibusdam sit ullam, consequatur sunt
tempore optio aliquid!
</Collapsible.Content>
</Collapsible>
),
args: {
open: false,
defaultOpen: false,
},
};
export const InContext: StoryFn<typeof Collapsible> = () => {
const [open, setOpen] = useState(false);
return (
<Collapsible open={open} onOpenChange={setOpen}>
<HStack justify="space-between" gap="4" align="center">
Steg 1 av 8{" "}
<Collapsible.Trigger asChild>
<Button
size="small"
variant="tertiary"
iconPosition="right"
icon={
open ? (
<ChevronUpIcon aria-hidden />
) : (
<ChevronDownIcon aria-hidden />
)
}
>
Vis steg
</Button>
</Collapsible.Trigger>
</HStack>
<Collapsible.Content>
<p>Lorem ipsum dolor sit</p>
</Collapsible.Content>
</Collapsible>
);
};
export const Animated: StoryFn<typeof Collapsible> = () => (
<Collapsible lazy>
<Collapsible.Trigger asChild>
<Button size="small" variant="secondary">
Animer open/lukk
</Button>
</Collapsible.Trigger>
<Collapsible.Content className="collapsible">
<div className="collapsibleContent">
<Box padding="8" background="surface-alt-3-subtle">
<div>lorem ipsum</div>
</Box>
</div>
</Collapsible.Content>
</Collapsible>
);
Animated.decorators = [
(Story) => (
<div style={{ height: 300 }}>
<style>{`
.collapsible {
display: grid;
grid-template-rows: 0fr;
overflow: hidden;
transition: grid-template-rows 1s ease-in-out;
}
.collapsible[data-state="closed"] {
width: 0;
}
.collapsible[data-state="open"] {
grid-template-rows: 1fr;
}
.collapsibleContent {
min-height: 0;
transition: visibility 1s;
visibility: hidden;
}
.collapsible[data-state="open"] .collapsibleContent {
visibility: visible;
}
`}</style>
{Story()}
</div>
),
];
export const Lazy: StoryFn<typeof Collapsible> = () => (
<Collapsible lazy>
<Collapsible.Trigger>Trigger</Collapsible.Trigger>
<Collapsible.Content>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae
corporis maxime aliquam, voluptates nobis numquam, non odit optio
architecto iure laborum possimus! Quibusdam sit ullam, consequatur sunt
tempore optio aliquid!
</Collapsible.Content>
</Collapsible>
);
export const AsChild: StoryFn<typeof Collapsible> = () => (
<Collapsible>
<Collapsible.Trigger asChild>
<Button>Button</Button>
</Collapsible.Trigger>
<Collapsible.Content asChild>
<Box padding="4" background="surface-alt-3-subtle">
<div>lorem ipsum</div>
</Box>
</Collapsible.Content>
</Collapsible>
);
export const DefaultOpen: StoryFn<typeof Collapsible> = () => (
<Collapsible defaultOpen>
<Collapsible.Trigger asChild>
<Button>Button</Button>
</Collapsible.Trigger>
<Collapsible.Content asChild>
<Box padding="4" background="surface-alt-3-subtle">
<div>lorem ipsum</div>
</Box>
</Collapsible.Content>
</Collapsible>
);
export const ControlledOpen: StoryFn<typeof Collapsible> = () => (
<Collapsible open>
<Collapsible.Trigger asChild>
<Button>Button</Button>
</Collapsible.Trigger>
<Collapsible.Content asChild>
<Box padding="4" background="surface-alt-3-subtle">
<div>lorem ipsum</div>
</Box>
</Collapsible.Content>
</Collapsible>
);
export const Disabled: StoryFn<typeof Collapsible> = ({ open = false }) => (
<Collapsible open={open}>
<Collapsible.Trigger asChild disabled>
<Button>Button</Button>
</Collapsible.Trigger>
<Collapsible.Content asChild>
<Box padding="4" background="surface-alt-3-subtle">
<div>lorem ipsum</div>
</Box>
</Collapsible.Content>
</Collapsible>
);
export const Chromatic: Story = {
render: () => (
<VStack gap="8">
<div>
<h2>In context</h2>
<InContext />
</div>
<div>
<h2>Animated</h2>
<Animated />
</div>
<div>
<h2>Lazy</h2>
<Lazy />
</div>
<div>
<h2>AsChild</h2>
<AsChild />
</div>
<div>
<h2>DefaultOpen</h2>
<DefaultOpen />
</div>
<div>
<h2>ControlledOpen</h2>
<ControlledOpen />
</div>
<div>
<h2>Disabled</h2>
<Disabled />
<Disabled open />
</div>
</VStack>
),
parameters: {
chromatic: { disable: false },
},
};
@@ -0,0 +1,100 @@
import React, { forwardRef, useCallback } from "react";
import { useControllableState, useId } from "../util/hooks";
import { CollapsibleContextProvider } from "./Collapsible.context";
import { CollapsibleBaseProps } from "./Collapsible.types";
import CollapsibleContent from "./parts/Collapsible.Content";
import CollapsibleTrigger from "./parts/Collapsible.Trigger";
export interface CollapsibleProps
extends CollapsibleBaseProps,
React.HTMLAttributes<HTMLDivElement> {}
interface CollapsibleComponent
extends React.ForwardRefExoticComponent<
CollapsibleProps & React.RefAttributes<HTMLDivElement>
> {
/**
* @see 🏷️ {@link CollapsibleTriggerProps}
*/
Trigger: typeof CollapsibleTrigger;
/**
* @see 🏷️ {@link CollapsibleContentProps}
*/
Content: typeof CollapsibleContent;
}
/**
* Collapsible is a component that allows you to toggle visibility of content.
*
* @example
* ```jsx
* <Collapsible>
* <Collapsible.Trigger>Trigger</Collapsible.Trigger>
* <Collapsible.Content>
* Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae
* corporis maxime aliquam, voluptates nobis numquam, non odit optio
* architecto iure laborum possimus! Quibusdam sit ullam, consequatur sunt
* tempore optio aliquid!
* </Collapsible.Content>
* </Collapsible>
* ```
*
* @example With asChild
* ```jsx
* <Collapsible>
* <Collapsible.Trigger asChild>
* <Button>Button</Button>
* </Collapsible.Trigger>
* <Collapsible.Content asChild>
* <Box padding="4" background="surface-alt-3-subtle">
* <div>lorem ipsum</div>
* </Box>
* </Collapsible.Content>
* </Collapsible>
* ```
*/
export const Collapsible = forwardRef<HTMLDivElement, CollapsibleProps>(
(
{
children,
open,
defaultOpen = false,
onOpenChange,
lazy = false,
...rest
},
ref,
) => {
const [_open, setOpen] = useControllableState({
value: open,
defaultValue: defaultOpen,
onChange: onOpenChange,
});
const internalId = useId();
const state = _open ? "open" : "closed";
return (
<CollapsibleContextProvider
open={_open}
onOpenToggle={useCallback(
() => setOpen((prevOpen) => !prevOpen),
[setOpen],
)}
contentId={`collapsible-content-${internalId}`}
triggerId={`collapsible-trigger-${internalId}`}
lazy={lazy}
state={state}
>
<div ref={ref} data-state={state} {...rest}>
{children}
</div>
</CollapsibleContextProvider>
);
},
) as CollapsibleComponent;
Collapsible.Trigger = CollapsibleTrigger;
Collapsible.Content = CollapsibleContent;
export default Collapsible;
@@ -0,0 +1,19 @@
export interface CollapsibleBaseProps {
/**
* The open state of the collapsible when it is initially rendered. Use when you do not need to control its open state.
*/
defaultOpen?: boolean;
/**
* The controlled open state of the collapsible. Must be used in conjunction with onOpenChange.
*/
open?: boolean;
/**
* Event handler called when the open state of the collapsible changes.
*/
onOpenChange?: (open: boolean) => void;
/**
* Render the collapsible content lazily. This means that the content will not be rendered until the collapsible is open.
* @default false
*/
lazy?: boolean;
}
@@ -0,0 +1,10 @@
"use client";
export { default as Collapsible, type CollapsibleProps } from "./Collapsible";
export {
default as CollapsibleTrigger,
type CollapsibleTriggerProps,
} from "./parts/Collapsible.Trigger";
export {
default as CollapsibleContent,
type CollapsibleContentProps,
} from "./parts/Collapsible.Content";
@@ -0,0 +1,39 @@
import React, { forwardRef } from "react";
import { Slot } from "../../util/Slot";
import { useCollapsibleContext } from "../Collapsible.context";
export interface CollapsibleContentProps
extends Omit<
React.HTMLAttributes<HTMLDivElement>,
"hidden" | "aria-controls" | "id"
> {
/**
* When true, will render element as its child. This merges classes, styles and event handlers.
* @default false
*/
asChild?: boolean;
}
export const CollapsibleContent = forwardRef<
HTMLDivElement,
CollapsibleContentProps
>(({ children, asChild, ...rest }, ref) => {
const ctx = useCollapsibleContext();
const Comp = asChild ? Slot : "div";
return (
<Comp
ref={ref}
{...rest}
data-state={ctx.state}
hidden={!ctx.open}
aria-controls={ctx.open ? ctx.triggerId : undefined}
id={ctx.contentId}
>
{ctx.lazy || ctx.open ? children : null}
</Comp>
);
});
export default CollapsibleContent;
@@ -0,0 +1,42 @@
import React, { forwardRef } from "react";
import { Slot } from "../../util/Slot";
import { composeEventHandlers } from "../../util/composeEventHandlers";
import { useCollapsibleContext } from "../Collapsible.context";
export interface CollapsibleTriggerProps
extends Omit<
React.ButtonHTMLAttributes<HTMLButtonElement>,
"id" | "aria-controls" | "aria-expanded"
> {
/**
* When true, will render element as its child. This merges classes, styles and event handlers.
* @default false
*/
asChild?: boolean;
}
export const CollapsibleTrigger = forwardRef<
HTMLButtonElement,
CollapsibleTriggerProps
>(({ children, asChild, onClick, ...rest }, ref) => {
const ctx = useCollapsibleContext();
const Comp = asChild ? Slot : "button";
return (
<Comp
ref={ref}
type="button"
data-state={ctx.state}
onClick={composeEventHandlers(onClick, ctx.onOpenToggle)}
{...rest}
id={ctx.triggerId}
aria-controls={ctx.open ? ctx.contentId : undefined}
aria-expanded={ctx.open}
>
{children}
</Comp>
);
});
export default CollapsibleTrigger;
@@ -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,4 +1,5 @@
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 { Button } from "../../button";
@@ -440,6 +441,39 @@ 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 userEvent.click(button10);
expect(button10.ariaPressed).toBe("true");
const button17 = canvas.getByText("17").closest("button");
expect(button17?.ariaPressed).toBe("false");
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}
/>
);
};
@@ -37,7 +37,11 @@ export const DropdownCaption = ({ displayMonth, id }: CaptionProps) => {
const handleMonthChange: React.ChangeEventHandler<HTMLSelectElement> = (e) =>
goToMonth(setMonth(startOfMonth(displayMonth), Number(e.target.value)));
const years = getYears(fromDate, toDate, displayMonth.getFullYear());
const years = getYears(
fromDate,
toDate,
displayMonth.getFullYear(),
).reverse();
const months = getMonths(fromDate, toDate, displayMonth);
const previousLabel = labelPrevious(previousMonth, { locale });
@@ -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 });
@@ -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,
@@ -35,7 +35,7 @@ export const MonthCaption = () => {
if (!years.map((x) => x.getFullYear()).includes(year.getFullYear())) {
years.push(setYear(startOfYear(new Date()), year.getFullYear()));
}
years.sort((a, b) => a.getFullYear() - b.getFullYear());
years.sort((a, b) => b.getFullYear() - a.getFullYear());
}
const handleYearChange = (e) =>
@@ -1,4 +1,5 @@
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 { Button } from "../../button";
@@ -121,25 +122,37 @@ 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 userEvent.click(buttonApr);
expect(buttonApr.ariaPressed).toBe("true");
const buttonSep = canvas.getByText("september").closest("button");
expect(buttonSep?.ariaPressed).toBe("false");
buttonSep && (await userEvent.click(buttonSep));
expect(buttonSep?.ariaPressed).toBe("true");
expect(buttonApr.ariaPressed).toBe("false");
},
};
export const UserControlled = () => {
@@ -193,7 +206,6 @@ export const Chromatic: Story = {
<DisabledMonths />
<UseMonthpicker />
<UseMonthpickerFormat />
<Required />
<UserControlled />
<FollowYear />
</div>
@@ -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,29 +0,0 @@
import React from "react";
import { XMarkIcon } from "@navikt/aksel-icons";
interface ClearButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
handleClear: (event: any) => void;
clearButtonLabel?: string;
}
export const ClearButton: React.FC<ClearButtonProps> = ({
handleClear,
clearButtonLabel,
...rest
}) => {
return (
<button
type="button"
onClick={handleClear}
className="navds-combobox__button-clear"
{...rest}
>
<span className="navds-sr-only">
{clearButtonLabel ? clearButtonLabel : "Tøm"}
</span>
<XMarkIcon aria-hidden />
</button>
);
};
export default ClearButton;
@@ -1,57 +1,31 @@
import cl from "clsx";
import React, { forwardRef, useRef } from "react";
import React, { forwardRef } from "react";
import { BodyShort, ErrorMessage, Label } from "../../typography";
import { useMergeRefs } from "../../util/hooks/useMergeRefs";
import ClearButton from "./ClearButton";
import ComboboxWrapper from "./ComboboxWrapper";
import FilteredOptions from "./FilteredOptions/FilteredOptions";
import { useFilteredOptionsContext } from "./FilteredOptions/filteredOptionsContext";
import Input from "./Input/Input";
import { useInputContext } from "./Input/inputContext";
import SelectedOptions from "./SelectedOptions/SelectedOptions";
import { useSelectedOptionsContext } from "./SelectedOptions/selectedOptionsContext";
import ToggleListButton from "./ToggleListButton";
import { useInputContext } from "./Input/Input.context";
import { InputController } from "./Input/InputController";
import { ComboboxProps } from "./types";
export const Combobox = forwardRef<
HTMLInputElement,
Omit<ComboboxProps, "onChange" | "options" | "size" | "onClear" | "value">
>((props, ref) => {
const {
className,
hideLabel = false,
description,
label,
clearButton = true,
clearButtonLabel,
toggleListButton = true,
toggleListButtonLabel,
inputClassName,
shouldShowSelectedOptions = true,
...rest
} = props;
const { className, hideLabel = false, description, label, ...rest } = props;
const toggleListButtonRef = useRef<HTMLButtonElement>(null);
const { activeDecendantId, toggleIsListOpen } = useFilteredOptionsContext();
const { selectedOptions } = useSelectedOptionsContext();
const { toggleIsListOpen } = useFilteredOptionsContext();
const {
clearInput,
error,
errorId,
focusInput,
hasError,
inputDescriptionId,
inputProps,
inputRef,
value,
showErrorMsg,
size = "medium",
} = useInputContext();
const mergedInputRef = useMergeRefs(inputRef, ref);
return (
<ComboboxWrapper
className={className}
@@ -59,7 +33,6 @@ export const Combobox = forwardRef<
inputProps={inputProps}
inputSize={size}
toggleIsListOpen={toggleIsListOpen}
toggleListButtonRef={toggleListButtonRef}
>
<Label
htmlFor={inputProps.id}
@@ -83,50 +56,7 @@ export const Combobox = forwardRef<
</BodyShort>
)}
<div className="navds-combobox__wrapper">
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
<div
className={cl(
"navds-combobox__wrapper-inner navds-text-field__input",
{
"navds-combobox__wrapper-inner--virtually-unfocused":
activeDecendantId !== undefined,
},
)}
onClick={focusInput}
>
{!shouldShowSelectedOptions ? (
<Input
id={inputProps.id}
ref={mergedInputRef}
inputClassName={inputClassName}
{...rest}
/>
) : (
<SelectedOptions selectedOptions={selectedOptions} size={size}>
<Input
id={inputProps.id}
ref={mergedInputRef}
inputClassName={inputClassName}
{...rest}
/>
</SelectedOptions>
)}
<div>
{value && clearButton && (
<ClearButton
handleClear={clearInput}
clearButtonLabel={clearButtonLabel}
tabIndex={-1}
/>
)}
{toggleListButton && (
<ToggleListButton
toggleListButtonLabel={toggleListButtonLabel}
ref={toggleListButtonRef}
/>
)}
</div>
</div>
<InputController ref={ref} {...rest} />
<FilteredOptions />
</div>
<div
@@ -1,7 +1,7 @@
import React, { forwardRef } from "react";
import Combobox from "./Combobox";
import { FilteredOptionsProvider } from "./FilteredOptions/filteredOptionsContext";
import { InputContextProvider } from "./Input/inputContext";
import { InputContextProvider } from "./Input/Input.context";
import { SelectedOptionsProvider } from "./SelectedOptions/selectedOptionsContext";
import { mapToComboboxOptionArray } from "./combobox-utils";
import { CustomOptionsProvider } from "./customOptionsContext";
@@ -1,5 +1,6 @@
import cl from "clsx";
import React, { useRef, useState } from "react";
import { useInputContext } from "./Input/Input.context";
type ComboboxWrapperProps = {
children: any;
@@ -10,7 +11,6 @@ type ComboboxWrapperProps = {
};
inputSize: string;
toggleIsListOpen: (isListOpen: boolean) => void;
toggleListButtonRef: React.RefObject<HTMLButtonElement>;
};
const ComboboxWrapper = ({
@@ -20,15 +20,16 @@ const ComboboxWrapper = ({
inputProps,
inputSize,
toggleIsListOpen,
toggleListButtonRef,
}: ComboboxWrapperProps) => {
const { toggleOpenButtonRef } = useInputContext();
const wrapperRef = useRef<HTMLDivElement | null>(null);
const [hasFocusWithin, setHasFocusWithin] = useState(false);
function onFocusInsideWrapper(e) {
if (
!wrapperRef.current?.contains(e.relatedTarget) &&
toggleListButtonRef?.current !== e.target
toggleOpenButtonRef?.current !== e.target
) {
toggleIsListOpen(true);
setHasFocusWithin(true);
@@ -1,23 +0,0 @@
import React from "react";
const CheckIcon = () => {
return (
<svg
width="16"
height="13"
viewBox="0 0 16 13"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
fillRule="evenodd"
clipRule="evenodd"
fill="#005B82"
d="M14.2014 0L16 1.89047L4.77943 13L0 8.39552L1.79361 6.5L4.77418 9.3019L14.2014 0Z"
/>
</svg>
);
};
export default CheckIcon;
@@ -3,7 +3,7 @@ import React from "react";
import { CheckmarkIcon, PlusIcon } from "@navikt/aksel-icons";
import { Loader } from "../../../loader";
import { BodyShort, Label } from "../../../typography";
import { useInputContext } from "../Input/inputContext";
import { useInputContext } from "../Input/Input.context";
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
import { isInList, toComboboxOption } from "../combobox-utils";
import { ComboboxOption } from "../types";
@@ -38,7 +38,7 @@ const FilteredOptions = () => {
const shouldRenderNonSelectables =
maxSelected?.isLimitReached || // Render maxSelected message
isLoading || // Render loading message
(!isLoading && filteredOptions.length === 0); // Render no hits message
(!isLoading && filteredOptions.length === 0 && !allowNewValues); // Render no hits message
const shouldRenderFilteredOptionsList =
(allowNewValues && isValueNew && !maxSelected?.isLimitReached) || // Render add new option
@@ -72,7 +72,7 @@ const FilteredOptions = () => {
<Loader title="Søker..." />
</div>
)}
{!isLoading && filteredOptions.length === 0 && (
{!isLoading && filteredOptions.length === 0 && !allowNewValues && (
<div
className="navds-combobox__list-item--no-options"
id={filteredOptionsUtil.getNoHitsId(id)}
@@ -1,17 +1,11 @@
import cl from "clsx";
import React, {
SetStateAction,
createContext,
useCallback,
useContext,
useMemo,
useState,
} from "react";
import React, { SetStateAction, useCallback, useMemo, useState } from "react";
import { createContext } from "../../../util/create-context";
import { useClientLayoutEffect, usePrevious } from "../../../util/hooks";
import { useInputContext } from "../Input/inputContext";
import { useInputContext } from "../Input/Input.context";
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
import { toComboboxOption } from "../combobox-utils";
import { useCustomOptionsContext } from "../customOptionsContext";
import { useComboboxCustomOptions } from "../customOptionsContext";
import { ComboboxOption, ComboboxProps } from "../types";
import filteredOptionsUtils from "./filtered-options-util";
import useVirtualFocus, { VirtualFocusType } from "./useVirtualFocus";
@@ -24,7 +18,7 @@ type FilteredOptionsProps = {
};
};
type FilteredOptionsContextType = {
type FilteredOptionsContextValue = {
activeDecendantId?: string;
allowNewValues?: boolean;
ariaDescribedBy?: string;
@@ -42,11 +36,14 @@ type FilteredOptionsContextType = {
shouldAutocomplete?: boolean;
virtualFocus: VirtualFocusType;
};
const FilteredOptionsContext = createContext<FilteredOptionsContextType>(
{} as FilteredOptionsContextType,
);
const [FilteredOptionsContextProvider, useFilteredOptionsContext] =
createContext<FilteredOptionsContextValue>({
name: "FilteredOptionsContext",
errorMessage:
"useFilteredOptionsContext must be used within a FilteredOptionsProvider",
});
export const FilteredOptionsProvider = ({
const FilteredOptionsProvider = ({
children,
value: props,
}: FilteredOptionsProps) => {
@@ -71,7 +68,7 @@ export const FilteredOptionsProvider = ({
const { maxSelected } = useSelectedOptionsContext();
const [isInternalListOpen, setInternalListOpen] = useState(false);
const { customOptions } = useCustomOptionsContext();
const { customOptions } = useComboboxCustomOptions();
const filteredOptions = useMemo(() => {
if (externalFilteredOptions) {
@@ -97,9 +94,17 @@ export const FilteredOptionsProvider = ({
[filteredOptionsUtils.getAddNewOptionId(id)]: allowNewValues
? toComboboxOption(value)
: undefined,
...customOptions.reduce(
(acc, customOption) => ({
...acc,
[filteredOptionsUtils.getOptionId(id, customOption.label)]:
customOption,
}),
{},
),
},
),
[allowNewValues, id, options, value],
[allowNewValues, customOptions, id, options, value],
);
useClientLayoutEffect(() => {
@@ -144,9 +149,9 @@ export const FilteredOptionsProvider = ({
const ariaDescribedBy = useMemo(() => {
let activeOption;
if (!isLoading && filteredOptions.length === 0) {
if (!isLoading && filteredOptions.length === 0 && !allowNewValues) {
activeOption = filteredOptionsUtils.getNoHitsId(id);
} else if ((value && value !== "") || isLoading) {
} else if (value || isLoading) {
if (shouldAutocomplete && filteredOptions[0]) {
activeOption = filteredOptionsUtils.getOptionId(
id,
@@ -172,6 +177,7 @@ export const FilteredOptionsProvider = ({
shouldAutocomplete,
filteredOptions,
id,
allowNewValues,
]);
const currentOption = useMemo(
@@ -203,18 +209,10 @@ export const FilteredOptionsProvider = ({
};
return (
<FilteredOptionsContext.Provider value={filteredOptionsState}>
<FilteredOptionsContextProvider {...filteredOptionsState}>
{children}
</FilteredOptionsContext.Provider>
</FilteredOptionsContextProvider>
);
};
export const useFilteredOptionsContext = () => {
const context = useContext(FilteredOptionsContext);
if (!context) {
throw new Error(
"useFilteredOptionsContext must be used within a FilteredOptionsProvider",
);
}
return context;
};
export { FilteredOptionsProvider, useFilteredOptionsContext };
@@ -1,18 +1,18 @@
import React, {
ChangeEvent,
ChangeEventHandler,
createContext,
useCallback,
useContext,
useMemo,
useRef,
useState,
} from "react";
import { createContext } from "../../../util/create-context";
import { useClientLayoutEffect } from "../../../util/hooks";
import { FormFieldType, useFormField } from "../../useFormField";
import { ComboboxProps } from "../types";
interface InputContextType extends FormFieldType {
clearInput: (event: React.PointerEvent | React.KeyboardEvent) => void;
interface InputContextValue extends FormFieldType {
clearInput: NonNullable<ComboboxProps["onClear"]>;
error?: string;
focusInput: () => void;
inputRef: React.RefObject<HTMLInputElement>;
@@ -22,11 +22,16 @@ interface InputContextType extends FormFieldType {
searchTerm: string;
setSearchTerm: React.Dispatch<React.SetStateAction<string>>;
shouldAutocomplete?: boolean;
toggleOpenButtonRef: React.RefObject<HTMLButtonElement>;
}
const InputContext = createContext<InputContextType>({} as InputContextType);
const [InputContextProvider, useInputContext] =
createContext<InputContextValue>({
name: "InputContext",
errorMessage: "useInputContext must be used within an InputContextProvider",
});
export const InputContextProvider = ({ children, value: props }) => {
const InputProvider = ({ children, value: props }) => {
const {
defaultValue = "",
description,
@@ -52,6 +57,7 @@ export const InputContextProvider = ({ children, value: props }) => {
"comboboxfield",
);
const inputRef = useRef<HTMLInputElement | null>(null);
const toggleOpenButtonRef = useRef<HTMLButtonElement>(null);
const [internalValue, setInternalValue] = useState<string>(defaultValue);
const value = useMemo(
@@ -79,7 +85,7 @@ export const InputContextProvider = ({ children, value: props }) => {
);
const clearInput = useCallback(
(event: React.PointerEvent | React.KeyboardEvent) => {
(event: React.PointerEvent | React.KeyboardEvent | React.MouseEvent) => {
onClear?.(event);
externalOnChange?.(null, "");
setValue("");
@@ -98,33 +104,24 @@ export const InputContextProvider = ({ children, value: props }) => {
}
}, [value, searchTerm, shouldAutocomplete]);
const contextValue = {
...formFieldProps,
clearInput,
error,
focusInput,
inputRef,
value,
setValue,
onChange,
searchTerm,
setSearchTerm,
shouldAutocomplete,
toggleOpenButtonRef,
};
return (
<InputContext.Provider
value={{
...formFieldProps,
clearInput,
error,
focusInput,
inputRef,
value,
setValue,
onChange,
searchTerm,
setSearchTerm,
shouldAutocomplete,
}}
>
{children}
</InputContext.Provider>
<InputContextProvider {...contextValue}>{children}</InputContextProvider>
);
};
export const useInputContext = () => {
const context = useContext(InputContext);
if (!context) {
throw new Error(
"useInputContext must be used within an InputContextProvider",
);
}
return context;
};
export { InputProvider as InputContextProvider, useInputContext };
@@ -9,7 +9,7 @@ import { omit } from "../../../util";
import filteredOptionsUtil from "../FilteredOptions/filtered-options-util";
import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
import { useInputContext } from "./inputContext";
import { useInputContext } from "./Input.context";
interface InputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, "value"> {
@@ -128,6 +128,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
removeSelectedOption(lastSelectedOption);
}
}
} else if (e.key === "Enter" || e.key === "Accept") {
if (activeDecendantId || value) {
e.preventDefault();
}
} else if (e.key === "ArrowDown") {
// Check that cursor position is at the end of the input field,
// so we don't interfere with text editing
@@ -182,6 +186,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
{...omit(inputProps, ["aria-invalid"])}
ref={ref}
value={value}
onBlur={() => virtualFocus.moveFocusToTop()}
onChange={onChangeHandler}
type="text"
role="combobox"
@@ -0,0 +1,102 @@
/* eslint-disable jsx-a11y/no-static-element-interactions */
import cl from "clsx";
import React, { forwardRef } from "react";
import { XMarkIcon } from "@navikt/aksel-icons";
import { useMergeRefs } from "../../../util/hooks";
import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
import SelectedOptions from "../SelectedOptions/SelectedOptions";
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
import { ComboboxProps } from "../types";
import Input from "./Input";
import { useInputContext } from "./Input.context";
import ToggleListButton from "./ToggleListButton";
/* eslint-disable jsx-a11y/click-events-have-key-events */
export const InputController = forwardRef<
HTMLInputElement,
Omit<
ComboboxProps,
| "label"
| "description"
| "hideLabel"
| "onChange"
| "options"
| "size"
| "onClear"
| "value"
>
>((props, ref) => {
const {
clearButton = true,
clearButtonLabel,
toggleListButton = true,
toggleListButtonLabel,
inputClassName,
shouldShowSelectedOptions = true,
...rest
} = props;
const {
clearInput,
focusInput,
inputProps,
value,
size = "medium",
inputRef,
toggleOpenButtonRef,
} = useInputContext();
const { activeDecendantId } = useFilteredOptionsContext();
const { selectedOptions } = useSelectedOptionsContext();
const mergedInputRef = useMergeRefs(inputRef, ref);
return (
<div
className={cl("navds-combobox__wrapper-inner navds-text-field__input", {
"navds-combobox__wrapper-inner--virtually-unfocused":
activeDecendantId !== undefined,
})}
onClick={focusInput}
>
{!shouldShowSelectedOptions ? (
<Input
id={inputProps.id}
ref={mergedInputRef}
inputClassName={inputClassName}
{...rest}
/>
) : (
<SelectedOptions selectedOptions={selectedOptions} size={size}>
<Input
id={inputProps.id}
ref={mergedInputRef}
inputClassName={inputClassName}
{...rest}
/>
</SelectedOptions>
)}
<div>
{value && clearButton && (
<button
type="button"
onClick={clearInput}
className="navds-combobox__button-clear"
tabIndex={-1}
>
<span className="navds-sr-only">
{clearButtonLabel ? clearButtonLabel : "Tøm"}
</span>
<XMarkIcon aria-hidden />
</button>
)}
{toggleListButton && (
<ToggleListButton
toggleListButtonLabel={toggleListButtonLabel}
ref={toggleOpenButtonRef}
/>
)}
</div>
</div>
);
});
@@ -1,6 +1,6 @@
import React, { forwardRef } from "react";
import { ChevronDownIcon, ChevronUpIcon } from "@navikt/aksel-icons";
import { useFilteredOptionsContext } from "./FilteredOptions/filteredOptionsContext";
import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
interface ToggleListButtonProps {
toggleListButtonLabel?: string;
@@ -1,6 +1,6 @@
import React from "react";
import { Chips } from "../../../chips";
import { useInputContext } from "../Input/inputContext";
import { useInputContext } from "../Input/Input.context";
import { ComboboxOption } from "../types";
import { useSelectedOptionsContext } from "./selectedOptionsContext";
@@ -1,17 +1,12 @@
import React, {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from "react";
import React, { useCallback, useMemo, useState } from "react";
import { createContext } from "../../../util/create-context";
import { usePrevious } from "../../../util/hooks";
import { useInputContext } from "../Input/inputContext";
import { useInputContext } from "../Input/Input.context";
import { isInList } from "../combobox-utils";
import { useCustomOptionsContext } from "../customOptionsContext";
import { useComboboxCustomOptions } from "../customOptionsContext";
import { ComboboxOption, ComboboxProps, MaxSelected } from "../types";
type SelectedOptionsContextType = {
type SelectedOptionsContextValue = {
addSelectedOption: (option: ComboboxOption) => void;
isMultiSelect?: boolean;
removeSelectedOption: (option: ComboboxOption) => void;
@@ -25,11 +20,10 @@ type SelectedOptionsContextType = {
) => void;
};
const SelectedOptionsContext = createContext<SelectedOptionsContextType>(
{} as SelectedOptionsContextType,
);
const [SelectedOptionsContextProvider, useSelectedOptionsContext] =
createContext<SelectedOptionsContextValue>();
export const SelectedOptionsProvider = ({
const SelectedOptionsProvider = ({
children,
value,
}: {
@@ -45,7 +39,7 @@ export const SelectedOptionsProvider = ({
removeCustomOption,
addCustomOption,
setCustomOptions,
} = useCustomOptionsContext();
} = useComboboxCustomOptions();
const {
allowNewValues,
isMultiSelect,
@@ -149,18 +143,10 @@ export const SelectedOptionsProvider = ({
};
return (
<SelectedOptionsContext.Provider value={selectedOptionsState}>
<SelectedOptionsContextProvider {...selectedOptionsState}>
{children}
</SelectedOptionsContext.Provider>
</SelectedOptionsContextProvider>
);
};
export const useSelectedOptionsContext = () => {
const context = useContext(SelectedOptionsContext);
if (!context) {
throw new Error(
"useSelectedOptionsContext must be used within a SelectedOptionsProvider",
);
}
return context;
};
export { SelectedOptionsProvider, useSelectedOptionsContext };
@@ -2,7 +2,7 @@ import {
isInList,
mapToComboboxOptionArray,
toComboboxOption,
} from "./combobox-utils";
} from "../combobox-utils";
const list = [
{ label: "Hjelpemidler", value: "HJE" },
@@ -1,10 +1,9 @@
/* eslint-disable testing-library/no-unnecessary-act -- https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning */
import { render, screen } from "@testing-library/react";
import { act, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React, { useId } from "react";
import { act } from "react-dom/test-utils";
import { describe, expect, test, vi } from "vitest";
import { UNSAFE_Combobox } from "./index";
import { UNSAFE_Combobox } from "../index";
const options = [
"banana",
@@ -464,13 +464,13 @@ export const RemoveSelectedMultiSelectTest: StoryObject = {
},
};
export const AddWhenAddNewDisabledTest: StoryObject = {
export const AllowNewValuesTest: StoryObject = {
render: () => {
return (
<UNSAFE_Combobox
options={options}
label="Hva er dine favorittfrukter?"
isMultiSelect
allowNewValues
/>
);
},
@@ -497,6 +497,35 @@ export const AddWhenAddNewDisabledTest: StoryObject = {
},
};
export const DisallowNewValuesTest: StoryObject = {
render: () => {
return (
<UNSAFE_Combobox options={options} label="Hva er dine favorittfrukter?" />
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByLabelText("Hva er dine favorittfrukter?");
userEvent.click(input);
await userEvent.type(input, "aaa", { delay: 200 });
await sleep(250);
userEvent.keyboard("{ArrowDown}");
await sleep(250);
userEvent.keyboard("{ArrowDown}");
await sleep(250);
userEvent.keyboard("{Enter}");
await sleep(250);
userEvent.keyboard("{Escape}");
await sleep(250);
const invalidSelect = canvas.queryByLabelText("aaa slett");
expect(invalidSelect).not.toBeInTheDocument();
},
};
export const TestThatCallbacksOnlyFireWhenExpected: StoryObj<{
onChange: ReturnType<typeof fn>;
onClear: ReturnType<typeof fn>;
@@ -624,3 +653,71 @@ export const TestHoverAndFocusSwitching: StoryObject = {
);
},
};
export const TestEnterNotSubmittingForm: StoryObj<{
onSubmit: ReturnType<typeof fn>;
}> = {
args: {
onSubmit: fn(),
},
render: ({ onSubmit }) => {
return (
<form action="https://www.aksel.nav.no" method="get" onSubmit={onSubmit}>
<UNSAFE_Combobox
options={options}
label="Hva er dine favorittfrukter?"
isMultiSelect
allowNewValues
/>
</form>
);
},
play: async ({ canvasElement, args }) => {
args.onSubmit.mockClear();
const canvas = within(canvasElement);
const waitTime = 0; // Change for debugging
await sleep(waitTime);
const getInput = () =>
canvas.getByRole("combobox", {
name: "Hva er dine favorittfrukter?",
});
const getOption = (name: string, selected: boolean) =>
canvas.getByRole("option", { name, selected });
await userEvent.click(getInput(), { delay: waitTime });
await userEvent.keyboard("{ArrowDown}", { delay: waitTime });
expect(getInput().getAttribute("aria-activedescendant")).toBe(
getOption("banana", false).getAttribute("id"),
);
await userEvent.keyboard("{Enter}", { delay: waitTime });
expect(args.onSubmit).not.toHaveBeenCalled();
expect(getOption("banana", true)).toBeVisible();
await userEvent.keyboard("{Shift>}{Tab}", { delay: waitTime });
await userEvent.keyboard("{Enter}", { delay: waitTime });
expect(args.onSubmit).not.toHaveBeenCalled();
await userEvent.keyboard("test"); // Type option that does not exist
await userEvent.keyboard("{Enter}", { delay: waitTime });
expect(args.onSubmit).not.toHaveBeenCalled();
await userEvent.keyboard("{ArrowDown}", { delay: waitTime }); // Select "test" custom option
expect(
canvas.getByRole("option", { name: "test", selected: true }),
).toBeVisible();
await userEvent.keyboard("{Enter}", { delay: waitTime }); // De-select "test"
expect(
canvas.queryByRole("option", { name: "test", selected: false }),
).toBeNull();
expect(args.onSubmit).not.toHaveBeenCalled();
await userEvent.keyboard("{Escape}", { delay: waitTime }); // Clear input field
await userEvent.keyboard("{Enter}", { delay: waitTime }); // Enter on empty Input with closed list
expect(args.onSubmit).toHaveBeenCalledOnce();
},
};
@@ -1,19 +1,23 @@
import React, { createContext, useCallback, useContext, useState } from "react";
import { useInputContext } from "./Input/inputContext";
import React, { useCallback, useState } from "react";
import { createContext } from "../../util/create-context";
import { useInputContext } from "./Input/Input.context";
import { ComboboxOption } from "./types";
type CustomOptionsContextType = {
type CustomOptionsContextValue = {
customOptions: ComboboxOption[];
removeCustomOption: (option: ComboboxOption) => void;
addCustomOption: (option: ComboboxOption) => void;
setCustomOptions: React.Dispatch<React.SetStateAction<ComboboxOption[]>>;
};
const CustomOptionsContext = createContext<CustomOptionsContextType>(
{} as CustomOptionsContextType,
);
const [ComboboxCustomOptionsProvider, useComboboxCustomOptions] =
createContext<CustomOptionsContextValue>({
name: "ComboboxCustomOptions",
errorMessage:
"useComboboxCustomOptions must be used within a ComboboxCustomOptionsProvider",
});
export const CustomOptionsProvider = ({
const CustomOptionsProvider = ({
children,
value,
}: {
@@ -54,18 +58,10 @@ export const CustomOptionsProvider = ({
};
return (
<CustomOptionsContext.Provider value={customOptionsState}>
<ComboboxCustomOptionsProvider {...customOptionsState}>
{children}
</CustomOptionsContext.Provider>
</ComboboxCustomOptionsProvider>
);
};
export const useCustomOptionsContext = () => {
const context = useContext(CustomOptionsContext);
if (!context) {
throw new Error(
"useCustomOptionsContext must be used within a CustomOptionsProvider",
);
}
return context;
};
export { CustomOptionsProvider, useComboboxCustomOptions };
@@ -100,7 +100,9 @@ export interface ComboboxProps
*
* @param event
*/
onClear?: (event: React.PointerEvent | React.KeyboardEvent) => void;
onClear?: (
event: React.PointerEvent | React.KeyboardEvent | React.MouseEvent,
) => void;
/**
* Callback function triggered whenever an option is selected or de-selected.
*
@@ -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 }}
@@ -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;
@@ -0,0 +1,388 @@
import { Meta, StoryFn, StoryObj } from "@storybook/react";
import React from "react";
import { LinkIcon } from "@navikt/aksel-icons";
import { VStack } from "../../layout/stack";
import { Link } from "../../link";
import FormSummary from "./FormSummary";
const meta: Meta<typeof FormSummary> = {
title: "ds-react/FormSummary",
component: FormSummary,
decorators: [
(story) => (
<div style={{ width: 600, maxWidth: "100%", margin: "0 auto" }}>
{story()}
</div>
),
],
parameters: { layout: "padded", chromatic: { disable: true } },
};
export default meta;
export const Default: StoryFn<typeof FormSummary> = () => (
<FormSummary>
<FormSummary.Header>
<FormSummary.Heading level="2">Personalia</FormSummary.Heading>
<FormSummary.EditLink href="#" />
</FormSummary.Header>
<FormSummary.Answers>
<FormSummary.Answer>
<FormSummary.Label>Navn</FormSummary.Label>
<FormSummary.Value>Ola Nordmann</FormSummary.Value>
</FormSummary.Answer>
<FormSummary.Answer>
<FormSummary.Label>Adresse</FormSummary.Label>
<FormSummary.Value>
Gate 123
<br />
1234 Sted
</FormSummary.Value>
</FormSummary.Answer>
<FormSummary.Answer>
<FormSummary.Label>Fødselsnummer</FormSummary.Label>
<FormSummary.Value>12345678910</FormSummary.Value>
</FormSummary.Answer>
<FormSummary.Answer>
<FormSummary.Label>Barn nr. 1</FormSummary.Label>
<FormSummary.Value>
<FormSummary.Answers>
<FormSummary.Answer>
<FormSummary.Label>Navn</FormSummary.Label>
<FormSummary.Value>Kari Nordmann</FormSummary.Value>
</FormSummary.Answer>
<FormSummary.Answer>
<FormSummary.Label>Kjønn</FormSummary.Label>
<FormSummary.Value>Jente</FormSummary.Value>
</FormSummary.Answer>
<FormSummary.Answer>
<FormSummary.Label>Alder</FormSummary.Label>
<FormSummary.Value>6</FormSummary.Value>
</FormSummary.Answer>
</FormSummary.Answers>
</FormSummary.Value>
</FormSummary.Answer>
<FormSummary.Answer>
<FormSummary.Label>Barn nr. 2</FormSummary.Label>
<FormSummary.Value>
<FormSummary.Answers>
<FormSummary.Answer>
<FormSummary.Label>Navn</FormSummary.Label>
<FormSummary.Value>Per Nordmann</FormSummary.Value>
</FormSummary.Answer>
<FormSummary.Answer>
<FormSummary.Label>Kjønn</FormSummary.Label>
<FormSummary.Value>Gutt</FormSummary.Value>
</FormSummary.Answer>
<FormSummary.Answer>
<FormSummary.Label>Alder</FormSummary.Label>
<FormSummary.Value>7</FormSummary.Value>
</FormSummary.Answer>
</FormSummary.Answers>
</FormSummary.Value>
</FormSummary.Answer>
<FormSummary.Answer>
<FormSummary.Label>Kontaktinformasjon</FormSummary.Label>
<FormSummary.Value>
<Link>Fyll ut kontaktinformasjon</Link>
</FormSummary.Value>
</FormSummary.Answer>
<FormSummary.Answer>
<FormSummary.Label>Hvordan vil du bli varslet?</FormSummary.Label>
<FormSummary.Value>E-post</FormSummary.Value>
<FormSummary.Value>SMS</FormSummary.Value>
</FormSummary.Answer>
</FormSummary.Answers>
</FormSummary>
);
export const LongTexts: StoryFn<typeof FormSummary> = () => (
<FormSummary>
<FormSummary.Header>
<FormSummary.Heading level="2">
Arbeidsforhold som du har i eller utenfor EØS-området
</FormSummary.Heading>
<FormSummary.EditLink href="#">
<LinkIcon />
Jeg vil endre dette svaret
</FormSummary.EditLink>
</FormSummary.Header>
<FormSummary.Answers>
<FormSummary.Answer>
<FormSummary.Label>
Har du jobbet i et annet EØS-land, Sveits eller Storbritannia i løpet
av de siste 36 månedene? Om ja, hvilket land og hvor lenge jobbet du
der? Kan du fortelle litt om hva du jobbet med?
</FormSummary.Label>
<FormSummary.Value>
Nei, jeg har ikke jobbet i et annet EØS-land, Sveits eller
Storbritannia i løpet av de siste 36 månedene. Og jeg har heller ikke
jobbet i Norge i løpet av de siste 36 månedene. Men jeg har jobbet i
et annet land utenfor EØS-området. Det var i Sør-Korea, og jeg jobbet
som lærer på en internasjonal skole. Jeg jobbet der i 12 måneder, og
det var en veldig spennende opplevelse. Jeg lærte mye om koreansk
kultur og språk, og jeg fikk mange nye venner. Jeg savner Sør-Korea
veldig mye, og jeg håper å kunne dra tilbake.
<FormSummary.Answers>
<FormSummary.Answer>
<FormSummary.Label>
Har du jobbet i et annet EØS-land, Sveits eller Storbritannia i
løpet av de siste 36 månedene? Om ja, hvilket land og hvor lenge
jobbet du der? Kan du fortelle litt om hva du jobbet med?
</FormSummary.Label>
<FormSummary.Value>
Nei, jeg har ikke jobbet i et annet EØS-land, Sveits eller
Storbritannia i løpet av de siste 36 månedene. Og jeg har heller
ikke jobbet i Norge i løpet av de siste 36 månedene. Men jeg har
jobbet i et annet land utenfor EØS-området. Det var i Sør-Korea,
og jeg jobbet som lærer på en internasjonal skole. Jeg jobbet
der i 12 måneder, og det var en veldig spennende opplevelse. Jeg
lærte mye om koreansk kultur og språk, og jeg fikk mange nye
venner. Jeg savner Sør-Korea veldig mye, og jeg håper å kunne
dra tilbake.
</FormSummary.Value>
</FormSummary.Answer>
</FormSummary.Answers>
</FormSummary.Value>
</FormSummary.Answer>
</FormSummary.Answers>
</FormSummary>
);
export const NoLink: StoryFn<typeof FormSummary> = () => (
<FormSummary>
<FormSummary.Header>
<FormSummary.Heading level="2">
Arbeidsforhold som du har i eller utenfor EØS-området
</FormSummary.Heading>
</FormSummary.Header>
<FormSummary.Answers>
<FormSummary.Answer>
<FormSummary.Label>Navn</FormSummary.Label>
<FormSummary.Value>Ola Nordmann</FormSummary.Value>
</FormSummary.Answer>
<FormSummary.Answer>
<FormSummary.Label>Fødselsnummer</FormSummary.Label>
<FormSummary.Value>12345678910</FormSummary.Value>
</FormSummary.Answer>
</FormSummary.Answers>
</FormSummary>
);
const answers = [
{
label: "Fortell din livshistorie",
value: `Det hele startet da jeg ble født en kald vinterdag i desember.
Jeg var en litt spesiell baby, for jeg hadde masse hår på hodet. Mamma
og pappa ble veldig overrasket da de så meg for første gang. De hadde egentlig
forventet at jeg skulle være helt normal og kjedelig. Du vil kanskje ikke høre resten
av historien min, for den er ganske lang og kjedelig...`,
},
{
label: "Fødselsnummer",
value: "12345678910",
},
{
label: "Ting du har i sekken.",
value: [
{
label: "Saks x2",
value: `Jeg liker å ha med meg en saks i sekken min. Det er alltid kjekt å ha en saks. Jeg pakker gjerne med meg to saks. Da har jeg en reserve hvis den første saksen min skulle bli borte. Det er veldig kjekt.`,
},
{
label: "Blyant",
value:
"Det er ikke mulig å skrive uten en blyant. Da blir det bare rot.",
},
{
label: "Kake",
value: `Jeg har alltid en kake i sekken min. Den blir litt most etter en
stund i sekken min men det gjør ingenting. Jeg liker moste kaker. Det er veldig godt.`,
},
{
label: "Kaffekopp",
value: "Dette har jeg ikke i sekken min",
},
],
},
{
label: "Flere ting du har i sekken.",
value: [
{
label: "Saks",
value: `En saks til`,
},
],
},
];
export const RealisticUsage: StoryFn<typeof FormSummary> = () => (
<FormSummary>
<FormSummary.Header>
<FormSummary.Heading level="2">
Diverse informasjon om forskjellige ting og tang. Kjekt å vite.
</FormSummary.Heading>
<FormSummary.EditLink href="#" />
</FormSummary.Header>
<FormSummary.Answers>
{answers.map((answer, index) => (
<FormSummary.Answer key={index}>
<FormSummary.Label>{answer.label}</FormSummary.Label>
<FormSummary.Value>
{Array.isArray(answer.value) ? (
<FormSummary.Answers>
{answer.value.map((subAnswer, subIndex) => (
<FormSummary.Answer key={subIndex}>
<FormSummary.Label>{subAnswer.label}</FormSummary.Label>
<FormSummary.Value>{subAnswer.value}</FormSummary.Value>
</FormSummary.Answer>
))}
</FormSummary.Answers>
) : (
answer.value
)}
</FormSummary.Value>
</FormSummary.Answer>
))}
</FormSummary.Answers>
</FormSummary>
);
export const Empty: StoryFn<typeof FormSummary> = () => (
<VStack gap="8">
<FormSummary>
<FormSummary.Header>
<FormSummary.Heading level="2">Just Header</FormSummary.Heading>
<FormSummary.EditLink href="#" />
</FormSummary.Header>
</FormSummary>
<FormSummary>
<FormSummary.Header>
<FormSummary.Heading level="2">Empty Answers</FormSummary.Heading>
<FormSummary.EditLink href="#" />
</FormSummary.Header>
<FormSummary.Answers>{null}</FormSummary.Answers>
</FormSummary>
<FormSummary>
<FormSummary.Header>
<FormSummary.Heading level="2">Empty Answer</FormSummary.Heading>
<FormSummary.EditLink href="#" />
</FormSummary.Header>
<FormSummary.Answers>
<FormSummary.Answer>{null}</FormSummary.Answer>
</FormSummary.Answers>
</FormSummary>
<FormSummary>
<FormSummary.Header>
<FormSummary.Heading level="2">Empty Label & Value</FormSummary.Heading>
<FormSummary.EditLink href="#" />
</FormSummary.Header>
<FormSummary.Answers>
<FormSummary.Answer>
<FormSummary.Label>{null}</FormSummary.Label>
<FormSummary.Value>{null}</FormSummary.Value>
</FormSummary.Answer>
</FormSummary.Answers>
</FormSummary>
<FormSummary>
<FormSummary.Header>
<FormSummary.Heading level="2">Empty Value</FormSummary.Heading>
<FormSummary.EditLink href="#" />
</FormSummary.Header>
<FormSummary.Answers>
<FormSummary.Answer>
<FormSummary.Label>Adresse</FormSummary.Label>
<FormSummary.Value>{null}</FormSummary.Value>
</FormSummary.Answer>
</FormSummary.Answers>
</FormSummary>
</VStack>
);
export const Chromatic: StoryObj<typeof FormSummary> = {
render: () => (
<div>
<div>
<h2>Default</h2>
<Default>{null}</Default>
</div>
<div>
<h2>Long Texts</h2>
<LongTexts>{null}</LongTexts>
</div>
<div>
<h2>No Link</h2>
<NoLink>{null}</NoLink>
</div>
<div>
<h2>Realistic Usage</h2>
<RealisticUsage>{null}</RealisticUsage>
</div>
<div>
<h2>Empty</h2>
<Empty>{null}</Empty>
</div>
</div>
),
parameters: {
chromatic: {
disable: false,
modes: {
default: {
viewport: {
width: 620,
},
},
sm: {
viewport: {
width: 479,
},
},
},
},
},
};
export const CommaSeparated: StoryFn<typeof FormSummary> = () => (
<FormSummary>
<FormSummary.Header>
<FormSummary.Heading level="2">Personalia</FormSummary.Heading>
<FormSummary.EditLink href="#" />
</FormSummary.Header>
<FormSummary.Answers>
<FormSummary.Answer>
<FormSummary.Label>
Hvilket transportmiddel er du mest kjent for?
</FormSummary.Label>
<FormSummary.Value>
sykkel, tog, bil, buss, sparkesykkel
</FormSummary.Value>
</FormSummary.Answer>
<FormSummary.Answer>
<FormSummary.Label>Favoritt slagord?</FormSummary.Label>
<FormSummary.Value>{`I'll be back.`}</FormSummary.Value>
<FormSummary.Value>Hasta la vista, baby.</FormSummary.Value>
<FormSummary.Value>Say hello to my little friend.</FormSummary.Value>
</FormSummary.Answer>
</FormSummary.Answers>
</FormSummary>
);
@@ -0,0 +1,106 @@
import cl from "clsx";
import React, { HTMLAttributes, forwardRef } from "react";
import FormSummaryAnswer from "./FormSummaryAnswer";
import FormSummaryAnswers from "./FormSummaryAnswers";
import FormSummaryEditLink from "./FormSummaryEditLink";
import FormSummaryHeader from "./FormSummaryHeader";
import FormSummaryHeading from "./FormSummaryHeading";
import FormSummaryLabel from "./FormSummaryLabel";
import FormSummaryValue from "./FormSummaryValue";
interface FormSummaryComponent
extends React.ForwardRefExoticComponent<
FormSummaryProps & React.RefAttributes<HTMLDivElement>
> {
/**
* Must include `<FormSummary.Heading>` and optionally `<FormSummary.EditLink>`.
*/
Header: typeof FormSummaryHeader;
/**
* Typographic Heading to use in the `FormSummary.Header` component.
*/
Heading: typeof FormSummaryHeading;
/**
* Link to edit the answers to use in the `FormSummary.Header` component. Should link to the relevant part of the form.
*/
EditLink: typeof FormSummaryEditLink;
/**
* Wrapper component for the answers.
*/
Answers: typeof FormSummaryAnswers;
/**
* Wrapper component for each answer. To be used in the `FormSummary.Answers` component.
*/
Answer: typeof FormSummaryAnswer;
/**
* Corresponds to the question in the form. To be used in the `FormSummary.Answer` component.
*/
Label: typeof FormSummaryLabel;
/**
* Corresponds to the answer in the form. To be used in the `FormSummary.Answer` component.
*/
Value: typeof FormSummaryValue;
}
export interface FormSummaryProps extends HTMLAttributes<HTMLDivElement> {
/**
* Must include:
*
* - `<FormSummary.Header>`
* - `<FormSummary.Answers>`
*
* @example
* <FormSummary>
* <FormSummary.Header>
* <FormSummary.Heading level="2">HeadingTekst</FormSummary.Heading>
* <FormSummary.EditLink href="#" />
* </FormSummary.Header>
* <FormSummary.Answers>
* <FormSummary.Answer>
* <FormSummary.Label>Navn</FormSummary.Label>
* <FormSummary.Value>Ola Nordmann</FormSummary.Value>
* </FormSummary.Answer>
* </FormSummary.Answers>
* </FormSummary>
*/
children: React.ReactNode;
}
/**
* A summary of a previously answered form.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/formsummary)
*
* @example
* <FormSummary>
* <FormSummary.Header>
* <FormSummary.Heading level="2">HeadingTekst</FormSummary.Heading>
* <FormSummary.EditLink href="#" />
* </FormSummary.Header>
* <FormSummary.Answers>
* <FormSummary.Answer>
* <FormSummary.Label>Navn</FormSummary.Label>
* <FormSummary.Value>Ola Nordmann</FormSummary.Value>
* </FormSummary.Answer>
* </FormSummary.Answers>
* </FormSummary>
*/
export const FormSummary = forwardRef<HTMLDivElement, FormSummaryProps>(
({ children, className, ...rest }, ref) => {
return (
<div ref={ref} {...rest} className={cl("navds-form-summary", className)}>
{children}
</div>
);
},
) as FormSummaryComponent;
FormSummary.Header = FormSummaryHeader;
FormSummary.Heading = FormSummaryHeading;
FormSummary.EditLink = FormSummaryEditLink;
FormSummary.Answers = FormSummaryAnswers;
FormSummary.Answer = FormSummaryAnswer;
FormSummary.Label = FormSummaryLabel;
FormSummary.Value = FormSummaryValue;
export default FormSummary;
@@ -0,0 +1,27 @@
import cl from "clsx";
import React from "react";
export interface FormSummaryAnswerProps
extends React.HTMLAttributes<HTMLDivElement> {
/**
* Must include:
* - `<FormSummary.Label>`
* - `<FormSummary.Value>`
*/
children: React.ReactNode;
}
export const FormSummaryAnswer = React.forwardRef<
HTMLDivElement,
FormSummaryAnswerProps
>(({ children, className, ...rest }, ref) => (
<div
ref={ref}
{...rest}
className={cl("navds-form-summary__answer", className)}
>
{children}
</div>
));
export default FormSummaryAnswer;
@@ -0,0 +1,25 @@
import cl from "clsx";
import React, { forwardRef } from "react";
export interface FormSummaryAnswersProps
extends React.HTMLAttributes<HTMLDListElement> {
/**
* Must include one or more of `<FormSummary.Answer>`.
*/
children: React.ReactNode;
}
export const FormSummaryAnswers = forwardRef<
HTMLDListElement,
FormSummaryAnswersProps
>(({ children, className, ...rest }: FormSummaryAnswersProps, ref) => (
<dl
ref={ref}
{...rest}
className={cl("navds-form-summary__answers", className)}
>
{children}
</dl>
));
export default FormSummaryAnswers;
@@ -0,0 +1,35 @@
import cl from "clsx";
import React, { forwardRef } from "react";
import { Link } from "../../link";
import { OverridableComponent } from "../../util/types";
// export type FormSummaryEditProps = Partial<LinkProps>;
export interface FormSummaryEditProps
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
/**
* URL to the relevant part of the form, where the answers can be edited.
*/
href?: string;
/**
* Link text.
*
* [How to write good links](https://aksel.nav.no/god-praksis/artikler/lenker).
* @default "Endre svar"
*/
children?: React.ReactNode;
}
export const FormSummaryEditLink: OverridableComponent<
FormSummaryEditProps,
HTMLAnchorElement
> = forwardRef(({ children = "Endre svar", className, ...rest }, ref) => (
<Link
ref={ref}
{...rest}
className={cl("navds-form-summary__edit", className)}
>
{children}
</Link>
));
export default FormSummaryEditLink;
@@ -0,0 +1,25 @@
import cl from "clsx";
import React, { forwardRef } from "react";
export interface FormSummaryHeaderProps
extends React.HTMLAttributes<HTMLDivElement> {
/**
* Must include `<FormSummary.Heading>` and optionally `<FormSummary.EditLink>`.
*/
children: React.ReactNode;
}
export const FormSummaryHeader = forwardRef<
HTMLDivElement,
FormSummaryHeaderProps
>(({ children, className, ...rest }, ref) => (
<header
ref={ref}
{...rest}
className={cl("navds-form-summary__header", className)}
>
{children}
</header>
));
export default FormSummaryHeader;
@@ -0,0 +1,23 @@
import React, { forwardRef } from "react";
import { Heading, HeadingProps } from "../../typography";
export interface FormSummaryHeadingProps
extends React.HTMLAttributes<HTMLHeadingElement> {
/**
* Heading text.
*/
children: React.ReactNode;
/**
* The heading level.
*/
level: Exclude<HeadingProps["level"], "1">;
}
export const FormSummaryHeading = forwardRef<
HTMLHeadingElement,
FormSummaryHeadingProps
>((props: FormSummaryHeadingProps, ref) => (
<Heading ref={ref} {...props} size="medium" />
));
export default FormSummaryHeading;
@@ -0,0 +1,17 @@
import React, { forwardRef } from "react";
import { Label } from "../../typography";
export interface FormSummaryLabelProps
extends React.HTMLAttributes<HTMLElement> {
children: React.ReactNode;
}
export const FormSummaryLabel = forwardRef<HTMLElement, FormSummaryLabelProps>(
({ children, ...rest }, ref) => (
<Label ref={ref} {...rest} as="dt">
{children}
</Label>
),
);
export default FormSummaryLabel;
@@ -0,0 +1,24 @@
import cl from "clsx";
import React, { forwardRef } from "react";
import { BodyLong } from "../../typography";
export interface FormSummaryValueProps
extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
export const FormSummaryValue = forwardRef<
HTMLDivElement,
FormSummaryValueProps
>(({ children, className, ...rest }, ref) => (
<BodyLong
ref={ref}
{...rest}
as="dd"
className={cl("navds-form-summary__value", className)}
>
{children}
</BodyLong>
));
export default FormSummaryValue;
@@ -0,0 +1,30 @@
"use client";
export { default as FormSummary, type FormSummaryProps } from "./FormSummary";
export {
default as FormSummaryAnswer,
type FormSummaryAnswerProps,
} from "./FormSummaryAnswer";
export {
default as FormSummaryAnswers,
type FormSummaryAnswersProps,
} from "./FormSummaryAnswers";
export {
default as FormSummaryEditLink,
type FormSummaryEditProps,
} from "./FormSummaryEditLink";
export {
default as FormSummaryHeader,
type FormSummaryHeaderProps,
} from "./FormSummaryHeader";
export {
default as FormSummaryHeading,
type FormSummaryHeadingProps,
} from "./FormSummaryHeading";
export {
default as FormSummaryLabel,
type FormSummaryLabelProps,
} from "./FormSummaryLabel";
export {
default as FormSummaryValue,
type FormSummaryValueProps,
} from "./FormSummaryValue";
@@ -18,7 +18,7 @@ export const RadioGroupContext =
export interface RadioGroupProps
extends Omit<
FieldsetProps,
"onChange" | "errorPropagation" | "defaultValue"
"onChange" | "errorPropagation" | "defaultValue" | "nativeReadOnly"
> {
/**
* Collection of `<Radio />`-elements
+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;
}
+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>
),
+17 -15
View File
@@ -26,8 +26,8 @@ export { Dropdown, type DropdownProps } from "./dropdown";
export { ExpansionCard, type ExpansionCardProps } from "./expansion-card";
export {
GuidePanel,
type GuidePanelProps,
GuidePanelDefaultIllustration,
type GuidePanelProps,
} from "./guide-panel";
export { HelpText, type HelpTextProps } from "./help-text";
export {
@@ -59,6 +59,7 @@ export { Modal, type ModalProps } from "./modal";
export { Pagination, type PaginationProps } from "./pagination";
export { Popover, type PopoverProps } from "./popover";
export { Portal, type PortalProps } from "./portal";
export { ProgressBar, type ProgressBarProps } from "./progress-bar";
export { Provider, type ProviderProps } from "./provider";
export { ReadMore, type ReadMoreProps } from "./read-more";
export { Skeleton, type SkeletonProps } from "./skeleton";
@@ -125,6 +126,21 @@ export {
} from "./form/confirmation-panel";
export { ErrorSummary, type ErrorSummaryProps } from "./form/error-summary";
export { Fieldset, type FieldsetProps } from "./form/fieldset";
export {
UNSAFE_FileUpload,
type FileAccepted,
type FileItem,
type FileMetadata,
type FileObject,
type FileRejected,
type FileRejectedPartitioned,
type FileRejectionReason,
type FileUploadDropzoneProps,
type FileUploadItemProps,
type FileUploadTriggerProps,
type FilesPartitioned,
} from "./form/file-upload";
export { FormSummary, type FormSummaryProps } from "./form/form-summary";
export {
Radio,
RadioGroup,
@@ -136,20 +152,6 @@ export { Select, type SelectProps } from "./form/select";
export { Switch, type SwitchProps } from "./form/switch";
export { Textarea, type TextareaProps } from "./form/textarea";
export { TextField, type TextFieldProps } from "./form/textfield";
export {
UNSAFE_FileUpload,
type FileUploadDropzoneProps,
type FileUploadTriggerProps,
type FileObject,
type FileRejected,
type FileAccepted,
type FileRejectedPartitioned,
type FilesPartitioned,
type FileRejectionReason,
type FileUploadItemProps,
type FileItem,
type FileMetadata,
} from "./form/file-upload";
/**
* @deprecated
@@ -1,4 +1,4 @@
import { Meta } from "@storybook/react";
import { Meta, StoryObj } from "@storybook/react";
import React from "react";
import {
ChevronDownIcon,
@@ -7,7 +7,7 @@ import {
LeaveIcon,
MenuGridIcon,
} from "@navikt/aksel-icons";
import { BodyLong, BodyShort, Detail, Spacer } from "@navikt/ds-react";
import { BodyLong, BodyShort, Detail, Spacer, VStack } from "@navikt/ds-react";
import { Dropdown } from "../dropdown";
import InternalHeader from "./InternalHeader";
@@ -22,8 +22,13 @@ export default {
},
},
},
parameters: {
chromatic: { disable: true },
},
} as Meta;
type Story = StoryObj<typeof InternalHeader>;
export const Default = {
render: (props) => {
return (
@@ -227,3 +232,37 @@ export const UserWithMenuGridIconMenu = () => (
<InternalHeader.User name="Ola Normann" description="id: 123456" />
</InternalHeader>
);
export const Chromatic: Story = {
render: () => (
<VStack gap="4">
<div>
<h2>TitleAsHeading</h2>
<TitleAsHeading />
</div>
<div>
<h2>TitleAsLink</h2>
<TitleAsLink />
</div>
<div>
<h2>User</h2>
<User />
</div>
<div>
<h2>UserWithDescription</h2>
<UserWithDescription />
</div>
<div>
<h2>UserWithMenu</h2>
<UserWithMenu />
</div>
<div>
<h2>UserWithMenuGridIconMenu</h2>
<UserWithMenuGridIconMenu />
</div>
</VStack>
),
parameters: {
chromatic: { disable: false },
},
};
@@ -1,4 +1,4 @@
import type { Meta } from "@storybook/react";
import type { Meta, StoryObj } from "@storybook/react";
import React, { ReactNode } from "react";
import { ChevronRightIcon } from "@navikt/aksel-icons";
import { BodyLong, Detail, Heading } from "../../typography";
@@ -10,9 +10,15 @@ import { Box } from "./Box";
const meta: Meta<typeof Box> = {
title: "ds-react/Primitives/Box",
component: Box,
parameters: {
chromatic: { disable: true },
},
};
export default meta;
type Story = StoryObj<typeof Box>;
export const Default = () => (
<Box>
<BodyLong>
@@ -378,3 +384,77 @@ export const PaddingDemo = () => (
</Box>
</VStack>
);
export const Chromatic: Story = {
render: () => (
<VStack gap="2">
<div>
<h2>Default</h2>
<Default />
</div>
<div>
<h2>As card</h2>
<AsCard />
</div>
<div>
<h2>Theming demo</h2>
<ThemingDemo />
</div>
<div>
<h2>Padding breakpoints</h2>
<PaddingBreakpoints.render />
</div>
<div>
<h2>Padding breakpoints inherit 1</h2>
<PaddingBreakpointsInherit1.render />
</div>
<div>
<h2>Padding breakpoints inherit 2</h2>
<PaddingBreakpointsInherit2 />
</div>
<div>
<h2>Padding</h2>
<Padding />
</div>
<div>
<h2>Box in box</h2>
<BoxInBox />
</div>
<div>
<h2>With HGrid</h2>
<WithHGrid />
</div>
<div>
<h2>Border width</h2>
<BorderWidth />
</div>
<div>
<h2>Border radius</h2>
<BorderRadius />
</div>
<div>
<h2>Padding demo</h2>
<PaddingDemo />
</div>
</VStack>
),
parameters: {
chromatic: {
disable: false,
modes: {
mobile_portrait: {
viewport: {
width: 400,
height: 850,
},
},
desktop: {
viewport: {
width: 1280,
height: 960,
},
},
},
},
},
};
@@ -1,3 +1,4 @@
import { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { HGrid } from ".";
import { VStack } from "../stack";
@@ -13,6 +14,7 @@ export default {
component: HGrid,
parameters: {
layout: "fullscreen",
chromatic: { disable: true },
},
argTypes: {
columnsType: {
@@ -21,7 +23,9 @@ export default {
control: { type: "radio" },
},
},
};
} as Meta;
type Story = StoryObj<typeof HGrid>;
/* const getColumnsProp = () */
@@ -120,3 +124,50 @@ function Placeholder({ text, height }: { text: string; height?: string }) {
</div>
);
}
export const Chromatic: Story = {
render: () => (
<VStack gap="4">
<div>
<h2>Gap</h2>
<Gap.render />
</div>
<div>
<h2>DynamicGap</h2>
<DynamicGap.render />
</div>
<div>
<h2>Columns</h2>
<Columns.render />
</div>
<div>
<h2>DynamicColumns</h2>
<DynamicColumns.render />
</div>
<div>
<h2>AlignItems</h2>
<AlignItems.render />
</div>
</VStack>
),
parameters: {
layout: "fullscreen",
chromatic: {
disable: false,
modes: {
mobile_portrait: {
viewport: {
width: 400,
height: 850,
},
},
desktop: {
viewport: {
width: 1280,
height: 960,
},
},
},
},
},
};
@@ -37,7 +37,7 @@ export const Default: StoryFn = ({
Default.argTypes = {
width: {
control: "radio",
options: widths,
options: [...widths],
},
background: {
control: "radio",
@@ -12,6 +12,6 @@ import React from "react";
* <MyComponent />
* </HStack>
*/
export const Spacer = () => <div className="navds-stack__spacer" />;
export const Spacer = () => <span className="navds-stack__spacer" />;
export default Spacer;
+48 -1
View File
@@ -1,4 +1,4 @@
import { Meta } from "@storybook/react";
import { Meta, StoryObj } from "@storybook/react";
import React from "react";
import {
BabyWrappedIcon,
@@ -12,8 +12,13 @@ import List from "./List";
export default {
title: "ds-react/List",
component: List,
parameters: {
chromatic: { disable: true },
},
} as Meta;
type Story = StoryObj<typeof List>;
export const Default = {
render: () => {
return (
@@ -278,3 +283,45 @@ export const Nested = {
);
},
};
export const Chromatic: Story = {
render: () => (
<VStack gap="2">
<div>
<h2>Default</h2>
<Default.render />
</div>
<div>
<h2>Ordered</h2>
<Ordered.render />
</div>
<div>
<h2>WithHeading</h2>
<WithHeading.render />
</div>
<div>
<h2>SizesUl</h2>
<SizesUl.render />
</div>
<div>
<h2>SizesOl</h2>
<SizesOl.render />
</div>
<div>
<h2>SizesIcons</h2>
<SizesIcons.render />
</div>
<div>
<h2>Icons</h2>
<Icons.render />
</div>
<div>
<h2>Nested</h2>
<Nested.render />
</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 Loader, { LoaderProps } from "./Loader";
export default {
@@ -27,8 +28,13 @@ export default {
},
},
},
parameters: {
chromatic: { disable: false },
},
} as Meta;
type Story = StoryObj<typeof Loader>;
export const Default = {
render: (props: LoaderProps) => {
return <Loader {...props} />;
@@ -80,3 +86,25 @@ export const Transparent = () => (
</div>
</div>
);
export const Chromatic: Story = {
render: () => (
<VStack gap="2">
<div>
<h2>Size</h2>
<Size />
</div>
<div>
<h2>Variant</h2>
<Variant />
</div>
<div>
<h2>Transparent</h2>
<Transparent />
</div>
</VStack>
),
parameters: {
chromatic: { disable: false },
},
};
+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,
},
},
},
},
};
+5
View File
@@ -0,0 +1,5 @@
# DismissableLayer API
`DismissableLayer`-API provides a robust handler for layers that can be dismissed by interacting outside of it.
One of the benefits is its ability to handle nested elements, relevant when creating nested dropdowns and similar solutions.
@@ -0,0 +1,201 @@
import React, { useRef, useState } from "react";
import { DismissableLayer } from "./DismissableLayer";
export default {
title: "Utilities/DismissableLayer",
parameters: {
chromatic: { disable: true },
},
};
export const Default = () => {
const [open, setOpen] = useState(false);
const openButtonRef = useRef(null);
const [dismissOnEscape, setDismissOnEscape] = useState(false);
const [dismissOnPointerDownOutside, setDismissOnPointerDownOutside] =
useState(false);
const [dismissOnFocusOutside, setDismissOnFocusOutside] = useState(false);
const [disabledOutsidePointerEvents, setDisableOutsidePointerEvents] =
useState(false);
return (
<div style={{ fontFamily: "sans-serif", textAlign: "center" }}>
<h1>DismissableLayer</h1>
<div
style={{ display: "inline-block", textAlign: "left", marginBottom: 20 }}
>
<label style={{ display: "block" }}>
<input
type="checkbox"
checked={dismissOnEscape}
onChange={(event) => setDismissOnEscape(event.target.checked)}
/>{" "}
Dismiss on escape?
</label>
<label style={{ display: "block" }}>
<input
type="checkbox"
checked={dismissOnPointerDownOutside}
onChange={(event) =>
setDismissOnPointerDownOutside(event.target.checked)
}
/>{" "}
Dismiss on pointer down outside?
</label>
<label style={{ display: "block" }}>
<input
type="checkbox"
checked={dismissOnFocusOutside}
onChange={(event) => setDismissOnFocusOutside(event.target.checked)}
/>{" "}
Dismiss on focus outside?
</label>
<hr />
<label style={{ display: "block" }}>
<input
type="checkbox"
checked={disabledOutsidePointerEvents}
onChange={(event) =>
setDisableOutsidePointerEvents(event.target.checked)
}
/>{" "}
Disable outside pointer events?
</label>
</div>
<div style={{ marginBottom: 20 }}>
<button
ref={openButtonRef}
type="button"
onClick={() => setOpen((x) => !x)}
>
{open ? "Close" : "Open"} layer
</button>
</div>
{open ? (
<DismissableLayer
asChild
onEscapeKeyDown={(event) => {
console.log("Escape key down");
if (dismissOnEscape === false) {
event.preventDefault();
}
}}
onPointerDownOutside={(event) => {
console.log("Pointer down outside");
if (
dismissOnPointerDownOutside === false ||
event.target === openButtonRef.current
) {
event.preventDefault();
}
}}
onFocusOutside={(event) => {
console.log("Focus outside");
if (dismissOnFocusOutside === false) {
event.preventDefault();
}
}}
disableOutsidePointerEvents={disabledOutsidePointerEvents}
onDismiss={() => setOpen(false)}
style={{
display: "inline-flex",
justifyContent: "center",
alignItems: "center",
verticalAlign: "middle",
width: 400,
height: 300,
backgroundColor: "black",
borderRadius: 10,
marginBottom: 20,
}}
>
<input type="text" />
</DismissableLayer>
) : null}
<div style={{ marginBottom: 20 }}>
<input type="text" defaultValue="hello" style={{ marginRight: 20 }} />
<button type="button" onMouseDown={() => alert("hey!")}>
hey!
</button>
</div>
</div>
);
};
export const Nested = () => {
return (
<div style={{ fontFamily: "sans-serif", textAlign: "center" }}>
<h1>DismissableLayer (nested)</h1>
<DismissableBox index={0} />
</div>
);
};
function DismissableBox(props) {
const [open, setOpen] = React.useState(false);
const openButtonRef = React.useRef(null);
return (
<DismissableLayer
{...props}
onEscapeKeyDown={() => {
console.log("Escape key down");
}}
disableOutsidePointerEvents={props.index % 3 === 0}
style={{
display: "inline-block",
verticalAlign: "middle",
padding: 20,
backgroundColor: "rgba(0, 0, 0, 0.2)",
borderRadius: 10,
marginTop: 20,
...props.style,
}}
>
<div>
<button
ref={openButtonRef}
type="button"
onClick={() => setOpen((x) => !x)}
>
{open ? "Close" : "Open"} new layer
</button>
</div>
{open ? (
<DismissableBox
index={props.index + 1}
onPointerDownOutside={(event) => {
if (event.target === openButtonRef.current) {
event.preventDefault();
}
}}
onFocusOutside={(event) => event.preventDefault()}
onDismiss={() => setOpen(false)}
/>
) : null}
</DismissableLayer>
);
}
export const DisableOutsidePointerEventsWhileHidden = () => (
<div>
<DismissableLayer disableOutsidePointerEvents={true} enabled={false}>
<div style={{ height: 100, width: 100, background: "red" }}>
<DismissableLayer disableOutsidePointerEvents={true} enabled={false}>
<div style={{ height: 50, width: 50, background: "blue" }} />
</DismissableLayer>
</div>
</DismissableLayer>
<button onClick={console.log}>Should be clickable</button>
</div>
);
@@ -0,0 +1,368 @@
import React, {
CSSProperties,
forwardRef,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { Slot } from "../../util/Slot";
import { useMergeRefs } from "../../util/hooks";
import { createDescendantContext } from "../../util/hooks/descendants/useDescendant";
import { AsChild } from "../../util/types/AsChild";
import {
CustomFocusEvent,
CustomPointerDownEvent,
} from "./util/dispatchCustomEvent";
import { useEscapeKeydown } from "./util/useEscapeKeydown";
import { useFocusOutside } from "./util/useFocusOutside";
import { usePointerDownOutside } from "./util/usePointerDownOutside";
interface DismissableLayerBaseProps {
/**
* When `true`, hover/focus/click interactions will be disabled on elements outside
* the `DismissableLayer`. Users will need to click twice on outside elements to
* interact with them: once to close the `DismissableLayer`, and again to trigger the element.
*/
disableOutsidePointerEvents?: boolean;
/**
* Event handler called when the escape key is down.
* Can be prevented.
*/
onEscapeKeyDown?: (event: KeyboardEvent) => void;
/**
* Event handler called when the a `pointerdown` event happens outside of the `DismissableLayer`.
* Can be prevented.
*/
onPointerDownOutside?: (event: CustomPointerDownEvent) => void;
/**
* Event handler called when the focus moves outside of the `DismissableLayer`.
* Can be prevented.
*/
onFocusOutside?: (event: CustomFocusEvent) => void;
/**
* Event handler called when an interaction happens outside the `DismissableLayer`.
* Specifically, when a `pointerdown` event happens outside or focus moves outside of it.
* Can be prevented.
*/
onInteractOutside?: (
event: CustomPointerDownEvent | CustomFocusEvent,
) => void;
/**
* Handler called when the `DismissableLayer` should be dismissed
*/
onDismiss?: () => void;
/**
* Stops `onDismiss` from beeing called when interacting with the `safeZone` elements.
* `safeZone.dismissable` is only needed when its element does not have a `tabIndex` since it will not receive focus-events.
*/
safeZone?: {
anchor?: Element | null;
dismissable?: Element | null;
};
style?: CSSProperties;
/**
* Disables layer from beeing counted in context for nested `DismissableLayer`.
*/
enabled?: boolean;
}
type DismissableLayerProps = DismissableLayerBaseProps & AsChild;
export const [
DismissableDescendantsProvider,
useDismissableDescendantsContext,
useDismissableDescendants,
useDismissableDescendant,
] = createDescendantContext<
HTMLDivElement,
{ disableOutsidePointerEvents: boolean }
>();
let originalBodyPointerEvents: string;
const DismissableLayer = forwardRef<HTMLDivElement, DismissableLayerProps>(
(props: DismissableLayerProps, ref) => {
const context = useDismissableDescendantsContext(false);
/**
* To correctly handle nested DismissableLayer,
* we only initialize the `Descendants`-API for the root layer to aboid resetting context
*/
return context ? (
<DismissableLayerNode ref={ref} {...props} />
) : (
<DismissableRoot>
<DismissableLayerNode ref={ref} {...props} />
</DismissableRoot>
);
},
);
/**
* DismissableRoot
*
* Used to initialize the `Descendants`-API at the root layer.
* All subsequent layers will use the same context.
*/
const DismissableRoot = ({ children }: { children: React.ReactNode }) => {
const descendants = useDismissableDescendants();
return (
<DismissableDescendantsProvider value={descendants}>
{children}
</DismissableDescendantsProvider>
);
};
const DismissableLayerNode = forwardRef<HTMLDivElement, DismissableLayerProps>(
(
{
children,
asChild,
onEscapeKeyDown,
onPointerDownOutside,
onFocusOutside,
onInteractOutside,
onDismiss,
safeZone,
disableOutsidePointerEvents = false,
enabled = true,
...rest
}: DismissableLayerProps,
ref,
) => {
const { register, index, descendants } = useDismissableDescendant({
disableOutsidePointerEvents,
disabled: !enabled,
});
/**
* `node` will be set to the ref of the component or nested component
* Ex: If
* ```
* <DismissableLayer asChild>
* <Popover />
* </DismissableLayer>
* ```
* `node` will in this case be the Popover-element.
* We use State her and not ref since we want to trigger a rerender when the node changes.
*/
const [node, setNode] = useState<HTMLDivElement | null>(null);
const mergedRefs = useMergeRefs(setNode, register, ref);
/**
* In some cases the `node.ownerDocument` can differ from global document.
* This can happend when portaling elements or using web-components
*/
const ownerDocument = node?.ownerDocument ?? globalThis?.document;
const hasInteractedOutsideRef = useRef(false);
const hasPointerDownOutsideRef = useRef(false);
const pointerEnabled = useMemo(() => {
let lastIndex = -1;
const descendantNodes = descendants.enabledValues();
descendantNodes.forEach((obj, _index) => {
if (obj.disableOutsidePointerEvents) {
lastIndex = _index;
}
});
return {
/**
* Makes sure we stop events at the highest layer with pointer events disabled.
* If not checked, we risk closing every layer when clicking outside the layer.
*/
isPointerEventsEnabled: index >= lastIndex,
/**
* If we find a node with `disableOutsidePointerEvents` we want to disable pointer events on the body.
*/
isBodyPointerEventsDisabled: lastIndex > -1,
};
}, [descendants, index]);
/**
* We want to prevent the Layer from closing when the trigger, anchor element, or its child elements are interacted with.
*
* To achieve this, we check if the event target is the trigger, anchor or a child. If it is, we prevent default event behavior.
*
* The `pointerDownOutside` and `focusOutside` handlers already check if the event target is within the DismissableLayer (`node`).
* However, since we don't add a `tabIndex` to the Popover/Tooltip, the `focusOutside` handler doesn't correctly handle focus events.
* Therefore, we also need to check that neither the trigger (`anchor`) nor the DismissableLayer (`dismissable`) are the event targets.
*/
function handleOutsideEvent(
event: CustomFocusEvent | CustomPointerDownEvent,
) {
if ((!safeZone?.anchor && !safeZone?.dismissable) || !enabled) {
return;
}
if (!event.defaultPrevented) {
hasInteractedOutsideRef.current = true;
if (event.detail.originalEvent.type === "pointerdown") {
hasPointerDownOutsideRef.current = true;
}
}
const target = event.target as HTMLElement;
/**
* pointerdown-events works as expected, but focus-events does not.
* For focus-event we need to also check `safeZone.dismissable` (the Popover/Tooltip itself) since it does not have a tabIndex.
*/
if (event.detail.originalEvent.type === "pointerdown") {
const targetIsTrigger =
safeZone?.anchor?.contains(target) || target === safeZone?.anchor;
targetIsTrigger && event.preventDefault();
} else {
const targetIsNotTrigger =
target instanceof HTMLElement &&
![safeZone?.anchor, safeZone?.dismissable].some(
(element) => element?.contains(target as Node),
) &&
!target.contains(safeZone?.dismissable ?? null);
!targetIsNotTrigger && event.preventDefault();
}
/**
* In Safari, if the trigger element is inside a container with tabIndex={0}, a click on the trigger
* will first fire a 'pointerdownoutside' event on the trigger itself. However, it will then fire a
* 'focusoutside' event on the container.
*
* To handle this, we ignore any 'focusoutside' events if a 'pointerdownoutside' event has already occurred.
* 'pointerdownoutside' event is sufficient to indicate interaction outside the DismissableLayer.
*/
if (
event.detail.originalEvent.type === "focusin" &&
hasPointerDownOutsideRef.current
) {
event.preventDefault();
}
hasPointerDownOutsideRef.current = false;
hasInteractedOutsideRef.current = false;
}
const pointerDownOutside = usePointerDownOutside((event) => {
if (!pointerEnabled.isPointerEventsEnabled || !enabled) {
return;
}
/**
* We call these before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault based certain cases.
*/
onPointerDownOutside?.(event);
onInteractOutside?.(event);
/**
* Add safeZone to prevent closing when interacting with trigger/anchor or its children.
*/
safeZone && handleOutsideEvent(event);
/**
* Both `onPointerDownOutside` and `onInteractOutside` are able to preventDefault the event, thus stopping call for `onDismiss`.
*/
if (!event.defaultPrevented && onDismiss) {
onDismiss();
}
}, ownerDocument);
const focusOutside = useFocusOutside((event) => {
if (!enabled) {
return;
}
/**
* We call these before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault based certain cases.
*/
onFocusOutside?.(event);
onInteractOutside?.(event);
/**
* Add safeZone to prevent closing when interacting with trigger/anchor or its children.
*/
safeZone && handleOutsideEvent(event);
/**
* Both `onFocusOutside` and `onInteractOutside` are able to preventDefault the event, thus stopping call for `onDismiss`.
*/
if (!event.defaultPrevented && onDismiss) {
onDismiss();
}
}, ownerDocument);
useEscapeKeydown((event) => {
if (!enabled) {
return;
}
/**
* The deepest nested element will always be last in the descendants list.
* This allows us to only close the highest layer when pressing escape.
*
* In some cases a layer might still exist, but be disabled. We want to ignore these layers.
*/
const isHighestLayer = index === descendants.enabledCount() - 1;
if (!isHighestLayer) {
return;
}
/**
* We call this before letting `handleOutsideEvent` do its checks to give consumer a chance to preventDefault based certain cases.
*/
onEscapeKeyDown?.(event);
/**
* `onEscapeKeyDown` is able to preventDefault the event, thus stopping call for `onDismiss`.
* We want to `preventDefault` the escape-event to avoid sideeffect from other elements on screen
*/
if (!event.defaultPrevented && onDismiss) {
event.preventDefault();
onDismiss();
}
}, ownerDocument);
/**
* If `disableOutsidePointerEvents` is true,
* we want to disable pointer events on the body when the first layer is opened.
*/
useEffect(() => {
if (!node || !disableOutsidePointerEvents || index !== 0 || !enabled) {
return;
}
originalBodyPointerEvents = ownerDocument.body.style.pointerEvents;
ownerDocument.body.style.pointerEvents = "none";
return () => {
ownerDocument.body.style.pointerEvents = originalBodyPointerEvents;
};
}, [node, ownerDocument, disableOutsidePointerEvents, index, enabled]);
const Comp = asChild ? Slot : "div";
return (
<Comp
ref={mergedRefs}
{...rest}
onFocusCapture={focusOutside.onFocusCapture}
onBlurCapture={focusOutside.onBlurCapture}
onPointerDownCapture={pointerDownOutside.onPointerDownCapture}
style={{
pointerEvents: pointerEnabled.isBodyPointerEventsDisabled
? pointerEnabled.isPointerEventsEnabled
? "auto"
: "none"
: undefined,
...rest.style,
}}
>
{children}
</Comp>
);
},
);
export { DismissableLayer, type DismissableLayerProps };
@@ -0,0 +1,77 @@
import ReactDOM from "react-dom";
type CustomFocusEvent = CustomEvent<{ originalEvent: FocusEvent }>;
type CustomPointerDownEvent = CustomEvent<{
originalEvent: PointerEvent;
}>;
export { CustomFocusEvent, CustomPointerDownEvent };
export const CUSTOM_EVENTS = {
FOCUS_OUTSIDE: "AKSEL_FOCUS_OUTSIDE",
POINTER_DOWN_OUTSIDE: "AKSEL_POINTER_DOWN_OUTSIDE",
};
/**
* Use of `discrete` flushes custom event dispatch. This is to mimic the behavior React has for `discrete` events.
* https://github.com/facebook/react/blob/a8a4742f1c54493df00da648a3f9d26e3db9c8b5/packages/react-dom/src/events/ReactDOMEventListener.js#L318
*
* React batches *all* event handlers since version 18, this introduces certain considerations when using custom event types.
*
* Internally, React prioritises events in the following order:
* - discrete
* - continuous
* - default
*
* `discrete` is an important distinction as updates within these events are applied immediately.
* React however, is not able to infer the priority of custom event types due to how they are detected internally.
* Because of this, it's possible for updates from custom events to be unexpectedly batched when
* dispatched by another `discrete` event.
*
* In order to ensure that updates from custom events are applied predictably, we need to manually flush the batch.
* This utility should be used when dispatching a custom event from within another `discrete` event, this utility
* is not nessesary when dispatching known event types, or if dispatching a custom type inside a non-discrete event.
* For example:
*
* dispatching a known click 👎
* target.dispatchEvent(new Event(‘click’))
*
* dispatching a custom type within a non-discrete event 👎
* onScroll={(event) => event.target.dispatchEvent(new CustomEvent(‘customType’))}
*
* dispatching a custom type within a `discrete` event 👍
* onPointerDown={(event) => dispatchDiscreteCustomEvent(event.target, new CustomEvent(‘customType’))}
*
* Note: though React classifies `focus`, `focusin` and `focusout` events as `discrete`, it's not recommended to use
* this utility with them. This is because it's possible for those handlers to be called implicitly during render
* e.g. when focus is within a component as it is unmounted, or when managing focus on mount.
*/
export function dispatchCustomEvent<
E extends CustomEvent,
OriginalEvent extends Event,
>(
name: string,
handler: ((event: E) => void) | undefined,
detail: { originalEvent: OriginalEvent } & (E extends CustomEvent<infer D>
? D
: never),
{ discrete }: { discrete: boolean } = { discrete: false },
) {
if (!handler) {
return;
}
const target = detail.originalEvent.target;
const event = new CustomEvent(name, {
bubbles: false,
cancelable: true,
detail,
});
target.addEventListener(name, handler as EventListener, { once: true });
if (discrete && target) {
ReactDOM.flushSync(() => target.dispatchEvent(event));
} else {
target.dispatchEvent(event);
}
}
@@ -0,0 +1,21 @@
import { useEffect } from "react";
import { useCallbackRef } from "../../../util/hooks";
export function useEscapeKeydown(
callback?: (event: KeyboardEvent) => void,
ownerDocument: Document = globalThis?.document,
) {
const onEscapeKeyDown = useCallbackRef(callback);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === "Escape") {
onEscapeKeyDown(event);
}
};
ownerDocument.addEventListener("keydown", handleKeyDown, true);
return () =>
ownerDocument.removeEventListener("keydown", handleKeyDown, true);
}, [onEscapeKeyDown, ownerDocument]);
}
@@ -0,0 +1,52 @@
import { useEffect, useRef } from "react";
import { useCallbackRef } from "../../../util/hooks";
import {
CUSTOM_EVENTS,
CustomFocusEvent,
dispatchCustomEvent,
} from "./dispatchCustomEvent";
/**
* Tracks focus outside a React subtree. Returns props for the subtree root.
*/
export function useFocusOutside(
callback?: (event: CustomFocusEvent) => void,
ownerDocument: Document = globalThis?.document,
) {
const handleFocusOutside = useCallbackRef(callback) as EventListener;
const isFocusInsideReactTreeRef = useRef(false);
useEffect(() => {
const handleFocus = (event: FocusEvent) => {
if (event.target && !isFocusInsideReactTreeRef.current) {
const eventDetail = { originalEvent: event };
/**
* The `DismisableLayer`-API is based on the ability to stop events from propagating and in the end calling `onDismiss`
* if `useFocusOutside`-callback runs `event.preventDefault()`.
*
* Because of the `focusin`-event not being cancelable,
* we need to use a custom event to ensure that the event is cancelable.
* https://developer.mozilla.org/en-US/docs/Web/API/Element/focusin_event
* > The focusin event is not cancelable.
*/
dispatchCustomEvent(
CUSTOM_EVENTS.FOCUS_OUTSIDE,
handleFocusOutside,
eventDetail,
);
}
};
ownerDocument.addEventListener("focusin", handleFocus);
return () => ownerDocument.removeEventListener("focusin", handleFocus);
}, [ownerDocument, handleFocusOutside]);
/**
* By directly setting isFocusInsideReactTreeRef on focus-events at the root of the "dismissable" element,
* we can eliminate the need for DOM traversal to verify if the focused element is within the react tree.
*/
return {
onFocusCapture: () => (isFocusInsideReactTreeRef.current = true),
onBlurCapture: () => (isFocusInsideReactTreeRef.current = false),
};
}
Loaded 100 of 417 files, more files were not shown because too many files have changed in this diff. Show more