Compare commits

..
Author SHA1 Message Date
github-actions[bot]andgithub-actions[bot] c0f334061b Version Packages (#2482)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-16 14:01:11 +01:00
Ken 79bddd9caa Primitives er nå mer spesifikk (#2469)
* 🐛 Mer spesifikke primitives

* 🔥 Fjernet story

* 📝 changeset

* 🎨 Oppdatert mappings
2023-11-16 11:41:31 +01:00
Ken 4a68409898 🎨 Oppdatert overridablecomponent (#2474)
* 🎨 Oppdatert overridablecomponent

* 📝 Kommentarer
2023-11-16 11:30:15 +01:00
Ken 786bff67ab 🔥 Fjernet assignees fra issue-templates 2023-11-15 13:14:59 +01:00
Ken 8a17caea6c 🐛 Fjernet TableOfContents fra bloggsider 2023-11-14 21:06:33 +01:00
Ken 3f43d0bf3b 🐛 Kjører nå ikke playroom build ved boot 2023-11-14 20:29:29 +01:00
Ken 26541984ac [aksel.nav.no] Sidebar & TableOfContents refactor (#2470)
* 🎨 toc setup

* 🎉 Implementert TOC-generering

* 💄 Nytt design på Toc

* 🎨 forenklet toc-styling

* 🎨 Scroll ved overflow

* 🎨 Bruker ny TOC på aksel

* 🐛 Bedre word-wrap

* 💄 Shadow on toc

* 🐛 Fikset fokusmarkering

* :trukket ut generateSidebar funksjon

* 🧪 tester for sidebar-generering

* 💄 Bedre design på toc

* 🎨 Mindre refactor av TOC

* 🎉 Ny sidebar

* 🎨 Oppdatert bruk av sidebar

* 🐛 Oppdatert sidebar-story

* 💄 Justert sidebar

* 💄 Indent på toc

* 💄 Hide scrollbar on toc

* 🎨 scrollfade toc

* ♻️ Flyttet scrollfade ut i egen komponent

* 💄 Smoother scroll-fade

* ⚡ Færre state-updates i toc

* 🎨 data atrrb for css

* 🎨 data-attrb i sidebar

* ⚡ Mindre css dom updates

* 🎨 Fjerner emojies fra toc

* 🎨 automatisk scroll-top

* 🎨 Next-link for sidebar

* 🎨 Toggle toc-button onClick

* 🐛 Fikset scroll-padding

* 🐛 Fikset fokushåndering av toc

* 🎨 Mindre tydelig fade for firefox

* 🔥 Fjernet console.log

* 🐛 Fikset recalculation ved page-change

* 🐛 Reversert til next 13.2.1
2023-11-14 20:10:42 +01:00
Julian NymarkandKen 10303602aa playroom (#2413)
* 🚧 playroom

testing it out, needs yarn run playroom:build before yarn dev (in aksel.nav.no)

* 🚧 width and height for iframe

* 🚚 move playroom into its own module

* 🚧

* 🐛 Fikset lasting av ts-filer

* build playroom & copy dist to public/

* 🚧 yarn maintenance

* 🚧 build the playroom before copy

* ✨ now examples might work in the sandbox? maybe 90% of them?!

* ✨ + 🔥 + ♻️

- remove route for /sandbox, in public/ means it gets served regardless?
- for each code example:
   - use jscodeshift to remove imports
   - also transpile to ESNext
- bump jscodeshift to 0.15.1 (for all uses)
- the sandbox button now sends you to /sandbox/index.html

* :refactor: j -> parse

* add non-ideal string replacer to make Examples work in Playroom

* ♻️ bring back CodeSandox link as fallback

* 🚚 Sandbox -> parts/Sandbox

* 🚚🚚 Sandbox -> parts/Sandbox

* seemingly working smoke tests for all sandbox examples

* ♻️ shuffle example code to work well with playroom

* ♻️ shuffle example code to work in sandbox (again)

* update sanity eksempler, do base64 url code work there

* PR comment fixes

* 📝 Sanity-schema for sandbox-snippets

* 🚚 generic cubeIcon -> codesandboxLogo

* 🔥 unused prop

* ♻️ do not run all tests in smoketest

* 🐛 pr comment fixes

* PR comment fixes, remove unneeded domcontentloaded await in tests

* ♻️ restructure so <Example> comes first

* 🐛 sandbox tests now for templates and examples

* 🐛 Fikset imports

* 🐛 disable some sandbox tests (for templates)

* 🐛 forgot to disable one template test

---------

Co-authored-by: Ken <ken.aleksander@gmail.com>
2023-11-13 15:12:52 +01:00
Ken 42254543d8 [aksel.nav.no] ♻️ Ryddet opp i imports, kodestruktur og ubrukte configs/moduler (#2465)
* 🚚 Oppdatert layout exports

* ⚡ Nextjs-config kan nå bruke webpack-builder

* 🐛 reverserte til gammel image-config i next.config

* 🚚 Oppdaterte assets exports

* 🚚 Fjernet barrel-exports av komponenter

* 🚚 Flyttet types

* 🚚 Flyttet sanityblock og withpreview

* 🚚 Flyttet styles inn i components

* ♻️ Forenklet jest-config

* 🚚 Flyttet logger

* ⚡ Kjører nå ikke middleware på alle side

* ♻️ next config analyze setup

* 🔥 fjernet getdocumentstmp

* 🔥 Fjernet getAkselDocuments

* 🎨 Forenklet imagebuilder

* ♻️ Fjernet ubrukte tailwind configs

* 🎨 Grupperer changelog med minor

* 🔥 Fjernet duplicate changelog

* 🎨 Ny ts alias

* ♻️ Refactor av utils dir

* ♻️ Header er nå default export

* ♻️ Byttet ut deprecated ingress med bodylong

* ♻️ Oppdatert assets export/imports

* ♻️ Oppdatert website/sanity imports

* 🐛 henter ut logger i dockerfile

* ⚡ Enda mer filtrering i middleware

* 🐛 Riktig generering av sider for e2e-sider

* 🐛 Fikset import i codeexamples update

* 🐛 la til mønster og maler i hamburgermeny
2023-11-10 18:44:42 +01:00
Ken 81c84e4817 📝 Liten M i Maler 2023-11-10 14:51:10 +01:00
Ken b441175028 📝 Oppdatert readme 2023-11-10 14:00:21 +01:00
dependabot[bot] 75362a0dc1 ⬆️ Bump axios from 1.3.6 to 1.6.0 in /@navikt/aksel (#2467)
Bumps [axios](https://github.com/axios/axios) from 1.3.6 to 1.6.0.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.3.6...v1.6.0)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-10 11:12:38 +01:00
Ken d8732ffa0e 🎉 Lagt til Mønster og Maler i header 2023-11-10 10:58:16 +01:00
github-actions[bot]andgithub-actions[bot] 0ea9155085 Version Packages (#2466)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-10 10:40:12 +01:00
Ken 92b9687ed1 Håndterer target-detection bedre i ExpandableRow (#2461)
* 🎨 Håndterer nå target-detection bedre

* 🎨 Forenklet if-tester, tester 2x nested elementer
2023-11-10 10:36:34 +01:00
Ken 1c022b14c9 🎨 Lagt til closeOnBackdropClick for mobilmeny på Aksel 2023-11-08 21:16:17 +01:00
Ken abdeb616d2 🎨 Fikste søk + codeexamples for templates (#2462) 2023-11-08 20:41:47 +01:00
github-actions[bot]andgithub-actions[bot] 37165e9f38 Version Packages (#2459)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-08 16:24:10 +01:00
KenandHalvor Haugan d19920ca78 [aksel.nav.no] 404 template (#2455)
* ✨ Initial template

* 💄 Automatisk skalert svg

* 🎉 Maler v1

* 📝 Bedre endringslogg

* Update aksel.nav.no/website/pages/templates/404/complete.tsx

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

* Update aksel.nav.no/website/pages/templates/404/complete.tsx

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

* 📝 Bedre kommentarer

* 📝 rename stories

* ♻️ Bruker nå page

* 🎨 Oppdatert mal med endringer

* 🎨 Bruker 404-mal på Aksel

* :trucK: Flyttet data-atributt

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2023-11-08 15:23:15 +01:00
Ken 60d3481300 [Primitives] Page-primitive (#2456)
* ✨ Frame-component

* 🏷️ rename to page

* 🎨 Belowfold

* 🎨 Gutters

* 🎨 Fjernet content width

* 🎨 Page-stories

* 📝 Config

* 📝 changeset

* 📝 Bedre props

* ♻️ Ryddet opp i CSS og prop-navn

* ♻️ Tokens for padding-verdier

* ♻️ Bedre background example

* ♻️ Bedre contentblockpadding example

* ♻️ Bedre gutters example

* 🔥 Fjernet dekoratør-story fra chromatic

* 🐛 Riktig page-padding

* 💄 Justert maksbredder

* 📝 kommentarer

* 📝 JSDoc

* 📝 jsdoc update

* 📝 Kommentarer
2023-11-08 12:14:35 +01:00
Halvor Haugan f486937439 ⬆️ Storybook 7.5.3 (#2458) 2023-11-08 09:12:00 +01:00
Halvor Haugan c62058abc1 🚨 eslint: sjekk avhengighetssykluser når i CI (#2430) 2023-11-07 14:29:51 +01:00
Halvor Haugan 84d9b8260f Fiks typo i timeline eksempel desc 2023-11-07 13:56:58 +01:00
Halvor Haugan c108063dcb Justering av eksempler på Timeline (#2454) 2023-11-06 08:55:31 +01:00
Ken 2a8533ad56 [aksel.nav.no] Templates vises nå riktig på mønster/mal-sider (#2450)
* 🐛 Fikset visning av templates

* 🐛 autoGenerateArrayKeys fungerte ikke for nested arrays

* 🐛 Fikset tester
2023-11-03 12:18:38 +01:00
KenandAnders Olsen Sandvik 9b990a2178 [aksel.nav.no] Legg til RSS feed på produktbloggen (#2439) (#2453)
* [aksel.nav.no] Legg til RSS feed på produktbloggen

* Adjust generateRssFeed to accept a path parameter

* 🎉 Oppdatert generering av rss

---------

Co-authored-by: Anders Olsen Sandvik <github@mlsv.no>
2023-11-03 10:26:35 +01:00
github-actions[bot]andgithub-actions[bot] 5fec1f24f4 Version Packages (#2452)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-02 22:03:10 +01:00
Halvor Haugan c27c1cfe26 🐛 Datepicker lukker ikke lenger modalen den er i (#2451) 2023-11-02 21:59:06 +01:00
Ken 5a649d2282 🐛 Kunne ikke se preview av mønster forside (#2449) 2023-11-02 15:11:34 +01:00
Ken baac9cc251 [aksel.nav.no] Sidemaler for Mønster og maler (#2438)
* 🎉 Sanity-schema for mønster og maler

* 🎨 La til mønster og maler i sanity structure

* 🎨 Støtte for templates i søk

* 🎨 Sidemaler for Mønster og Maler

* 🎨 Riktig håndtering av breadcrumbs

* 🐛 Fikset story

* 🐛 Fikset search-indeksering

* 🐛 Fikset search-test

* 🐛 Fikset typo

* [aksel.nav.no] Refaktorert synking av eksempler/templates mellom kode og sanity (#2442)

* 🎨 Template pages

* 🔥 Fjernet templates fra logging

* ♻️ Refaktorert example-generering

* ✅ extractArgs tests

* ✅ filter-code tester

* ✅ Ny code-parsing tester

* 🚚 Flyttet filer

* 🎨 Oppdaterer sanity remote

* 🎨 sanity schema for templates

* 🎨 Schema for metadata in templates

* 🎨 henter nå ut metadata fra templates

* 🎨 Flyttet createId funksjon

* 🚚 rename folder

* 🎨 schema for templates i sanity

* 🐛 Fjernet dryrun fra oppdatering

* 🎨 Make sure to delete old documents after updating

* 🐛 Riktige typer for version-schema

* [aksel.nav.no] Integrert mønster og maler templates inn på aksel (#2447)

* 🎨 Template pages

* 🔥 Fjernet templates fra logging

* ♻️ Refaktorert example-generering

* ✅ extractArgs tests

* ✅ filter-code tester

* ✅ Ny code-parsing tester

* 🚚 Flyttet filer

* 🎨 Oppdaterer sanity remote

* 🎨 sanity schema for templates

* 🎨 Schema for metadata in templates

* 🎨 henter nå ut metadata fra templates

* 🎨 Flyttet createId funksjon

* 🚚 rename folder

* 🎨 schema for templates i sanity

* 🐛 Fjernet dryrun fra oppdatering

* 🎨 Make sure to delete old documents after updating

* 🐛 Riktige typer for version-schema

* 🎨 Kan nå vise eksempler på side

* 🎨 Automatisk visning av changelog

* 📝 typo

* 📝 Rename fra changelog til Endringer

* 🐛 Fikset story-typer

* 🎨 Forsideblokk for templates

* 🐛 feil riktekst ble brukt
2023-11-02 11:47:57 +01:00
Sjur d924d523e4 Update in-the-wild.yml (#2448)
Tok bort "assignees"
2023-11-02 10:38:51 +01:00
Sjur fc5a77081d Create in-the-wild.yml (#2445)
Issue template for å ta imot eksempler fra folk
2023-11-02 10:01:49 +01:00
github-actions[bot]andgithub-actions[bot] 1f3477a3e9 Version Packages (#2446)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-02 09:55:38 +01:00
Halvor Haugan 925450cb73 💄 Modal: Bedre håndtering av mobiler i landskapsmodus (#2444) 2023-11-02 09:30:19 +01:00
github-actions[bot]andgithub-actions[bot] 534a6a3b40 Version Packages (#2443)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-11-01 15:46:41 +01:00
51eee7f011 [Datepicker/Modal] Datepicker bruker nå Modal på mobil og for nested Modal (#2419)
* 🎨 Initial Modal-update

* 🐛 close(open handling for Modal

* 🔥 Fjernet openOnFocus

* 🔥 Fjernet outsideclick og useEscape

* 🐛 monthpicker onClose forwarding

* 🎨 Flyttet DateInput til egen parts-mappe

* 🏷️ DateInput imports

* 🚚 Flyttet DateWrapper ut til egen komponent

* 🎨 Monthpicker bruker dateWrapper

* 💄 Oppdatert styling for datepicker

* 📝 Oppdatert eksempler

* 📝 changeset

* 🐛 Fikset imports i date-hooks

* ♻️ Bedre context-bruk i Modal.tsx

* 📝 Bedre tekster for deprecation

* 🐛 Støtter nå jsdom

* 🐛 Fikset polyfill av datepicker

* 🎨 closeOnBackdropClick i datepicker

* 🏷️ 🐛 Fikset outsideClick boundary + fjernet bubbleEscape-prop

* 🔥 Fjernet console.log

* 🔥 Fjernet bubbleEscape-prop

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

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>

* 🎨 Bedre className for nested modal

* 🐛 Bedre check om popover er i datepicker

* 🔥 Fjernet unødvendig if rundt window-check

* 🎨 Bedre fallback for useMedia-hook

* 🎨 bedre navn i useMedia

* 📝 Datepicker-stories for size har preset-values

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

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

* 🎨 DateContext er nå default null hvis ikke satt

* Update @navikt/core/react/src/date/parts/DateWrapper.tsx

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

* Update @navikt/core/react/src/date/parts/DateWrapper.tsx

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

* Update @navikt/core/react/src/date/parts/DateWrapper.tsx

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

* Update @navikt/core/react/src/date/utils/labels.ts

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

* 🐛 Fikset small-size padding

* 🔥 Fjernet error-demo

* fokusgreier

* 🎨 bedre onClose-handling av modal

* 🐛 Håndterer focus ved cancel og select nå

* 🐛 Feil i typer for dateinput

* 🐛 setAnchorRef var optional

* 🐛 null-test

* 🎨 Fjernet jsdom sjekk tester for useMedia

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2023-11-01 15:38:57 +01:00
Vegard Haugstvedt 909ae6aed1 Add new useVirtualFocus hook, for use in Combobox (#2394)
* Add new useVirtualFocus hook, for use in Combobox. Might be useful for other components later, but will scope it for Combobox at first.

* Create filteredOptionsUtils, which has utiltity functions for creating IDs and for text lookups in lists.

* Get rid of data-value, and use filteredOptionsMap to look up the value of the currently (virtually) focused element instead

This allows use of e.g. numbers as options, which data-value didn't.

* Using useState instead of useRef to handle filteredOptionsRef

* Added changeset

* Force option id to be lower case, to avoid different casing from being interpreted as being different from the existing options

* Fix bug where it was not possible to select multiple custom (new) options in a multiselect

selectedOptionsContext is not available inside customOptionsContext, so need to take isMultiSelect from props instead

* Switch to use toLocalLowerCase and remove uneccessary use of string literal

* type mismatch

* Clean up types - reuse from ComboboxProps type definition, and fix resulting type mismatches

* Rename filteredOptionsRef variable returned from filteredOptionsContext to reflect that it is now actually a setState-function

* Expose virtualFocus as an object through filteredOptionsContext, instead of each separate method.

Done to remove need to wrap moveFocusUp and moveFocusDown in order to toggle isListOpen
2023-11-01 12:02:57 +01:00
Lars Eirik Korsgaard Hansen cd3859a91e 📝 add aria roles example to Alert (#2440) 2023-11-01 09:16:42 +01:00
Ken 338f14e7ac [aksel.nav.no] Refaktorert ErrorBoundary-bruk for fast-refresh (#2433)
* ♻️ errorboundary refactor

* ♻️ Converted to non-anonymous function errorboundary

* ♻️ Fikset ErrorBoundary Full-refresh

* [aksel.nav.no] ♻️ Component-structure (#2435)

* 🔥 Fjernet UUFeedback-modul

* 🚚 Flyttet filer i egne mapper

* 🚚 Oppdatert struktur for sanity-moduler

* 🚚 Oppdatert akselcube exports

* 🔥 Fjernet Feedback-modul

* 🚚 Rename SuggestionBlock

* 🚚 Flyttet relatert innhold

* 🐛 Fikset suggestionblock imports

* 🎨 Oppdatert bruk av ShowMore
2023-10-30 15:42:35 +01:00
Halvor Haugan aed1d082c3 🐛 Legg på onClose på Popover-eksempler (#2436) 2023-10-30 12:36:47 +01:00
Ken 31e9226b55 [Aksel.nav.no] Bruker alpha ShowMore lokalt på nettsiden (#2409)
* 📝 Story for TokenTable

* ✨ Tester lokal alpha-showmore på Aksel

* 🎨 Showmore brukes nå bare når det er over 10 tokens i listen

* 📝 yarn lock
2023-10-30 12:10:16 +01:00
653 changed files with 12017 additions and 7987 deletions

No files matched your search

+22
View File
@@ -0,0 +1,22 @@
name: Eksempel på bruk
description: Send et eksempel på ting fra Aksel som er brukt i det fri (tokens, komponenter, maler, etc)
title: "[Eksempel på bruk]: "
labels: ["dokumentasjon 📖"]
body:
- type: markdown
attributes:
value: |
Takk for at du bidrar til Aksel!
- type: input
id: title
attributes:
label: Lenke til side
description: Lenke til siden hvor eksemplet er
placeholder:
validations:
required: true
- type: textarea
id: examples
attributes:
label: Forklaring
description: Forklar om det er noe man må gjøre for å se eksemplet, eller hvor på siden det ligger
-2
View File
@@ -2,8 +2,6 @@ name: Ny komponent
description: Send et ønske om en eller flere nye komponenter
title: "[Ny komponent]: "
labels: ["nytt ✨", "forespørsel 🥰", "komponenter 🧩"]
assignees:
- KenAJoh
body:
- type: markdown
attributes:
-2
View File
@@ -2,8 +2,6 @@ name: Nytt ikon
description: Send et ønske om et eller flere nye ikoner
title: "[Nytt ikon]: "
labels: ["nytt ✨", "ikoner 🖼", "forespørsel 🥰"]
assignees:
- KenAJoh
body:
- type: markdown
attributes:
@@ -2,8 +2,6 @@ name: Innspill til komponent
description: Send inn innspill til komponent
title: "[Innspill til komponent]: "
labels: ["forespørsel 🥰", "komponenter 🧩", "nytt ✨", "beta 🧪"]
assignees:
- KenAJoh
body:
- type: markdown
attributes:
-2
View File
@@ -2,8 +2,6 @@ name: Innspill til ikon
description: Send inn innspill til ikon
title: "[Tilbakemelding på ikon]: "
labels: ["forespørsel 🥰", "ikoner 🖼️"]
assignees:
- KenAJoh
body:
- type: markdown
attributes:
+2 -1
View File
@@ -151,4 +151,5 @@ playwright-report/
backup-*.tar.gz
sitemap-urls.json
*.tar.gz
searchindex.json
searchindex.json
aksel.nav.no/website/public/rss
+12
View File
@@ -1,5 +1,17 @@
# @navikt/aksel-icons
## 5.10.2
## 5.10.1
## 5.10.0
## 5.9.2
## 5.9.1
## 5.9.0
## 5.8.0
## 5.7.6
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "5.8.0",
"version": "5.10.2",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
+12
View File
@@ -1,5 +1,17 @@
# @navikt/aksel-stylelint
## 5.10.2
## 5.10.1
## 5.10.0
## 5.9.2
## 5.9.1
## 5.9.0
## 5.8.0
## 5.7.6
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "5.8.0",
"version": "5.10.2",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
"repository": {
@@ -35,8 +35,8 @@
"dev": "yarn watch:lint"
},
"devDependencies": {
"@navikt/ds-css": "^5.8.0",
"@navikt/ds-tokens": "^5.8.0",
"@navikt/ds-css": "^5.10.2",
"@navikt/ds-tokens": "^5.10.2",
"@types/jest": "^29.0.0",
"concurrently": "7.2.1",
"copyfiles": "2.4.1",
+42
View File
@@ -1,5 +1,47 @@
# @navikt/aksel
## 5.10.2
### Patch Changes
- Updated dependencies [[`79bddd9ca`](https://github.com/navikt/aksel/commit/79bddd9caa9467b60e1e0c1c15b16f76dccbc304)]:
- @navikt/ds-css@5.10.2
## 5.10.1
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@5.10.1
## 5.10.0
### Patch Changes
- Updated dependencies [[`60d348130`](https://github.com/navikt/aksel/commit/60d34813009377a1a7d84bed6627c60524849957)]:
- @navikt/ds-css@5.10.0
## 5.9.2
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@5.9.2
## 5.9.1
### Patch Changes
- Updated dependencies [[`925450cb7`](https://github.com/navikt/aksel/commit/925450cb73cd792cd3b6ad3ebf9f38ba022ee4df)]:
- @navikt/ds-css@5.9.1
## 5.9.0
### Patch Changes
- Updated dependencies [[`51eee7f01`](https://github.com/navikt/aksel/commit/51eee7f011aa016973bb952313cf65a1c55aa382), [`51eee7f01`](https://github.com/navikt/aksel/commit/51eee7f011aa016973bb952313cf65a1c55aa382), [`51eee7f01`](https://github.com/navikt/aksel/commit/51eee7f011aa016973bb952313cf65a1c55aa382), [`51eee7f01`](https://github.com/navikt/aksel/commit/51eee7f011aa016973bb952313cf65a1c55aa382)]:
- @navikt/ds-css@5.9.0
## 5.8.0
### Patch Changes
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "5.8.0",
"version": "5.10.2",
"description": "Aksel command line interface. Handles css-imports, codemods and more",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -29,8 +29,8 @@
},
"homepage": "https://aksel.nav.no/grunnleggende/kode/kommandolinje",
"dependencies": {
"@navikt/ds-css": "5.8.0",
"axios": "1.3.6",
"@navikt/ds-css": "5.10.2",
"axios": "1.6.0",
"chalk": "4.1.0",
"clipboardy": "^2.3.0",
"commander": "10.0.1",
@@ -38,7 +38,7 @@
"fast-glob": "3.2.11",
"figlet": "1.6.0",
"is-git-clean": "1.1.0",
"jscodeshift": "0.13.1",
"jscodeshift": "^0.15.1",
"jscodeshift-add-imports": "1.0.10",
"lodash": "4.17.21",
"react-scanner": "^1.1.0"
+34
View File
@@ -1,5 +1,39 @@
# @navikt/ds-css
## 5.10.2
### Patch Changes
- Primtives: CSS-import for primitives er nå mer spesifikk ([#2469](https://github.com/navikt/aksel/pull/2469))
## 5.10.1
## 5.10.0
### Minor Changes
- Primitives: Ny komponent `Page` og `Page.Block` ([#2456](https://github.com/navikt/aksel/pull/2456))
## 5.9.2
## 5.9.1
### Patch Changes
- :lipstick: Modal: Bedre håndtering av mobiler i landskapsmodus ([#2444](https://github.com/navikt/aksel/pull/2444))
## 5.9.0
### Minor Changes
- Datepicker/Monthpicker: Hvis man bruker komponentene i Modal vil Popover bli erstattet med Modal uansett om man er på desktop eller mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
- Datepicker/Monthpicker/Popover: Fjernet `bubbleEscape`-prop. ([#2419](https://github.com/navikt/aksel/pull/2419))
- useDatepicker/useMonthPicker/useRangedpicker: Fjernet `openOnFocus`-prop, kan nå bare åpnes ved klikk på date-knapp i input. ([#2419](https://github.com/navikt/aksel/pull/2419))
- Datepicker/Monthpicker: Bytter nå automatisk til Modalvisning på mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
## 5.8.0
### Patch Changes
+5 -1
View File
@@ -35,6 +35,10 @@ const StyleMappings = {
component: "Box",
main: primitivesCss,
},
{
component: "Page",
main: primitivesCss,
},
{
component: "HGrid",
main: primitivesCss,
@@ -183,7 +187,7 @@ const StyleMappings = {
{
component: "DatePicker",
main: "date.css",
dependencies: [typoCss, primitivesCss, "button.css", "popover.css"],
dependencies: [typoCss, "button.css", "popover.css", primitivesCss],
},
{
component: "MonthPicker",
+26 -18
View File
@@ -149,18 +149,6 @@
width: fit-content;
}
/* Focus layering */
.navds-date__field-input:focus-visible,
.navds-date__field-button {
z-index: 1;
}
@supports not selector(:focus-visible) {
.navds-date__field-input:focus {
z-index: 1;
}
}
.navds-date .rdp-day_selected,
.navds-monthpicker__month--selected {
color: var(--ac-date-selected-text, var(--a-text-on-action));
@@ -201,11 +189,11 @@
}
.navds-date__field-input {
padding-right: var(--a-spacing-14);
padding-right: var(--a-spacing-12);
}
.navds-form-field--small .navds-date__field-input {
padding-right: var(--a-spacing-10);
padding-right: var(--a-spacing-8);
}
/* Error-handling */
@@ -282,10 +270,6 @@
pointer-events: none;
}
.navds-date__popover:where(.navds-popover) {
border: none;
}
/* Readonly */
.navds-date__field--readonly .navds-date__field-button {
cursor: default;
@@ -310,11 +294,35 @@
margin: 0;
}
.navds-date__modal.navds-date {
padding: 0;
}
.navds-date__modal-body {
display: flex;
flex-direction: column;
align-items: flex-end;
padding: var(--a-spacing-4);
gap: var(--a-spacing-2);
}
.navds-date__modal-body > .navds-date {
padding: 0;
}
.navds-date__popover:where(.navds-popover) {
border: none;
}
@media (min-width: 480px) {
.navds-date {
padding: var(--a-spacing-5) var(--a-spacing-4);
}
.navds-date__modal-body {
padding: var(--a-spacing-6);
}
.navds-date__caption {
gap: var(--a-spacing-2);
}
+1 -1
View File
@@ -1,7 +1,6 @@
@charset "UTF-8";
@import "baseline/index.css";
@import "typography.css";
@import "primitives/index.css";
@import "grid.css";
@import "accordion.css";
@import "alert.css";
@@ -34,3 +33,4 @@
@import "table.css";
@import "tabs.css";
@import "list.css";
@import "primitives/index.css";
+16 -3
View File
@@ -59,12 +59,17 @@
@media (min-width: 480px) {
.navds-modal {
max-width: calc((100% - 6px) - 2em);
max-height: calc((100% - 6px) - 2em);
max-width: calc(100% - 6px - 2em);
}
.navds-modal--autowidth {
max-width: min(700px, calc((100% - 6px) - 2em));
max-width: min(700px, calc(100% - 6px - 2em));
}
}
@media (min-height: 480px) {
.navds-modal {
max-height: calc(100% - 6px - 2em);
}
}
@@ -130,6 +135,14 @@
margin-left: auto;
}
/* When Datepicker is used nested inside a Modal */
.navds-modal--polyfilled .navds-modal--polyfilled.navds-date__nested-modal {
min-width: fit-content;
max-width: 100vw;
max-height: 100vh;
animation: none;
}
@keyframes akselModalFadeIn {
from {
opacity: 0;
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "5.8.0",
"version": "5.10.2",
"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": "^5.8.0",
"@navikt/ds-tokens": "^5.10.2",
"cssnano": "6.0.0",
"fast-glob": "3.2.11",
"lodash": "4.17.21",
+1
View File
@@ -1,5 +1,6 @@
@import "box.css";
@import "bleed.css";
@import "page.css";
@import "hgrid.css";
@import "stack.css";
@import "responsive.css";
+50
View File
@@ -0,0 +1,50 @@
.navds-page {
display: flex;
flex-direction: column;
min-height: 100vh;
min-height: 100lvh;
/* stylelint-disable-next-line csstools/value-no-unknown-custom-properties */
background-color: var(--__ac-page-background);
}
/* Page.Block */
.navds-pageblock {
margin-inline: auto;
width: 100%;
}
.navds-page__content--grow {
flex-grow: 1;
}
.navds-page__content--fullheight {
min-height: 100vh;
min-height: 100lvh;
}
.navds-page__content--padding {
padding-block-end: var(--a-spacing-8);
}
.navds-pageblock--lg {
max-width: 1024px;
}
.navds-pageblock--xl {
max-width: 1280px;
}
.navds-pageblock--2xl {
max-width: 1440px;
}
.navds-pageblock--gutters {
padding-inline: var(--a-spacing-4);
}
@media (min-width: 1024px) {
.navds-pageblock--gutters {
padding-inline: var(--a-spacing-12);
}
}
+70
View File
@@ -1,5 +1,75 @@
# @navikt/ds-react
## 5.10.2
### Patch Changes
- OverridableComponent: Fungerer nå bedre ved bruk av typeof <komponent> ([#2474](https://github.com/navikt/aksel/pull/2474))
- Updated dependencies []:
- @navikt/ds-tokens@5.10.2
- @navikt/aksel-icons@5.10.2
## 5.10.1
### Patch Changes
- Table: Håndterer nå sjekk for interaktive elementer i ExpandableRow bedre ([#2461](https://github.com/navikt/aksel/pull/2461))
- Updated dependencies []:
- @navikt/ds-tokens@5.10.1
- @navikt/aksel-icons@5.10.1
## 5.10.0
### Minor Changes
- Primitives: Ny komponent `Page` og `Page.Block` ([#2456](https://github.com/navikt/aksel/pull/2456))
### Patch Changes
- Updated dependencies []:
- @navikt/ds-tokens@5.10.0
- @navikt/aksel-icons@5.10.0
## 5.9.2
### Patch Changes
- :bug: Datepicker lukker ikke lenger modalen den er i ([#2451](https://github.com/navikt/aksel/pull/2451))
- Updated dependencies []:
- @navikt/ds-tokens@5.9.2
- @navikt/aksel-icons@5.9.2
## 5.9.1
### Patch Changes
- Updated dependencies []:
- @navikt/ds-tokens@5.9.1
- @navikt/aksel-icons@5.9.1
## 5.9.0
### Minor Changes
- Datepicker/Monthpicker: Hvis man bruker komponentene i Modal vil Popover bli erstattet med Modal uansett om man er på desktop eller mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
- Datepicker/Monthpicker/Popover: Fjernet `bubbleEscape`-prop. ([#2419](https://github.com/navikt/aksel/pull/2419))
- useDatepicker/useMonthPicker/useRangedpicker: Fjernet `openOnFocus`-prop, kan nå bare åpnes ved klikk på date-knapp i input. ([#2419](https://github.com/navikt/aksel/pull/2419))
- Datepicker/Monthpicker: Bytter nå automatisk til Modalvisning på mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
### Patch Changes
- Added useVirtualFocus hook - used in Combobox for now ([#2394](https://github.com/navikt/aksel/pull/2394))
- Updated dependencies []:
- @navikt/ds-tokens@5.9.0
- @navikt/aksel-icons@5.9.0
## 5.8.0
### Minor Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "5.8.0",
"version": "5.10.2",
"description": "Aksel react-components for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -38,8 +38,8 @@
},
"dependencies": {
"@floating-ui/react": "0.25.4",
"@navikt/aksel-icons": "^5.8.0",
"@navikt/ds-tokens": "^5.8.0",
"@navikt/aksel-icons": "^5.10.2",
"@navikt/ds-tokens": "^5.10.2",
"@radix-ui/react-tabs": "1.0.0",
"@radix-ui/react-toggle-group": "1.0.0",
"clsx": "^1.2.1",
@@ -13,13 +13,13 @@ interface DateContextContextProps {
* Aria-connected ID
*/
ariaId?: string;
/**
* Flag for enabled-check
*/
defined: boolean;
}
export const DateContext = createContext<DateContextContextProps>({
open: false,
onOpen: () => null,
ariaId: undefined,
});
export const DateContext = createContext<DateContextContextProps | null>(null);
export const useDateInputContext = () => {
const context = useContext(DateContext);
@@ -1,11 +1,11 @@
import cl from "clsx";
import isWeekend from "date-fns/isWeekend";
import React, { forwardRef, useRef, useState } from "react";
import React, { forwardRef, useMemo, useRef, useState } from "react";
import { DateRange, DayPicker, isMatch } from "react-day-picker";
import { Popover } from "../../popover";
import { omit, useId } from "../../util";
import { DatePickerInput } from "../DateInput";
import { mergeRefs, omit, useId } from "../../util";
import { DateContext } from "../context";
import { DatePickerInput } from "../parts/DateInput";
import { DateWrapper } from "../parts/DateWrapper";
import { getLocaleFromString, labels } from "../utils";
import DatePickerStandalone from "./DatePickerStandalone";
import Caption from "./parts/Caption";
@@ -79,7 +79,6 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
onClose,
onOpenToggle,
strategy,
bubbleEscape = false,
onWeekNumberClick,
...rest
},
@@ -89,6 +88,7 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
const [open, setOpen] = useState(_open ?? false);
const wrapperRef = useRef<HTMLDivElement | null>(null);
const mergedRef = useMemo(() => mergeRefs([wrapperRef, ref]), [ref]);
const [selectedDates, setSelectedDates] = React.useState<
Date | Date[] | DateRange | undefined
@@ -110,6 +110,44 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
rest?.onSelect?.(newSelected);
};
const DatePickerComponent = (
<DayPicker
locale={getLocaleFromString(locale)}
mode={mode}
onSelect={handleSelect}
selected={selected ?? selectedDates}
components={{
Caption: dropdownCaption ? DropdownCaption : Caption,
Head: TableHead,
HeadRow,
WeekNumber,
Row,
Day: DayButton,
}}
className={cl("navds-date", className)}
classNames={{
vhidden: "navds-sr-only",
}}
disabled={(day) => {
return (disableWeekends && isWeekend(day)) || isMatch(day, disabled);
}}
weekStartsOn={1}
initialFocus={false}
labels={labels as any}
modifiers={{
weekend: (day) => disableWeekends && isWeekend(day),
}}
modifiersClassNames={{
weekend: "rdp-day__weekend",
}}
showWeekNumber={showWeekNumber}
onWeekNumberClick={mode === "multiple" ? onWeekNumberClick : undefined}
fixedWeeks
showOutsideDays
{...omit(rest, ["onSelect"])}
/>
);
return (
<DateContext.Provider
value={{
@@ -119,72 +157,27 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
onOpenToggle?.();
},
ariaId,
defined: true,
}}
>
<div
ref={wrapperRef}
ref={mergedRef}
className={cl("navds-date__wrapper", wrapperClassName)}
>
{children}
{(_open ?? open) && (
<Popover
arrow={false}
anchorEl={wrapperRef.current}
open={_open ?? open}
onClose={() => {
onClose?.() ?? setOpen(false);
}}
placement="bottom-start"
id={ariaId}
role="dialog"
ref={ref}
strategy={strategy}
className="navds-date__popover"
bubbleEscape={bubbleEscape}
flip={false}
>
<DayPicker
locale={getLocaleFromString(locale)}
mode={mode}
onSelect={handleSelect}
selected={selected ?? selectedDates}
components={{
Caption: dropdownCaption ? DropdownCaption : Caption,
Head: TableHead,
HeadRow,
WeekNumber,
Row,
Day: DayButton,
}}
className={cl("navds-date", className)}
classNames={{
vhidden: "navds-sr-only",
}}
disabled={(day) => {
return (
(disableWeekends && isWeekend(day)) ||
isMatch(day, disabled)
);
}}
weekStartsOn={1}
initialFocus={false}
labels={labels as any}
modifiers={{
weekend: (day) => disableWeekends && isWeekend(day),
}}
modifiersClassNames={{
weekend: "rdp-day__weekend",
}}
showWeekNumber={showWeekNumber}
onWeekNumberClick={
mode === "multiple" ? onWeekNumberClick : undefined
}
fixedWeeks
showOutsideDays
{...omit(rest, ["onSelect"])}
/>
</Popover>
)}
<DateWrapper
open={_open ?? open}
anchor={wrapperRef.current}
onClose={() => onClose?.() ?? setOpen(false)}
locale={locale}
variant={mode}
popoverProps={{
id: ariaId,
strategy,
}}
>
{DatePickerComponent}
</DateWrapper>
</div>
</DateContext.Provider>
);
@@ -3,7 +3,7 @@ import { Meta, StoryObj } from "@storybook/react";
import isSameDay from "date-fns/isSameDay";
import React, { useId, useState } from "react";
import { useDatepicker, useRangeDatepicker } from "..";
import { BodyLong, Button, HGrid, HStack, Modal, VStack } from "../..";
import { BodyLong, Button, HGrid, Modal, VStack } from "../..";
import DatePicker, { DatePickerProps } from "./DatePicker";
const disabledDays = [
@@ -18,7 +18,6 @@ export default {
type DefaultStoryProps = DatePickerProps & {
size: "medium" | "small";
openOnFocus: boolean;
inputfield: boolean;
standalone: boolean;
};
@@ -30,13 +29,11 @@ export const Default: StoryObj<DefaultStoryProps> = {
const rangeCtx = useRangeDatepicker({
fromDate: new Date("Aug 23 2020"),
toDate: new Date("Aug 23 2023"),
openOnFocus: props.openOnFocus,
});
const singleCtx = useDatepicker({
fromDate: new Date("Aug 23 2020"),
toDate: new Date("Aug 23 2023"),
openOnFocus: props.openOnFocus,
});
const newProps = {
@@ -111,7 +108,6 @@ export const Default: StoryObj<DefaultStoryProps> = {
disableWeekends: false,
showWeekNumber: false,
mode: "single",
openOnFocus: true,
inputfield: true,
standalone: false,
},
@@ -186,21 +182,6 @@ export const UseRangedDatepicker = () => {
);
};
export const OpenOnFocus = () => {
const { datepickerProps, inputProps } = useDatepicker({
onDateChange: console.log,
openOnFocus: false,
});
return (
<div style={{ display: "flex", gap: "1rem" }}>
<DatePicker {...datepickerProps}>
<DatePicker.Input {...inputProps} label="Velg dato" />
</DatePicker>
</div>
);
};
export const NB = () => (
<DatePicker.Standalone locale="nb" today={new Date("2006-07-01")} />
);
@@ -333,15 +314,32 @@ export const Size = () => {
fromDate: new Date("Aug 23 2019"),
toDate: new Date("Feb 23 2024"),
onDateChange: console.log,
defaultSelected: new Date("Feb 23 2023"),
});
const { datepickerProps: d2, inputProps: i2 } = useDatepicker({
fromDate: new Date("Aug 23 2019"),
toDate: new Date("Feb 23 2024"),
onDateChange: console.log,
defaultSelected: new Date("Feb 23 2023"),
});
return (
<div style={{ display: "flex", gap: "1rem" }}>
<DatePicker {...datepickerProps} dropdownCaption>
<DatePicker.Input size="medium" {...inputProps} label="Velg dato" />
<DatePicker.Input
placeholder="10/10/2023"
size="medium"
{...inputProps}
label="Velg dato"
/>
</DatePicker>
<DatePicker {...datepickerProps} dropdownCaption>
<DatePicker.Input size="small" {...inputProps} label="Velg dato" />
<DatePicker {...d2} dropdownCaption>
<DatePicker.Input
placeholder="10/10/2023"
size="small"
{...i2}
label="Velg dato"
/>
</DatePicker>
</div>
);
@@ -439,35 +437,36 @@ export const ModalDemo = () => {
toDate: new Date("Feb 23 2024"),
onDateChange: console.log,
});
const [open, setOpen] = useState(true);
return (
<Modal open header={{ heading: "Modal-demo" }} width="1024px">
<Modal.Body style={{ position: "relative" }}>
<BodyLong>
Lorem ipsum dolor sit, amet consectetur adipisicing elit. Maiores nisi
incidunt ipsum cupiditate nostrum nesciunt, corrupti nihil at atque
animi ab aut. Quam iusto harum eligendi magnam nulla repudiandae
molestias.
</BodyLong>
<>
<Button onClick={() => setOpen(!open)}>Open modal</Button>
<Modal
open={open}
onClose={() => setOpen(false)}
header={{ heading: "Modal-demo" }}
>
<Modal.Body style={{ position: "relative" }}>
<BodyLong spacing>
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
</BodyLong>
<HGrid gap="6" columns={{ xs: 1, lg: 2 }}>
<HStack justify="center" align="start">
<DatePicker.Standalone
fromDate={new Date("Aug 23 2019")}
toDate={new Date("Feb 23 2024")}
/>
</HStack>
<DatePicker {...datepickerProps} dropdownCaption>
<DatePicker.Input {...inputProps} label="Velg dato" />
</DatePicker>
<HStack justify="center">
<DatePicker.Standalone
fromDate={new Date("Aug 23 2019")}
toDate={new Date("Feb 23 2024")}
<DatePicker.Input
{...inputProps}
label="Velg dato"
description="Format: dd.mm.yyyy"
/>
</HStack>
</HGrid>
</Modal.Body>
</Modal>
</DatePicker>
</Modal.Body>
<Modal.Footer>
<Button>Neste</Button>
<Button variant="secondary">Tilbake</Button>
<Button variant="tertiary">Avbryt</Button>
</Modal.Footer>
</Modal>
</>
);
};
ModalDemo.parameters = { chromatic: { pauseAnimationAtEnd: true } };
@@ -101,9 +101,4 @@ export interface DatePickerDefaultProps
* @default See Popover
*/
strategy?: "absolute" | "fixed";
/**
* Bubbles Escape keydown-event up trough DOM-tree. This is set to false by default to prevent closing components like Modal on Escape
* @default false
*/
bubbleEscape?: boolean;
}
@@ -1,17 +1,15 @@
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
import isWeekend from "date-fns/isWeekend";
import React, { useCallback, useRef, useState } from "react";
import React, { useCallback, useState } from "react";
import { DayClickEventHandler, isMatch } from "react-day-picker";
import { DateInputProps } from "../DateInput";
import { DatePickerProps } from "../datepicker/DatePicker";
import { DateInputProps } from "../parts/DateInput";
import {
formatDateForInput,
getLocaleFromString,
isValidDate,
parseDate,
} from "../utils";
import { useEscape } from "./useEscape";
import { useOutsideClickHandler } from "./useOutsideClickHandler";
export interface UseDatepickerOptions
extends Pick<
@@ -59,10 +57,10 @@ export interface UseDatepickerOptions
*/
allowTwoDigitYear?: boolean;
/**
* Opens datepicker on input-focus
* @default true
* Will be removed in a future major-version
* @deprecated
*/
openOnFocus?: boolean;
openOnFocus?: false;
}
interface UseDatepickerValue {
@@ -76,7 +74,14 @@ interface UseDatepickerValue {
inputProps: Pick<
DateInputProps,
"onChange" | "onFocus" | "onBlur" | "value"
> & { ref: React.RefObject<HTMLInputElement> };
> & {
/**
* @private
*/
setAnchorRef: React.Dispatch<
React.SetStateAction<HTMLButtonElement | null>
>;
};
/**
* Resets all states (callback)
*/
@@ -143,14 +148,11 @@ export const useDatepicker = (
onValidate,
defaultMonth,
allowTwoDigitYear = true,
openOnFocus = true,
} = opt;
const [anchorRef, setAnchorRef] = useState<HTMLButtonElement | null>(null);
const locale = getLocaleFromString(_locale);
const inputRef = useRef<HTMLInputElement>(null);
const [daypickerRef, setDaypickerRef] = useState<HTMLDivElement>();
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
// Initialize states
@@ -172,14 +174,6 @@ export const useDatepicker = (
[defaultMonth, defaultSelected, selectedDay, today]
);
useOutsideClickHandler(open, handleOpen, [
daypickerRef,
inputRef.current,
inputRef.current?.nextSibling,
]);
useEscape(open, handleOpen, inputRef);
const updateDate = (date?: Date) => {
onDateChange?.(date);
setSelectedDay(date);
@@ -207,7 +201,6 @@ export const useDatepicker = (
if (e.target.readOnly) {
return;
}
!open && openOnFocus && handleOpen(true);
const day = parseDate(
e.target.value,
today,
@@ -243,7 +236,7 @@ export const useDatepicker = (
const handleDayClick: DayClickEventHandler = (day, { selected }) => {
if (day && !selected) {
handleOpen(false);
inputRef.current && inputRef.current.focus();
anchorRef?.focus();
}
if (!required && selected) {
@@ -319,11 +312,13 @@ export const useDatepicker = (
toDate,
today,
open,
onClose: () => {
handleOpen(false);
anchorRef?.focus();
},
onOpenToggle: () => handleOpen(!open),
disabled,
disableWeekends,
bubbleEscape: true,
ref: setDaypickerRef,
};
const inputProps = {
@@ -331,7 +326,7 @@ export const useDatepicker = (
onFocus: handleFocus,
onBlur: handleBlur,
value: inputValue,
ref: inputRef,
setAnchorRef,
};
return { datepickerProps, inputProps, reset, selectedDay, setSelected };
@@ -1,30 +0,0 @@
import { useCallback, useEffect, RefObject } from "react";
export const useEscape = (
open: boolean,
setOpen: (openState: boolean) => void,
focusRef: RefObject<HTMLElement>
) => {
const handleClose = useCallback(() => {
setOpen(false);
focusRef?.current && focusRef.current.focus();
}, [focusRef, setOpen]);
const escape = useCallback(
(event: KeyboardEvent) => {
if (open && event.key === "Escape") {
event.preventDefault(); // This prevents modal from closing when using datepicker inside modal
handleClose();
}
},
[handleClose, open]
);
useEffect(() => {
window.addEventListener("keydown", escape, false);
return () => {
window.removeEventListener("keydown", escape, false);
};
}, [escape]);
};
@@ -1,6 +1,6 @@
import React, { useCallback, useMemo, useRef, useState } from "react";
import { DateInputProps } from "../DateInput";
import React, { useCallback, useMemo, useState } from "react";
import { MonthPickerProps } from "../monthpicker/types";
import { DateInputProps } from "../parts/DateInput";
import {
formatDateForInput,
getLocaleFromString,
@@ -8,8 +8,6 @@ import {
isValidDate,
parseDate,
} from "../utils";
import { useEscape } from "./useEscape";
import { useOutsideClickHandler } from "./useOutsideClickHandler";
export interface UseMonthPickerOptions
extends Pick<
@@ -45,8 +43,8 @@ export interface UseMonthPickerOptions
*/
allowTwoDigitYear?: boolean;
/**
* Opens datepicker on input-focus
* @default true
* Will be removed in a future major-version
* @deprecated
*/
openOnFocus?: boolean;
}
@@ -60,7 +58,12 @@ interface UseMonthPickerValue {
* Use: <MonthPicker.Input {...inputProps} />
*/
inputProps: Pick<DateInputProps, "onChange" | "onFocus" | "value"> & {
ref: React.RefObject<HTMLInputElement>;
/**
* @private
*/
setAnchorRef: React.Dispatch<
React.SetStateAction<HTMLButtonElement | null>
>;
};
/**
* Currently selected Date
@@ -138,17 +141,14 @@ export const useMonthpicker = (
onValidate,
defaultYear,
allowTwoDigitYear = true,
openOnFocus = true,
} = opt;
const [anchorRef, setAnchorRef] = useState<HTMLButtonElement | null>(null);
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
const today = useMemo(() => new Date(), []);
const locale = getLocaleFromString(_locale);
const inputRef = useRef<HTMLInputElement>(null);
const [monthpickerRef, setMonthpickerRef] = useState<HTMLDivElement>();
// Initialize states
const [year, setYear] = useState(defaultSelected ?? defaultYear ?? today);
const [selectedMonth, setSelectedMonth] = useState(defaultSelected);
@@ -169,14 +169,6 @@ export const useMonthpicker = (
[defaultSelected, defaultYear, selectedMonth, today]
);
useOutsideClickHandler(open, handleOpen, [
monthpickerRef,
inputRef.current,
inputRef.current?.nextSibling,
]);
useEscape(open, handleOpen, inputRef);
const updateMonth = (date?: Date) => {
onMonthChange?.(date);
setSelectedMonth(date);
@@ -204,7 +196,7 @@ export const useMonthpicker = (
if (e.target.readOnly) {
return;
}
!open && openOnFocus && handleOpen(true);
const day = parseDate(
e.target.value,
today,
@@ -239,8 +231,8 @@ export const useMonthpicker = (
const handleMonthClick = (month?: Date) => {
if (month) {
handleOpen(false);
inputRef.current && inputRef.current.focus();
setYear(month);
anchorRef?.focus();
}
if (!required && !month) {
@@ -314,9 +306,11 @@ export const useMonthpicker = (
toDate,
open,
onOpenToggle: () => handleOpen(!open),
onClose: () => {
handleOpen(false);
anchorRef?.focus();
},
disabled,
bubbleEscape: true,
ref: setMonthpickerRef,
};
const inputProps = {
@@ -324,7 +318,7 @@ export const useMonthpicker = (
onFocus: handleFocus,
onBlur: handleBlur,
value: inputValue,
ref: inputRef,
setAnchorRef,
};
return { monthpickerProps, inputProps, reset, selectedMonth, setSelected };
@@ -1,34 +0,0 @@
import { useCallback, useEffect } from "react";
export const useOutsideClickHandler = (
open: boolean,
setOpen: (openState: boolean) => void,
refs: Array<any>
) => {
const handleFocusIn = useCallback(
(e) => {
const composed = e.composedPath?.()?.[0];
if (!e?.target || !e?.target?.nodeType || !composed) {
return;
}
if (
!refs.some(
(element) =>
element?.contains(e.target) || element?.contains(composed)
)
) {
open && setOpen(false);
}
},
[open, refs, setOpen]
);
useEffect(() => {
window.addEventListener("focusin", handleFocusIn);
window.addEventListener("pointerdown", handleFocusIn);
return () => {
window?.removeEventListener?.("focusin", handleFocusIn);
window?.removeEventListener?.("pointerdown", handleFocusIn);
};
}, [handleFocusIn]);
};
@@ -1,10 +1,10 @@
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
import checkIsBefore from "date-fns/isBefore";
import isWeekend from "date-fns/isWeekend";
import React, { useRef, useState } from "react";
import React, { useState } from "react";
import { DateRange, isMatch } from "react-day-picker";
import { DateInputProps } from "../DateInput";
import { DatePickerProps } from "../datepicker/DatePicker";
import { DateInputProps } from "../parts/DateInput";
import {
formatDateForInput,
getLocaleFromString,
@@ -12,8 +12,6 @@ import {
parseDate,
} from "../utils";
import { DateValidationT, UseDatepickerOptions } from "./useDatepicker";
import { useEscape } from "./useEscape";
import { useOutsideClickHandler } from "./useOutsideClickHandler";
export type RangeValidationT = {
from: DateValidationT;
@@ -74,14 +72,28 @@ interface UseRangeDatepickerValue {
fromInputProps: Pick<
DateInputProps,
"onChange" | "onFocus" | "onBlur" | "value"
> & { ref: React.RefObject<HTMLInputElement> };
> & {
/**
* @private
*/
setAnchorRef: React.Dispatch<
React.SetStateAction<HTMLButtonElement | null>
>;
};
/**
* Use: <DatePicker.Input label="to" {...toInputProps}/>
*/
toInputProps: Pick<
DateInputProps,
"onChange" | "onFocus" | "onBlur" | "value"
> & { ref: React.RefObject<HTMLInputElement> };
> & {
/**
* @private
*/
setAnchorRef?: React.Dispatch<
React.SetStateAction<HTMLButtonElement | null>
>;
};
/**
* Resets all states (callback)
*/
@@ -218,14 +230,11 @@ export const useRangeDatepicker = (
onValidate,
defaultMonth,
allowTwoDigitYear = true,
openOnFocus = true,
} = opt;
const locale = getLocaleFromString(_locale);
const [anchorRef, setAnchorRef] = useState<HTMLButtonElement | null>(null);
const inputRefTo = useRef<HTMLInputElement>(null);
const inputRefFrom = useRef<HTMLInputElement>(null);
const [daypickerRef, setDaypickerRef] = useState<HTMLDivElement>();
const locale = getLocaleFromString(_locale);
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
@@ -255,20 +264,6 @@ export const useRangeDatepicker = (
const [open, setOpen] = useState(false);
useOutsideClickHandler(open, setOpen, [
daypickerRef,
inputRefTo.current,
inputRefFrom.current,
inputRefTo.current?.nextSibling,
inputRefFrom.current?.nextSibling,
]);
useEscape(
open,
setOpen,
selectedRange?.from && !selectedRange?.to ? inputRefTo : inputRefFrom
);
const updateRange = (range?: DateRange) => {
onRangeChange?.(range);
setSelectedRange(range);
@@ -324,7 +319,6 @@ export const useRangeDatepicker = (
if (e.target.readOnly) {
return;
}
!open && openOnFocus && setOpen(true);
const day = parseDate(
e.target.value,
today,
@@ -378,6 +372,7 @@ export const useRangeDatepicker = (
const handleSelect = (range) => {
if (range?.from && range?.to) {
setOpen(false);
anchorRef?.focus();
}
const prevToRange =
!selectedRange?.from && selectedRange?.to ? selectedRange?.to : range?.to;
@@ -545,10 +540,12 @@ export const useRangeDatepicker = (
mode: "range" as const,
open,
onOpenToggle: () => setOpen((x) => !x),
onClose: () => {
setOpen(false);
anchorRef?.focus();
},
disabled,
disableWeekends,
bubbleEscape: true,
ref: setDaypickerRef,
};
const fromInputProps = {
@@ -556,7 +553,7 @@ export const useRangeDatepicker = (
onFocus: (e) => handleFocus(e, RANGE.FROM),
onBlur: (e) => handleBlur(e, RANGE.FROM),
value: fromInputValue,
ref: inputRefFrom,
setAnchorRef,
};
const toInputProps = {
@@ -564,7 +561,7 @@ export const useRangeDatepicker = (
onFocus: (e) => handleFocus(e, RANGE.TO),
onBlur: (e) => handleBlur(e, RANGE.TO),
value: toInputValue,
ref: inputRefTo,
setAnchorRef,
};
return {
+1 -1
View File
@@ -1,4 +1,3 @@
export { type DateInputProps } from "./DateInput";
export {
default as DatePicker,
type DatePickerProps,
@@ -15,3 +14,4 @@ export {
export { default as MonthPicker } from "./monthpicker/MonthPicker";
export { type MonthPickerStandaloneProps } from "./monthpicker/MonthPickerStandalone";
export { type MonthPickerProps } from "./monthpicker/types";
export { type DateInputProps } from "./parts/DateInput";
@@ -1,10 +1,10 @@
import cl from "clsx";
import React, { forwardRef, useRef, useState } from "react";
import React, { forwardRef, useMemo, useRef, useState } from "react";
import { RootProvider } from "react-day-picker";
import { Popover } from "../../popover";
import { useId } from "../../util";
import { MonthPickerInput } from "../DateInput";
import { mergeRefs, useId } from "../../util";
import { DateContext, SharedMonthProvider } from "../context";
import { MonthPickerInput } from "../parts/DateInput";
import { DateWrapper } from "../parts/DateWrapper";
import { getLocaleFromString } from "../utils";
import MonthCaption from "./MonthCaption";
import MonthPickerStandalone from "./MonthPickerStandalone";
@@ -74,7 +74,6 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
year,
onYearChange,
strategy = "absolute",
bubbleEscape = false,
},
ref
) => {
@@ -82,6 +81,7 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
const [open, setOpen] = useState(_open ?? false);
const wrapperRef = useRef<HTMLDivElement | null>(null);
const mergedRef = useMemo(() => mergeRefs([wrapperRef, ref]), [ref]);
const [selectedMonth, setSelectedMonth] = useState<Date | undefined>(
defaultSelected
@@ -107,51 +107,47 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
onOpenToggle?.();
},
ariaId,
defined: true,
}}
>
<div
ref={wrapperRef}
ref={mergedRef}
className={cl("navds-date__wrapper", wrapperClassName)}
>
{children}
{(_open ?? open) && (
<Popover
arrow={false}
anchorEl={wrapperRef.current}
open={_open ?? open}
onClose={() => onClose?.() ?? setOpen(false)}
placement="bottom-start"
role="dialog"
ref={ref}
id={ariaId}
className="navds-date navds-date__popover"
strategy={strategy}
bubbleEscape={bubbleEscape}
flip={false}
<DateWrapper
open={_open ?? open}
anchor={wrapperRef.current}
onClose={() => onClose?.() ?? setOpen(false)}
locale={locale}
variant="month"
popoverProps={{
id: ariaId,
strategy,
}}
>
<RootProvider
locale={getLocaleFromString(locale)}
selected={selected ?? selectedMonth}
toDate={toDate}
fromDate={fromDate}
month={selected ?? selectedMonth}
>
<RootProvider
locale={getLocaleFromString(locale)}
selected={selected ?? selectedMonth}
toDate={toDate}
fromDate={fromDate}
month={selected ?? selectedMonth}
>
<div className={cl("rdp-month", className)}>
<SharedMonthProvider
dropdownCaption={dropdownCaption}
disabled={disabled}
selected={selected ?? selectedMonth}
onSelect={handleSelect}
year={year}
onYearChange={onYearChange}
>
<MonthCaption />
<MonthSelector />
</SharedMonthProvider>
</div>
</RootProvider>
</Popover>
)}
<div className={cl("rdp-month", className)}>
<SharedMonthProvider
dropdownCaption={dropdownCaption}
disabled={disabled}
selected={selected ?? selectedMonth}
onSelect={handleSelect}
year={year}
onYearChange={onYearChange}
>
<MonthCaption />
<MonthSelector />
</SharedMonthProvider>
</div>
</RootProvider>
</DateWrapper>
</div>
</DateContext.Provider>
);
@@ -75,9 +75,4 @@ export interface MonthPickerProps extends React.HTMLAttributes<HTMLDivElement> {
* @default "absolute"
*/
strategy?: "absolute" | "fixed";
/**
* Bubbles Escape keydown-event up trough DOM-tree. This is set to false by default to prevent closing components like Modal on Escape
* @default false
*/
bubbleEscape?: boolean;
}
@@ -1,11 +1,11 @@
import { CalendarIcon } from "@navikt/aksel-icons";
import cl from "clsx";
import React, { forwardRef, InputHTMLAttributes } from "react";
import { FormFieldProps, useFormField } from "../form/useFormField";
import { useDateInputContext } from "./context";
import { ReadOnlyIcon } from "../form/ReadOnlyIcon";
import { BodyShort, ErrorMessage, Label } from "../typography";
import { omit } from "../util";
import React, { forwardRef, InputHTMLAttributes, useRef } from "react";
import { ReadOnlyIcon } from "../../form/ReadOnlyIcon";
import { FormFieldProps, useFormField } from "../../form/useFormField";
import { BodyShort, ErrorMessage, Label } from "../../typography";
import { omit } from "../../util";
import { useDateInputContext } from "../context";
export interface DateInputProps
extends FormFieldProps,
@@ -28,6 +28,10 @@ export interface DateInputProps
* @private
*/
variant?: "datepicker" | "monthpicker";
/**
* @private
*/
setAnchorRef?: React.Dispatch<React.SetStateAction<HTMLButtonElement | null>>;
}
const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
@@ -37,9 +41,12 @@ const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
label,
description,
variant = "datepicker",
setAnchorRef,
...rest
} = props;
const buttonRef = useRef<HTMLButtonElement>(null);
const isDatepickerVariant = variant === "datepicker";
const conditionalVariables = {
@@ -50,7 +57,7 @@ const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
},
};
const { onOpen, ariaId, open } = useDateInputContext();
const context = useDateInputContext();
const {
inputProps,
@@ -108,7 +115,7 @@ const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
{...omit(rest, ["error", "errorId", "size"])}
{...inputProps}
autoComplete="off"
aria-controls={open ? ariaId : undefined}
aria-controls={context?.open ? context.ariaId : undefined}
readOnly={readOnly}
className={cl(
"navds-date__field-input",
@@ -116,19 +123,23 @@ const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
"navds-body-short",
`navds-body-short--${size}`
)}
size={isDatepickerVariant ? 10 : 14}
size={isDatepickerVariant ? 11 : 14}
/>
<button
disabled={inputProps.disabled || readOnly}
tabIndex={readOnly ? -1 : open ? -1 : 0}
onClick={() => onOpen()}
tabIndex={readOnly ? -1 : context?.open ? -1 : 0}
onClick={() => {
context?.onOpen();
setAnchorRef?.(buttonRef.current);
}}
type="button"
className="navds-date__field-button"
ref={buttonRef}
>
<CalendarIcon
pointerEvents="none"
title={
open
context?.open
? conditionalVariables.iconTitle.close
: conditionalVariables.iconTitle.open
}
@@ -0,0 +1,83 @@
import cl from "clsx";
import React, { useContext, useRef } from "react";
import { Button } from "../../button";
import { Modal } from "../../modal";
import { ModalContext } from "../../modal/ModalContext";
import { Popover } from "../../popover";
import { useMedia } from "../../util/useMedia";
import { modalCloseButtonLabel, modalLabel } from "../utils/labels";
type DateWrapperProps = {
open: boolean;
children: React.ReactNode;
onClose: () => void;
anchor: HTMLDivElement | null;
locale: "nb" | "nn" | "en";
variant: "single" | "multiple" | "range" | "month";
popoverProps: {
id?: string;
strategy?: "absolute" | "fixed";
};
};
export const DateWrapper = ({
open,
children,
onClose,
anchor,
locale,
variant,
popoverProps,
}: DateWrapperProps) => {
const modalRef = useRef<HTMLDialogElement>(null);
const isInModal = useContext(ModalContext) !== null;
const hideModal =
useMedia("screen and (min-width: 768px)", true) && !isInModal;
if (hideModal) {
return (
<Popover
arrow={false}
anchorEl={anchor}
open={open}
onClose={onClose}
placement="bottom-start"
role="dialog"
className={cl("navds-date__popover", {
"navds-date": variant === "month",
})}
flip={false}
{...popoverProps}
>
{children}
</Popover>
);
}
return (
<Modal
ref={modalRef}
open={open}
onClose={(event) => {
event.stopPropagation();
onClose();
}}
aria-label={modalLabel(locale, variant)}
className={cl("navds-date__modal", {
"navds-date__nested-modal": isInModal,
"navds-date": variant === "month",
})}
closeOnBackdropClick
>
<div className="navds-date__modal-body">
{children}
<Button
variant="tertiary"
onClick={() => modalRef?.current?.close()}
size="small"
>
{modalCloseButtonLabel(locale)}
</Button>
</div>
</Modal>
);
};
@@ -133,3 +133,86 @@ export const labelWeek = (localeCode?: string): string => {
return `Uke:`;
}
};
const modalLabelSingle = (localeCode?: string): string => {
switch (localeCode) {
case "nb":
return `Velg dato`;
case "nn":
return `Vel dato`;
case "en-GB":
return `Pick a date`;
default:
return `Velg dato`;
}
};
const modalLabelMultiple = (localeCode?: string): string => {
switch (localeCode) {
case "nb":
return `Velg datoer`;
case "nn":
return `Vel datoar`;
case "en-GB":
return `Pick dates`;
default:
return `Velg datoer`;
}
};
const modalLabelRanged = (localeCode?: string): string => {
switch (localeCode) {
case "nb":
return `Velg start- og sluttdato`;
case "nn":
return `Vel start- og sluttdato`;
case "en-GB":
return `Pick a start and end date`;
default:
return `Velg start- og sluttdato`;
}
};
const modalLabelMonth = (localeCode?: string): string => {
switch (localeCode) {
case "nb":
return `Velg måned`;
case "nn":
return `Vel månad`;
case "en-GB":
return `Pick a month`;
default:
return `Velg måned`;
}
};
export const modalLabel = (
localeCode: string,
variant: "single" | "multiple" | "range" | "month"
) => {
switch (variant) {
case "single":
return modalLabelSingle(localeCode);
case "multiple":
return modalLabelMultiple(localeCode);
case "range":
return modalLabelRanged(localeCode);
case "month":
return modalLabelMonth(localeCode);
default:
return modalLabelSingle(localeCode);
}
};
export const modalCloseButtonLabel = (localeCode?: string): string => {
switch (localeCode) {
case "nb":
return `Lukk`;
case "nn":
return `Lukk`;
case "en-GB":
return `Close`;
default:
return `Lukk`;
}
};
@@ -33,7 +33,7 @@ export const Combobox = forwardRef<
const toggleListButtonRef = useRef<HTMLButtonElement>(null);
const { currentOption, toggleIsListOpen } = useFilteredOptionsContext();
const { activeDecendantId, toggleIsListOpen } = useFilteredOptionsContext();
const { selectedOptions } = useSelectedOptionsContext();
const {
@@ -92,7 +92,7 @@ export const Combobox = forwardRef<
"navds-combobox__wrapper-inner navds-text-field__input",
{
"navds-combobox__wrapper-inner--virtually-unfocused":
currentOption !== null,
activeDecendantId !== null,
}
)}
onClick={focusInput}
@@ -65,7 +65,7 @@ const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
size,
}}
>
<CustomOptionsProvider>
<CustomOptionsProvider value={{ isMultiSelect }}>
<SelectedOptionsProvider
value={{
allowNewValues,
@@ -81,7 +81,6 @@ const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
filteredOptions,
isListOpen,
isLoading,
isMultiSelect,
options,
}}
>
@@ -6,6 +6,7 @@ import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsCon
import { useInputContext } from "../Input/inputContext";
import { Loader } from "../../../loader";
import { BodyShort, Label } from "../../../typography";
import filteredOptionsUtil from "./filtered-options-util";
const FilteredOptions = () => {
const {
@@ -18,25 +19,25 @@ const FilteredOptions = () => {
isLoading,
isListOpen,
filteredOptions,
filteredOptionsIndex,
filteredOptionsRef,
setFilteredOptionsRef,
isMouseLastUsedInputDevice,
setIsMouseLastUsedInputDevice,
isValueNew,
setFilteredOptionsIndex,
toggleIsListOpen,
activeDecendantId,
virtualFocus,
} = useFilteredOptionsContext();
const { isMultiSelect, selectedOptions, toggleOption } =
useSelectedOptionsContext();
return (
<ul
ref={filteredOptionsRef}
ref={setFilteredOptionsRef}
className={cl("navds-combobox__list", {
"navds-combobox__list--closed": !isListOpen,
"navds-combobox__list--with-hover": isMouseLastUsedInputDevice,
})}
id={`${id}-filtered-options`}
id={filteredOptionsUtil.getFilteredOptionsId(id)}
role="listbox"
tabIndex={-1}
>
@@ -45,7 +46,8 @@ const FilteredOptions = () => {
className="navds-combobox__list-item--loading"
role="option"
aria-selected={false}
id={`${id}-is-loading`}
id={filteredOptionsUtil.getIsLoadingId(id)}
data-no-focus="true"
>
<Loader aria-label="Søker..." />
</li>
@@ -54,8 +56,12 @@ const FilteredOptions = () => {
<li
tabIndex={-1}
onMouseMove={() => {
if (filteredOptionsIndex !== -1) {
setFilteredOptionsIndex(-1);
if (
activeDecendantId !== filteredOptionsUtil.getAddNewOptionId(id)
) {
virtualFocus.moveFocusToElement(
filteredOptionsUtil.getAddNewOptionId(id)
);
setIsMouseLastUsedInputDevice(true);
}
}}
@@ -64,10 +70,10 @@ const FilteredOptions = () => {
if (!isMultiSelect && !selectedOptions.includes(value))
toggleIsListOpen(false);
}}
id={`${id}-combobox-new-option`}
id={filteredOptionsUtil.getAddNewOptionId(id)}
className={cl("navds-combobox__list-item__new-option", {
"navds-combobox__list-item__new-option--focus":
filteredOptionsIndex === -1,
activeDecendantId === filteredOptionsUtil.getAddNewOptionId(id),
})}
role="option"
aria-selected={false}
@@ -86,24 +92,30 @@ const FilteredOptions = () => {
className="navds-combobox__list-item__no-options"
role="option"
aria-selected={false}
id={`${id}-no-hits`}
id={filteredOptionsUtil.getNoHitsId(id)}
data-no-focus="true"
>
Ingen søketreff
</li>
)}
{filteredOptions.map((option, index) => (
{filteredOptions.map((option) => (
<li
className={cl("navds-combobox__list-item", {
"navds-combobox__list-item--focus": index === filteredOptionsIndex,
"navds-combobox__list-item--focus":
activeDecendantId === filteredOptionsUtil.getOptionId(id, option),
"navds-combobox__list-item--selected":
selectedOptions.includes(option),
})}
id={`${id}-option-${option.replace(" ", "-")}`}
id={filteredOptionsUtil.getOptionId(id, option)}
key={option}
tabIndex={-1}
onMouseMove={() => {
if (filteredOptionsIndex !== index) {
setFilteredOptionsIndex(index);
if (
activeDecendantId !== filteredOptionsUtil.getOptionId(id, option)
) {
virtualFocus.moveFocusToElement(
filteredOptionsUtil.getOptionId(id, option)
);
setIsMouseLastUsedInputDevice(true);
}
}}
@@ -0,0 +1,38 @@
const normalizeText = (text: string): string =>
typeof text === "string" ? text.toLocaleLowerCase().trim() : "";
const isPartOfText = (value, text) =>
normalizeText(text).startsWith(normalizeText(value ?? ""));
const isValueInList = (value, list) =>
list?.find((listItem) => normalizeText(value) === normalizeText(listItem));
const getMatchingValuesFromList = (value, list) =>
list?.filter((listItem) => isPartOfText(value, listItem));
const getFilteredOptionsId = (comboboxId: string) =>
`${comboboxId}-filtered-options`;
const getOptionId = (comboboxId: string, option: string) =>
`${comboboxId.toLocaleLowerCase()}-option-${option
.replace(" ", "-")
.toLocaleLowerCase()}`;
const getAddNewOptionId = (comboboxId: string) =>
`${comboboxId}-combobox-new-option`;
const getIsLoadingId = (comboboxId: string) => `${comboboxId}-is-loading`;
const getNoHitsId = (comboboxId: string) => `${comboboxId}-no-hits`;
export default {
normalizeText,
isPartOfText,
isValueInList,
getMatchingValuesFromList,
getFilteredOptionsId,
getAddNewOptionId,
getOptionId,
getIsLoadingId,
getNoHitsId,
};
@@ -1,11 +1,9 @@
import React, {
useState,
useEffect,
useMemo,
createContext,
useContext,
useCallback,
useRef,
SetStateAction,
} from "react";
import cl from "clsx";
@@ -13,26 +11,29 @@ import { useCustomOptionsContext } from "../customOptionsContext";
import { useInputContext } from "../Input/inputContext";
import usePrevious from "../../../util/usePrevious";
import { useClientLayoutEffect } from "../../../util";
import filteredOptionsUtils from "./filtered-options-util";
import useVirtualFocus, { VirtualFocusType } from "./useVirtualFocus";
import { ComboboxProps } from "../types";
const normalizeText = (text: string): string =>
typeof text === "string" ? `${text}`.toLowerCase().trim() : "";
const isPartOfText = (value, text) =>
normalizeText(text).startsWith(normalizeText(value ?? ""));
const isValueInList = (value, list) =>
list?.find((listItem) => normalizeText(value) === normalizeText(listItem));
const getMatchingValuesFromList = (value, list) =>
list?.filter((listItem) => isPartOfText(value, listItem));
type FilteredOptionsProps = {
children: any;
value: Pick<
ComboboxProps,
| "allowNewValues"
| "filteredOptions"
| "isListOpen"
| "isLoading"
| "options"
>;
};
type FilteredOptionsContextType = {
activeDecendantId?: string;
allowNewValues?: boolean;
ariaDescribedBy?: string;
filteredOptionsRef: React.RefObject<HTMLUListElement>;
filteredOptionsIndex: number | null;
setFilteredOptionsIndex: (index: number) => void;
setFilteredOptionsRef: React.Dispatch<
React.SetStateAction<HTMLUListElement | null>
>;
isListOpen: boolean;
isLoading?: boolean;
filteredOptions: string[];
@@ -40,19 +41,18 @@ type FilteredOptionsContextType = {
setIsMouseLastUsedInputDevice: React.Dispatch<SetStateAction<boolean>>;
isValueNew: boolean;
toggleIsListOpen: (newState?: boolean) => void;
currentOption: string | null;
resetFilteredOptionsIndex: () => void;
moveFocusUp: () => void;
moveFocusDown: () => void;
moveFocusToInput: () => void;
moveFocusToEnd: () => void;
currentOption?: string;
shouldAutocomplete?: boolean;
virtualFocus: VirtualFocusType;
};
const FilteredOptionsContext = createContext<FilteredOptionsContextType>(
{} as FilteredOptionsContextType
);
export const FilteredOptionsProvider = ({ children, value: props }) => {
export const FilteredOptionsProvider = ({
children,
value: props,
}: FilteredOptionsProps) => {
const {
allowNewValues,
filteredOptions: externalFilteredOptions,
@@ -60,7 +60,9 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
isLoading,
options,
} = props;
const filteredOptionsRef = useRef<HTMLUListElement | null>(null);
const [filteredOptionsRef, setFilteredOptionsRef] =
useState<HTMLUListElement | null>(null);
const virtualFocus = useVirtualFocus(filteredOptionsRef);
const {
inputProps: { "aria-describedby": partialAriaDescribedBy, id },
value,
@@ -70,9 +72,6 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
shouldAutocomplete,
} = useInputContext();
const [filteredOptionsIndex, setFilteredOptionsIndex] = useState<
number | null
>(null);
const [isInternalListOpen, setInternalListOpen] = useState(false);
const { customOptions } = useCustomOptionsContext();
@@ -81,8 +80,7 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
return externalFilteredOptions;
}
const opts = [...customOptions, ...options];
setFilteredOptionsIndex(null);
return getMatchingValuesFromList(searchTerm, opts);
return filteredOptionsUtils.getMatchingValuesFromList(searchTerm, opts);
}, [customOptions, externalFilteredOptions, options, searchTerm]);
const previousSearchTerm = usePrevious(searchTerm);
@@ -90,10 +88,26 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
const [isMouseLastUsedInputDevice, setIsMouseLastUsedInputDevice] =
useState(false);
const filteredOptionsMap = useMemo(
() =>
options.reduce(
(map, _option) => ({
...map,
[filteredOptionsUtils.getOptionId(id, _option)]: _option,
}),
{
[filteredOptionsUtils.getAddNewOptionId(id)]: allowNewValues
? value
: undefined,
}
),
[allowNewValues, id, options, value]
);
useClientLayoutEffect(() => {
if (
shouldAutocomplete &&
normalizeText(searchTerm) !== "" &&
filteredOptionsUtils.normalizeText(searchTerm) !== "" &&
(previousSearchTerm?.length || 0) < searchTerm.length &&
filteredOptions.length > 0
) {
@@ -115,29 +129,30 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
return isExternalListOpen ?? isInternalListOpen;
}, [isExternalListOpen, isInternalListOpen]);
const toggleIsListOpen = useCallback((newState?: boolean) => {
setFilteredOptionsIndex(null);
setInternalListOpen((oldState) => newState ?? !oldState);
}, []);
const isValueNew = useMemo(
() => Boolean(value) && !isValueInList(value, filteredOptions),
[value, filteredOptions]
const toggleIsListOpen = useCallback(
(newState?: boolean) => {
virtualFocus.moveFocusToTop();
setInternalListOpen((oldState) => newState ?? !oldState);
},
[virtualFocus]
);
const getMinimumIndex = useCallback(() => {
return isValueNew && allowNewValues ? -1 : 0;
}, [allowNewValues, isValueNew]);
const isValueNew = useMemo(
() =>
Boolean(value) &&
!filteredOptionsMap[filteredOptionsUtils.getOptionId(id, value)],
[filteredOptionsMap, id, value]
);
const ariaDescribedBy = useMemo(() => {
let activeOption;
if (!isLoading && filteredOptions.length === 0) {
activeOption = `${id}-no-hits`;
activeOption = filteredOptionsUtils.getNoHitsId(id);
} else if ((value && value !== "") || isLoading) {
if (shouldAutocomplete && filteredOptions[0]) {
activeOption = `${id}-option-${filteredOptions[0].replace(" ", "-")}`;
activeOption = filteredOptionsUtils.getOptionId(id, filteredOptions[0]);
} else if (isListOpen && isLoading) {
activeOption = `${id}-is-loading`;
activeOption = filteredOptionsUtils.getIsLoadingId(id);
}
}
return cl(activeOption, partialAriaDescribedBy) || undefined;
@@ -151,102 +166,21 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
id,
]);
const currentOption = useMemo(() => {
if (filteredOptionsIndex == null) {
return null;
}
if (filteredOptionsIndex === -1) {
return value;
}
return filteredOptions[filteredOptionsIndex];
}, [filteredOptionsIndex, filteredOptions, value]);
const currentOption = useMemo(
() =>
filteredOptionsMap[virtualFocus.activeElement?.getAttribute("id") || -1],
[filteredOptionsMap, virtualFocus]
);
const resetFilteredOptionsIndex = () => {
setFilteredOptionsIndex(getMinimumIndex());
};
const scrollToOption = useCallback((newIndex: number) => {
if (
filteredOptionsRef.current &&
filteredOptionsRef.current.children[newIndex]
) {
const child = filteredOptionsRef.current.children[newIndex];
const { top, bottom } = child.getBoundingClientRect();
const parentRect = filteredOptionsRef.current.getBoundingClientRect();
if (top < parentRect.top || bottom > parentRect.bottom) {
child.scrollIntoView({ block: "nearest" });
}
}
}, []);
useEffect(() => {
if (filteredOptionsIndex !== null && isListOpen) {
scrollToOption(filteredOptionsIndex);
}
}, [filteredOptionsIndex, isListOpen, scrollToOption]);
const moveFocusToInput = useCallback(() => {
setFilteredOptionsIndex(null);
toggleIsListOpen(false);
}, [toggleIsListOpen]);
const moveFocusToEnd = useCallback(() => {
const lastIndex = filteredOptions.length - 1;
toggleIsListOpen(true);
setFilteredOptionsIndex(lastIndex);
}, [filteredOptions.length, toggleIsListOpen]);
const moveFocusUp = useCallback(() => {
if (filteredOptionsIndex === null) {
return;
}
if (filteredOptionsIndex === getMinimumIndex()) {
toggleIsListOpen(false);
setFilteredOptionsIndex(null);
} else {
const newIndex = Math.max(getMinimumIndex(), filteredOptionsIndex - 1);
setFilteredOptionsIndex(newIndex);
}
}, [filteredOptionsIndex, getMinimumIndex, toggleIsListOpen]);
const moveFocusDown = useCallback(() => {
if (filteredOptionsIndex === null || !isListOpen) {
toggleIsListOpen(true);
if (allowNewValues || filteredOptions.length >= 1) {
setFilteredOptionsIndex(getMinimumIndex());
}
return;
}
const newIndex = Math.min(
filteredOptionsIndex + 1,
Math.max(getMinimumIndex(), filteredOptions.length - 1)
);
setFilteredOptionsIndex(newIndex);
}, [
allowNewValues,
filteredOptions.length,
filteredOptionsIndex,
getMinimumIndex,
isListOpen,
toggleIsListOpen,
]);
const activeDecendantId = useMemo(() => {
if (filteredOptionsIndex === null) {
return undefined;
} else if (filteredOptionsIndex === -1) {
return `${id}-combobox-new-option`;
} else {
return `${id}-option-${currentOption?.replace(" ", "-")}`;
}
}, [filteredOptionsIndex, currentOption, id]);
const activeDecendantId = useMemo(
() => virtualFocus.activeElement?.getAttribute("id") || undefined,
[virtualFocus.activeElement]
);
const filteredOptionsState = {
activeDecendantId,
allowNewValues,
filteredOptionsRef,
filteredOptionsIndex,
setFilteredOptionsIndex,
setFilteredOptionsRef,
shouldAutocomplete,
isListOpen,
isLoading,
@@ -256,11 +190,7 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
isValueNew,
toggleIsListOpen,
currentOption,
resetFilteredOptionsIndex,
moveFocusUp,
moveFocusDown,
moveFocusToInput,
moveFocusToEnd,
virtualFocus,
ariaDescribedBy,
};
@@ -0,0 +1,87 @@
import { Dispatch, SetStateAction, useState } from "react";
export type VirtualFocusType = {
activeElement: HTMLElement | undefined;
getElementById: (id: string) => HTMLElement | undefined;
isFocusOnTheTop: boolean;
isFocusOnTheBottom: boolean;
setIndex: Dispatch<SetStateAction<number>>;
moveFocusUp: () => void;
moveFocusDown: () => void;
moveFocusToElement: (id: string) => void;
moveFocusToTop: () => void;
moveFocusToBottom: () => void;
};
const useVirtualFocus = (
containerRef: HTMLElement | null
): VirtualFocusType => {
const [index, setIndex] = useState(-1);
const listOfAllChildren: Array<HTMLElement> = containerRef?.children
? Array.prototype.slice.call(containerRef?.children)
: [];
const elementsAbleToReceiveFocus = listOfAllChildren.filter(
(child) => child.getAttribute("data-no-focus") !== "true"
);
const activeElement = elementsAbleToReceiveFocus[index];
const getElementById = (id: string) =>
listOfAllChildren.find((element) => element.id === id);
const isFocusOnTheTop = index === 0;
const isFocusOnTheBottom = index === elementsAbleToReceiveFocus.length - 1;
const scrollToOption = (newIndex: number) => {
const indexOfElementToScrollTo = Math.min(
Math.max(newIndex, 0),
containerRef?.children.length || 0
);
if (containerRef?.children[indexOfElementToScrollTo]) {
const child = containerRef.children[indexOfElementToScrollTo];
const { top, bottom } = child.getBoundingClientRect();
const parentRect = containerRef.getBoundingClientRect();
if (top < parentRect.top || bottom > parentRect.bottom) {
child.scrollIntoView({ block: "nearest" });
}
}
};
const _moveFocusAndScrollTo = (_index: number) => {
setIndex(_index);
scrollToOption(_index);
};
const moveFocusUp = () => _moveFocusAndScrollTo(Math.max(index - 1, -1));
const moveFocusDown = () =>
_moveFocusAndScrollTo(
Math.min(index + 1, elementsAbleToReceiveFocus.length - 1)
);
const moveFocusToTop = () => _moveFocusAndScrollTo(-1);
const moveFocusToBottom = () =>
_moveFocusAndScrollTo(elementsAbleToReceiveFocus.length - 1);
const moveFocusToElement = (id: string) => {
const thisElement = elementsAbleToReceiveFocus.find(
(_element) => _element.getAttribute("id") === id
);
const indexOfElement = thisElement
? elementsAbleToReceiveFocus.indexOf(thisElement)
: -1;
if (indexOfElement >= 0) {
setIndex(indexOfElement);
}
};
return {
activeElement,
getElementById,
isFocusOnTheTop,
isFocusOnTheBottom,
setIndex,
moveFocusUp,
moveFocusDown,
moveFocusToElement,
moveFocusToTop,
moveFocusToBottom,
};
};
export default useVirtualFocus;
@@ -9,6 +9,7 @@ import cl from "clsx";
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
import { useInputContext } from "./inputContext";
import filteredOptionsUtil from "../FilteredOptions/filtered-options-util";
interface InputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, "value"> {
@@ -34,15 +35,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
isValueNew,
toggleIsListOpen,
isListOpen,
filteredOptionsIndex,
moveFocusUp,
moveFocusDown,
ariaDescribedBy,
moveFocusToInput,
moveFocusToEnd,
setFilteredOptionsIndex,
setIsMouseLastUsedInputDevice,
shouldAutocomplete,
virtualFocus,
} = useFilteredOptionsContext();
const onEnter = useCallback(
@@ -57,8 +53,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
event.preventDefault();
// Selecting a value from the dropdown / FilteredOptions
toggleOption(currentOption, event);
if (!isMultiSelect && !isTextInSelectedOptions(currentOption))
if (!isMultiSelect && !isTextInSelectedOptions(currentOption)) {
toggleIsListOpen(false);
}
} else if (shouldAutocomplete && isTextInSelectedOptions(value)) {
event.preventDefault();
// Trying to set the same value that is already set, so just clearing the input
@@ -104,10 +101,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
onEnter(e);
break;
case "Home":
moveFocusToInput();
virtualFocus.moveFocusToTop();
break;
case "End":
moveFocusToEnd();
virtualFocus.moveFocusToBottom();
break;
default:
break;
@@ -128,14 +125,20 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
// so we don't interfere with text editing
if (e.target.selectionStart === value?.length) {
e.preventDefault();
moveFocusDown();
if (virtualFocus.activeElement === null || !isListOpen) {
toggleIsListOpen(true);
}
virtualFocus.moveFocusDown();
}
} else if (e.key === "ArrowUp") {
// Check that the FilteredOptions list is open and has virtual focus.
// Otherwise ignore keystrokes, so it doesn't interfere with text editing
if (isListOpen && filteredOptionsIndex !== null) {
if (isListOpen && activeDecendantId) {
e.preventDefault();
moveFocusUp();
if (virtualFocus.isFocusOnTheTop) {
toggleIsListOpen(false);
}
virtualFocus.moveFocusUp();
}
}
},
@@ -143,11 +146,11 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
value,
selectedOptions,
removeSelectedOption,
moveFocusDown,
isListOpen,
filteredOptionsIndex,
moveFocusUp,
activeDecendantId,
setIsMouseLastUsedInputDevice,
toggleIsListOpen,
virtualFocus,
]
);
@@ -159,13 +162,14 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
} else if (filteredOptions.length === 0) {
toggleIsListOpen(false);
}
virtualFocus.moveFocusToTop();
onChange(event);
},
[filteredOptions.length, onChange, toggleIsListOpen]
[filteredOptions.length, virtualFocus, onChange, toggleIsListOpen]
);
const onBlur = () => {
setFilteredOptionsIndex(-1);
virtualFocus.moveFocusToTop();
};
return (
@@ -180,7 +184,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
onBlur={onBlur}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
aria-controls={`${inputProps.id}-filtered-options`}
aria-controls={filteredOptionsUtil.getFilteredOptionsId(inputProps.id)}
aria-expanded={!!isListOpen}
autoComplete="off"
aria-autocomplete={shouldAutocomplete ? "both" : "list"}
@@ -1,6 +1,5 @@
import React, { useState, useCallback, createContext, useContext } from "react";
import { useInputContext } from "./Input/inputContext";
import { useSelectedOptionsContext } from "./SelectedOptions/selectedOptionsContext";
type CustomOptionsContextType = {
customOptions: string[];
@@ -13,13 +12,19 @@ const CustomOptionsContext = createContext<CustomOptionsContextType>(
{} as CustomOptionsContextType
);
export const CustomOptionsProvider = ({ children }) => {
export const CustomOptionsProvider = ({
children,
value,
}: {
children: any;
value: { isMultiSelect?: boolean };
}) => {
const [customOptions, setCustomOptions] = useState<string[]>([]);
const { focusInput } = useInputContext();
const { isMultiSelect } = useSelectedOptionsContext();
const { isMultiSelect } = value;
const removeCustomOption = useCallback(
(option) => {
(option: string) => {
setCustomOptions((prevCustomOptions) =>
prevCustomOptions.filter((o) => o !== option)
);
@@ -29,7 +34,7 @@ export const CustomOptionsProvider = ({ children }) => {
);
const addCustomOption = useCallback(
(option) => {
(option: string) => {
if (isMultiSelect) {
setCustomOptions((prevOptions) => [...prevOptions, option]);
} else {
@@ -1,7 +1,7 @@
import { InformationIcon } from "@navikt/aksel-icons";
import { Meta } from "@storybook/react";
import React from "react";
import { BodyLong, GuidePanel, VStack } from "../index";
import { Meta } from "@storybook/react";
import { InformationIcon } from "@navikt/aksel-icons";
export default {
title: "ds-react/GuidePanel",
+1
View File
@@ -38,3 +38,4 @@ export * from "./layout/stack";
export * from "./layout/grid";
export * from "./layout/content-container";
export * from "./layout/responsive";
export * from "./layout/page";
@@ -1,8 +1,8 @@
import React from "react";
import type { Meta } from "@storybook/react";
import React from "react";
import { Box, HStack, VStack } from "../..";
import { BodyLong } from "../../typography";
import { Bleed } from "./Bleed";
import { Box, HStack, VStack } from "../..";
export default {
title: "ds-react/Primitives/Bleed",
@@ -0,0 +1,271 @@
import bgColors from "@navikt/ds-tokens/src/colors-bg.json";
import type { Meta, StoryFn } from "@storybook/react";
import React, { useEffect } from "react";
import { Box } from "../box";
import { HGrid } from "../grid";
import Page from "./Page";
import { widths } from "./parts/PageBlock";
const meta: Meta = {
title: "ds-react/Primitives/Page",
component: Page,
parameters: {
layout: "fullscreen",
},
};
export default meta;
export const Default: StoryFn = ({
belowFold,
background,
width,
gutters,
contentBlockPadding,
}) => (
<Page
footer={<Footer width={width} gutters={gutters} />}
footerPosition={belowFold ? "belowFold" : undefined}
background={background}
contentBlockPadding={contentBlockPadding}
>
<Header width={width} gutters={gutters} />
<Content width={width} gutters={gutters} />
</Page>
);
Default.argTypes = {
width: {
control: "radio",
options: widths,
},
background: {
control: "radio",
options: Object.keys(bgColors.a),
},
belowFold: {
control: "boolean",
},
gutters: {
control: "boolean",
},
contentBlockPadding: {
control: "radio",
options: ["end", "none"],
},
};
Default.args = {
belowFold: false,
gutters: false,
contentBlockPadding: "end",
};
export const BelowFold: StoryFn = () => (
<Page
footer={<Footer />}
footerPosition="belowFold"
contentBlockPadding="end"
>
<Header />
<Content />
</Page>
);
export const Background: StoryFn = () => (
<HGrid columns={2} gap="4">
<Page
background="bg-default"
footer={<div>footer</div>}
contentBlockPadding="end"
>
<div>header</div>
<div>content</div>
</Page>
<Page
background="bg-subtle"
footer={<div>footer</div>}
contentBlockPadding="end"
>
<div>header</div>
<div>content</div>
</Page>
</HGrid>
);
export const ContentBlockPadding: StoryFn = () => (
<HGrid columns={2} gap="6" align="start">
<Page
footer={<Footer width="lg" gutters />}
contentBlockPadding="end"
background="bg-subtle"
>
<Header width="lg" gutters />
<Page.Block width="lg" gutters as="main">
<Box background="surface-alt-3-subtle" style={{ height: "80vh" }}>
Main
</Box>
</Page.Block>
</Page>
<Page
footer={<Footer width="lg" gutters />}
contentBlockPadding="none"
background="bg-subtle"
>
<Header width="lg" gutters />
<Page.Block width="lg" gutters as="main">
<Box background="surface-alt-3-subtle" style={{ height: "80vh" }}>
Main
</Box>
</Page.Block>
</Page>
</HGrid>
);
export const Gutters: StoryFn = () => (
<HGrid columns={2} gap="6" align="start">
<Page footer={<Footer width="lg" gutters />} background="bg-subtle">
<Header width="lg" gutters />
<Page.Block width="lg" gutters as="main">
<Box background="surface-alt-3-subtle" style={{ height: "80vh" }}>
Main
</Box>
</Page.Block>
</Page>
<Page footer={<Footer width="lg" />} background="bg-subtle">
<Header width="lg" />
<Page.Block width="lg" as="main">
<Box background="surface-alt-3-subtle" style={{ height: "80vh" }}>
Main
</Box>
</Page.Block>
</Page>
</HGrid>
);
Gutters.parameters = {
chromatic: {
modes: {
mobile_portrait: {
viewport: {
width: 400,
height: 850,
},
},
desktop: {
viewport: {
width: 1280,
height: 960,
},
},
},
},
};
const MILJO_URL = "https://www.nav.no/dekoratoren";
export const WithDecorator: StoryFn = () => {
return (
<Page
contentBlockPadding="end"
footerPosition="belowFold"
footer={<div id="decorator-footer"></div>}
>
<div id="decorator-header"></div>
<Content width="lg" gutters />
<div
id="decorator-env"
data-src={`${MILJO_URL}/env?context=privatperson`}
></div>
</Page>
);
};
WithDecorator.decorators = [
(Story) => {
useEffect(() => {
const script = document.createElement("script");
script.src = `${MILJO_URL}/client.js`;
script.async = true;
document.body.appendChild(script);
const styles = document.createElement("link");
styles.href = `${MILJO_URL}/css/client.css`;
styles.rel = "stylesheet";
document.head.appendChild(styles);
return () => {
document.body.removeChild(script);
document.head.removeChild(styles);
};
}, []);
return <Story />;
},
];
WithDecorator.parameters = {
chromatic: {
disable: true,
},
};
export const OutsideBackground = () => {
return (
<Page
contentBlockPadding="end"
footer={
<Box
background="surface-alt-3-subtle"
style={{ height: 100 }}
as="footer"
>
<Page.Block width="lg" gutters>
Footer
</Page.Block>
</Box>
}
>
<Box background="surface-alt-1-subtle" style={{ height: 64 }} as="header">
<Page.Block width="lg" gutters>
Header
</Page.Block>
</Box>
<Box background="surface-alt-2-subtle" style={{ height: 300 }} as="main">
<Page.Block width="lg" gutters>
main
</Page.Block>
</Box>
</Page>
);
};
function Header({ width = "lg", gutters = false }: any) {
return (
<Page.Block as="header" width={width} gutters={gutters}>
<Box background="surface-alt-3-subtle" style={{ height: 64 }}>
Header
</Box>
</Page.Block>
);
}
function Content({ width = "lg", gutters = false }: any) {
return (
<Page.Block width={width} gutters={gutters} as="main">
<Box background="surface-alt-3-subtle" style={{ height: 300 }}>
Main
</Box>
</Page.Block>
);
}
function Footer({ width = "lg", gutters = false }: any) {
return (
<Page.Block width={width} gutters={gutters} as="footer">
<Box background="surface-alt-3-subtle" style={{ height: 100 }}>
Footer
</Box>
</Page.Block>
);
}
+115
View File
@@ -0,0 +1,115 @@
import bgColors from "@navikt/ds-tokens/src/colors-bg.json";
import cl from "clsx";
import React, { forwardRef } from "react";
import { OverridableComponent } from "../../util";
import { PageBlock } from "./parts/PageBlock";
export interface PageProps extends React.HTMLAttributes<HTMLElement> {
/**
* Overrides html-tag
* @default "div"
*/
as?: "div" | "body";
/**
* Background color. Accepts a color token.
* @default bg-default
*/
background?: keyof typeof bgColors.a;
/**
* Allows better positioning of footer
*/
footer?: React.ReactNode;
/**
* Places footer below page-fold
*/
footerPosition?: "belowFold";
/**
* Adds a standardised padding of 4rem between content and footer
* @default block-end
*/
contentBlockPadding?: "end" | "none";
}
interface PageComponentType
extends OverridableComponent<PageProps, HTMLElement> {
Block: typeof PageBlock;
}
export const PageComponent: OverridableComponent<PageProps, HTMLElement> =
forwardRef(
(
{
as: Component = "div",
className,
style: _style,
footer,
children,
footerPosition,
background = "bg-default",
contentBlockPadding = "end",
...rest
},
ref
) => {
const style: React.CSSProperties = {
..._style,
"--__ac-page-background": `var(--a-${background})`,
};
const belowFold = footerPosition === "belowFold";
return (
<Component
{...rest}
className={cl("navds-page", className)}
ref={ref}
style={style}
>
<div
className={cl({
"navds-page__content--fullheight": belowFold,
"navds-page__content--grow": !belowFold,
"navds-page__content--padding": contentBlockPadding === "end",
})}
>
{children}
</div>
{footer}
</Component>
);
}
);
/**
* Page helps with establishing a top-level layout for your page
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/primitives/page)
* @see 🏷️ {@link PageProps}
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
*
* @example
* ```jsx
* <Page
* footer={<Page.Block width="xl" gutters />}
* >
* <Page.Block width="xl" gutters />// Header
* <Page.Block width="xl" gutters />// Content
* </Page>
* ```
* @example
* Footer placed below page-fold
* ```jsx
* <Page
* footer={<Page.Block width="xl" gutters />}
* footerPosition="belowFold"
* >
* <Page.Block width="xl" gutters />// Header
* <Page.Block width="xl" gutters />// Content
* </Page>
* ```
*/
const Page = PageComponent as PageComponentType;
Page.Block = PageBlock;
export default Page;
@@ -0,0 +1,2 @@
export { default as Page, type PageProps } from "./Page";
export { type PageBlockProps } from "./parts/PageBlock";
@@ -0,0 +1,72 @@
import cl from "clsx";
import React, { forwardRef } from "react";
import { OverridableComponent } from "../../../util/OverridableComponent";
export const widths = ["lg", "xl", "2xl"] as const;
export interface PageBlockProps extends React.HTMLAttributes<HTMLDivElement> {
/**
* Predefined max-width
* @example
* lg: 1024px
* xl: 1280px
* 2xl: 1440px
* @default max-width: 100%;
*/
width?: (typeof widths)[number];
/**
* Adds a standardised responsive padding-inline
* @example
* 3rem on > md
* 1rem on < md
* @default false
*/
gutters?: boolean;
}
/**
* Acts as a top-level container for defining max-width, gutters and horizontal centering
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/primitives/page)
* @see 🏷️ {@link PageBlockProps}
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
*
* @example
* ```jsx
* <Page
* footer={<Page.Block width="xl" gutters />}
* >
* <Page.Block width="xl" gutters />// Header
* <Page.Block width="xl" gutters />// Content
* </Page>
* ```
* @example
* With background bleed
* Wrapping `Page.Block` with `Box` allows the background-color to use full screen-width
* ```jsx
* <Page
* footer={<Box background="..."><Page.Block width="xl" gutters /></Box>}
* footerPosition="belowFold"
* >
* <Box background="..."><Page.Block width="xl" gutters /></Box>//Header
* <Box background="..."><Page.Block width="xl" gutters /></Box>//Content
* </Page>
* ```
*/
export const PageBlock: OverridableComponent<PageBlockProps, HTMLDivElement> =
forwardRef(
({ as: Component = "div", gutters, className, width, ...rest }, ref) => {
return (
<Component
{...rest}
className={cl(
"navds-pageblock",
`navds-pageblock--${width}`,
className,
{ "navds-pageblock--gutters": gutters }
)}
ref={ref}
/>
);
}
);
+4 -1
View File
@@ -8,6 +8,7 @@ import React, {
useRef,
} from "react";
import { createPortal } from "react-dom";
import { DateContext } from "../date/context";
import { useProvider } from "../provider";
import { Detail, Heading } from "../typography";
import { mergeRefs, useId } from "../util";
@@ -99,7 +100,9 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
const rootElement = useProvider()?.rootElement;
const portalNode = useFloatingPortalNode({ root: rootElement });
if (useContext(ModalContext)) {
const dateContext = useContext(DateContext);
const modalContext = useContext(ModalContext);
if (modalContext && !dateContext) {
console.error("Modals should not be nested");
}
+93 -21
View File
@@ -1,17 +1,19 @@
import { FileIcon } from "@navikt/aksel-icons";
import { Meta, StoryFn } from "@storybook/react";
import React, { useRef, useState } from "react";
import { BodyLong, Button, Heading, Tooltip } from "..";
import Modal from "./Modal";
export default {
const meta: Meta<typeof Modal> = {
title: "ds-react/Modal",
component: Modal,
parameters: {
chromatic: { pauseAnimationAtEnd: true },
},
};
export default meta;
export const WithUseRef = () => {
export const WithUseRef: StoryFn = () => {
const ref = useRef<HTMLDialogElement>(null);
const ref2 = useRef<HTMLDialogElement>(null);
@@ -87,7 +89,7 @@ export const WithUseRef = () => {
};
WithUseRef.storyName = "With useRef";
export const WithUseState = () => {
export const WithUseState: StoryFn = () => {
const [open, setOpen] = useState(true);
const [open2, setOpen2] = useState(false);
@@ -140,36 +142,43 @@ export const WithUseState = () => {
};
WithUseState.storyName = "With useState";
export const EmptyHeader = () => (
<div style={{ minWidth: "800px", minHeight: "600px" } /* For Chromatic */}>
<Modal open>
<Modal.Header />
<Modal.Body>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia deserunt
mollit anim id est laborum.
</Modal.Body>
</Modal>
</div>
export const EmptyHeader: StoryFn = () => (
<Modal open>
<Modal.Header />
<Modal.Body>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod
tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim
veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea
commodo consequat. Duis aute irure dolor in reprehenderit in voluptate
velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat
cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id
est laborum.
</Modal.Body>
</Modal>
);
export const Small = () => (
export const Small: StoryFn = () => (
<Modal open width="small" header={{ heading: "Simple header" }}>
<Modal.Body>Lorem ipsum dolor sit amet.</Modal.Body>
</Modal>
);
Small.storyName = "Size = Small";
export const MediumWithPortal = () => (
export const MediumWithPortal: StoryFn = () => (
<Modal open portal width="medium" header={{ heading: "Simple header" }}>
<Modal.Body>Lorem ipsum dolor sit amet.</Modal.Body>
</Modal>
);
MediumWithPortal.storyName = "Size = Medium (with portal)";
export const WithTooltip = () => {
export const Large800: StoryFn = () => (
<Modal open width={800} header={{ heading: "Simple header" }}>
<Modal.Body>Lorem ipsum dolor sit amet.</Modal.Body>
</Modal>
);
Large800.storyName = "Size = 800px";
export const WithTooltip: StoryFn = () => {
const ref = useRef<HTMLDialogElement>(null);
return (
@@ -193,3 +202,66 @@ export const WithTooltip = () => {
</div>
);
};
export const ChromaticViewportTesting: StoryFn = () => (
<div id="modal-story-wrapper" style={{ width: "100vw", height: "100vh" }}>
<style>{`#storybook-root { padding: 0 !important }`}</style>
<Modal
open
header={{ heading: "Chromatic Viewports Testing", label: "Test" }}
>
<Modal.Body>
<BodyLong spacing>
This story is tailored for testing the breakpoints with Chromatic.
</BodyLong>
<BodyLong spacing>
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do
eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad
minim veniam, quis nostrud exercitation ullamco laboris nisi ut
aliquip ex ea commodo consequat.
</BodyLong>
<BodyLong spacing>
Duis aute irure dolor in reprehenderit in voluptate velit esse cillum
dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non
proident, sunt in culpa qui officia deserunt mollit anim id est
laborum.
</BodyLong>
<BodyLong>
Sed ut perspiciatis unde omnis iste natus error sit voluptatem
accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae
ab illo inventore veritatis et quasi architecto beatae vitae dicta
sunt explicabo.
</BodyLong>
</Modal.Body>
<Modal.Footer>
<Button>Primary</Button>
<Button variant="secondary">Secondary</Button>
<Button variant="tertiary">Tertiary</Button>
</Modal.Footer>
</Modal>
</div>
);
ChromaticViewportTesting.parameters = {
chromatic: {
modes: {
mobile_portrait: {
viewport: {
width: 400,
height: 850,
},
},
mobile_landscape: {
viewport: {
width: 850,
height: 400,
},
},
desktop: {
viewport: {
width: 1280,
height: 960,
},
},
},
},
};
+4 -12
View File
@@ -18,6 +18,7 @@ import React, {
useMemo,
useRef,
} from "react";
import { DateContext } from "../date/context";
import { ModalContext } from "../modal/ModalContext";
import { mergeRefs, useClientLayoutEffect, useEventListener } from "../util";
import PopoverContent, { PopoverContentType } from "./PopoverContent";
@@ -73,11 +74,6 @@ export interface PopoverProps extends HTMLAttributes<HTMLDivElement> {
* @default "absolute"
*/
strategy?: "absolute" | "fixed";
/**
* Bubbles Escape keydown-event up trough DOM-tree. This is set to false by default to prevent closing components like Modal on Escape
* @default false
*/
bubbleEscape?: boolean;
/**
* Changes placement of the floating element in order to keep it in view.
* @default true
@@ -124,7 +120,6 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
placement = "top",
offset,
strategy: userStrategy,
bubbleEscape = false,
flip: _flip = true,
...rest
},
@@ -132,8 +127,9 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
) => {
const arrowRef = useRef<HTMLDivElement | null>(null);
const isInModal = useContext(ModalContext) !== null;
const isInDatepicker = useContext(DateContext) !== null;
const chosenStrategy = userStrategy ?? (isInModal ? "fixed" : "absolute");
const chosenFlip = isInModal ? true : _flip;
const chosenFlip = isInDatepicker ? false : _flip;
const {
x,
@@ -160,11 +156,7 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
const { getFloatingProps } = useInteractions([
useClick(context),
useDismiss(context, {
bubbles: {
escapeKey: bubbleEscape,
},
}),
useDismiss(context),
]);
useClientLayoutEffect(() => {
+17 -5
View File
@@ -81,11 +81,8 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
e.stopPropagation();
};
const onRowClick = (e) => {
if (e.target.nodeName === "TD" || e.target.nodeName === "TH") {
expansionHandler(e);
}
};
const onRowClick = (e) =>
!isInteractiveTarget(e.target) && expansionHandler(e);
return (
<>
@@ -143,4 +140,19 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
}
);
function isInteractiveTarget(elm: HTMLElement) {
if (elm.nodeName === "TD" || elm.nodeName === "TH" || !elm.parentElement) {
return false;
}
if (
["BUTTON", "DETAILS", "LABEL", "SELECT", "TEXTAREA", "INPUT", "A"].includes(
elm.nodeName
)
) {
return true;
}
return isInteractiveTarget(elm.parentElement);
}
export default ExpandableRow;
@@ -0,0 +1,100 @@
import { expect, jest } from "@storybook/jest";
import { userEvent, within } from "@storybook/testing-library";
import React from "react";
import { Table } from "../..";
export default {
title: "ds-react/Table/Tests",
component: Table,
};
export const ClickableRowTest = {
render: ({ onOpenChange }) => {
return (
<>
<Table zebraStripes>
<Table.Header>
<Table.Row>
<Table.HeaderCell aria-hidden />
<Table.HeaderCell aria-hidden />
<Table.HeaderCell aria-hidden />
<Table.HeaderCell aria-hidden />
<Table.HeaderCell aria-hidden />
<Table.HeaderCell aria-hidden />
</Table.Row>
</Table.Header>
<Table.Body>
<Table.ExpandableRow
content={<div>placeholder row 2</div>}
togglePlacement="right"
data-testid="row1"
expandOnRowClick
onOpenChange={onOpenChange}
>
<Table.DataCell>
<div data-testid="cell1">Should be clickable</div>
</Table.DataCell>
<Table.DataCell data-testid="cell2">
Should also be clickable
</Table.DataCell>
<Table.DataCell>
<button data-testid="cell3">Should not be clickable</button>
</Table.DataCell>
<Table.DataCell>
<div>
<div>
<button data-testid="cell4">
Nested should not be clickable
</button>
</div>
</div>
</Table.DataCell>
<Table.DataCell>
<div>
<div>
<button data-testid="cell4">
<span>2x nested should not be clickable</span>
</button>
</div>
</div>
</Table.DataCell>
</Table.ExpandableRow>
</Table.Body>
</Table>
</>
);
},
args: {
onOpenChange: jest.fn(),
},
play: async ({ canvasElement, args }) => {
args.onOpenChange.mockClear();
const canvas = within(canvasElement);
const cell1 = canvas.getByText("Should be clickable");
const cell2 = canvas.getByText("Should also be clickable");
const cell3 = canvas.getByText("Should not be clickable");
const cell4 = canvas.getByText("Nested should not be clickable");
const cell5 = canvas.getByText("2x nested should not be clickable");
await userEvent.click(cell1);
expect(args.onOpenChange.mock.calls).toHaveLength(1);
await userEvent.click(cell1);
expect(args.onOpenChange.mock.calls).toHaveLength(2);
await userEvent.click(cell2);
expect(args.onOpenChange.mock.calls).toHaveLength(3);
await userEvent.click(cell2);
expect(args.onOpenChange.mock.calls).toHaveLength(4);
await userEvent.click(cell3);
expect(args.onOpenChange.mock.calls).toHaveLength(4);
await userEvent.click(cell4);
expect(args.onOpenChange.mock.calls).toHaveLength(4);
await userEvent.click(cell5);
expect(args.onOpenChange.mock.calls).toHaveLength(4);
},
};
@@ -1,11 +1,28 @@
import React from "react";
/**
* This interface represents a component that can be overridden with different props and elements.
* @template Component The type of the props for the component.
* @template Element The type of the HTML element that the component renders.
*/
export interface OverridableComponent<Component, Element extends HTMLElement> {
/**
* This is a function type that takes props and returns a React functional component.
* @param props The props for the component, which are a combination of the Component type and React.RefAttributes for the Element type.
* @returns A React functional component.
*/
(props: Component & React.RefAttributes<Element>): ReturnType<React.FC>;
/**
* This is a function type that takes props and returns a React functional component.
* It allows for the 'as' prop to override the type of element that the component renders.
* @template As The type of the element that the component should render as.
* @param props The props for the component, which are a combination of the Component type, the 'as' prop, and any other props that are not part of the Component type or the 'as' prop.
* @returns A React functional component.
*/
<As extends React.ElementType>(
props: {
as: As;
as?: As;
} & Component &
Omit<React.ComponentPropsWithRef<As>, keyof Component | "as">
): ReturnType<React.FC>;
@@ -0,0 +1,19 @@
import { render, screen } from "@testing-library/react";
import React from "react";
import { useMedia } from "../useMedia";
function TestComponent({ fallback }: { fallback?: boolean }) {
const media = useMedia("screen and (min-width: 1024px)", fallback);
return <div data-testid="media-id">{`${media}`}</div>;
}
describe("useMedia", () => {
test("Should return 'undefined' when no fallback is given", async () => {
render(<TestComponent />);
expect(screen.getByTestId("media-id").innerHTML).toEqual("undefined");
});
test("Should return fallback", async () => {
render(<TestComponent fallback={true} />);
expect(screen.getByTestId("media-id").innerHTML).toEqual("true");
});
});
+38
View File
@@ -0,0 +1,38 @@
import { useEffect, useState } from "react";
export const noMatchMedia =
typeof window !== "undefined" && window.matchMedia === undefined;
/**
* @example useMedia("screen and (min-width: 1024px)")
* @param media string
* @param fallback boolean
* @returns boolean | undefined
*/
export const useMedia = (
media: string,
fallback?: boolean
): boolean | undefined => {
const [matches, setMatches] = useState(fallback);
useEffect(() => {
if (noMatchMedia) {
return;
}
const mediaQueryList = window.matchMedia(media);
setMatches(mediaQueryList.matches);
const listener = (evt: MediaQueryListEvent) => {
setMatches(evt.matches);
};
mediaQueryList.addEventListener("change", listener);
return () => {
mediaQueryList.removeEventListener("change", listener);
};
}, [media]);
return matches;
};
+12
View File
@@ -1,5 +1,17 @@
# @navikt/ds-tailwind
## 5.10.2
## 5.10.1
## 5.10.0
## 5.9.2
## 5.9.1
## 5.9.0
## 5.8.0
## 5.7.6
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tailwind",
"version": "5.8.0",
"version": "5.10.2",
"description": "Tailwind config based on @navikt/ds-tokens",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -22,7 +22,7 @@
"directory": "@navikt/core/tailwind"
},
"devDependencies": {
"@navikt/ds-tokens": "^5.8.0",
"@navikt/ds-tokens": "^5.10.2",
"@types/jest": "^29.0.0",
"color": "4.2.3",
"jest": "^29.0.0",
+12
View File
@@ -1,5 +1,17 @@
# @navikt/ds-tokens
## 5.10.2
## 5.10.1
## 5.10.0
## 5.9.2
## 5.9.1
## 5.9.0
## 5.8.0
## 5.7.6
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tokens",
"version": "5.8.0",
"version": "5.10.2",
"description": "Design-tokens for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
+436 -834
View File
File diff suppressed because it is too large. Load diff
+1 -1
View File
@@ -4,7 +4,7 @@ ENV NODE_ENV production
WORKDIR /app
COPY aksel.nav.no/website/.next/standalone ./
COPY aksel.nav.no/website/next-logger.config.js ./
COPY aksel.nav.no/website/config/next-logger.config.js ./
ENV NODE_OPTIONS '-r next-logger'
+1 -1
View File
@@ -19,7 +19,7 @@ Det publiserte studioet finner man på [aksel.nav.no/admin](https://aksel.nav.no
### Bidrag
Branch `main` blir pusher rett til prod -> aksel.nav.no. For å utvikle nye features kan branch `next` brukes, som ved push publiseres til devmiljø https://aksel.ekstern.dev.nav.no/. Kan også publisere andre branches til dev ved bruk av workflow-dispatch.
Branch `main` pusher rett til prod -> aksel.nav.no. For å utvikle nye features kan branch `next` brukes, som ved push publiseres til devmiljø https://aksel.ekstern.dev.nav.no/. Kan også publisere andre branches til dev ved bruk av workflow-dispatch.
## Kode-eksempler
+7 -6
View File
@@ -12,14 +12,15 @@
"test": "yarn workspace website test"
},
"workspaces": [
"website"
"website",
"playroom"
],
"dependencies": {
"@navikt/aksel-icons": "^5.8.0",
"@navikt/ds-css": "^5.8.0",
"@navikt/ds-react": "^5.8.0",
"@navikt/ds-tailwind": "^5.8.0",
"@navikt/ds-tokens": "^5.8.0"
"@navikt/aksel-icons": "^5.10.2",
"@navikt/ds-css": "^5.10.2",
"@navikt/ds-react": "^5.10.2",
"@navikt/ds-tailwind": "^5.10.2",
"@navikt/ds-tokens": "^5.10.2"
},
"devDependencies": {
"prettier-plugin-tailwindcss": "^0.2.3"
+5
View File
@@ -0,0 +1,5 @@
# playroom
https://github.com/seek-oss/playroom
This is the playroom / code sandbox that is built as a standalone static app, and then it's added to the aksel website through an iframe.
+49
View File
@@ -0,0 +1,49 @@
/**
* @type {import('@babel/core').TransformOptions}
*/
module.exports = function (api) {
const envName = api.env();
const development = envName === "development" || envName === "test";
return {
presets: [
[
"@babel/preset-env",
{ useBuiltIns: "entry", corejs: "3.0", bugfixes: true },
],
["@babel/preset-typescript"],
["@babel/preset-react", { development, useBuiltIns: true }],
],
assumptions: {
setPublicClassFields: true,
privateFieldsAsProperties: true,
// nothing accesses `document.all`:
noDocumentAll: true,
// nothing relies on class constructors invoked without `new` throwing:
noClassCalls: true,
// nothing should be relying on tagged template strings being frozen:
mutableTemplateObject: true,
// nothing is relying on Function.prototype.length:
ignoreFunctionLength: true,
// nothing is relying on mutable re-exported bindings:
constantReexports: true,
// don't bother marking Module records non-enumerable:
enumerableModuleMeta: true,
// nothing uses [[Symbol.toPrimitive]]:
// (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive)
ignoreToPrimitiveHint: true,
// nothing relies on spread copying Symbol keys: ({...{ [Symbol()]: 1 }})
objectRestNoSymbols: true,
// nothing relies on `new (() => {})` throwing:
noNewArrows: true,
// transpile object spread to assignment instead of defineProperty():
setSpreadProperties: true,
// nothing should be using custom iterator protocol:
skipForOfIteratorClosing: true,
// nothing inherits from a constructor function with explicit return value:
superIsCallableConstructor: true,
// nothing relies on CJS-transpiled namespace imports having all properties prior to module execution completing:
noIncompleteNsImportDetection: true,
},
babelrcRoots: ["."],
};
};
+22
View File
@@ -0,0 +1,22 @@
{
"name": "playroom",
"scripts": {
"start": "playroom start",
"build:playroom": "playroom build"
},
"devDependencies": {
"@babel/preset-env": "^7.23.2",
"@babel/preset-react": "^7.22.15",
"@babel/preset-typescript": "^7.23.2",
"@navikt/ds-css": "^5.9.2",
"@navikt/ds-react": "^5.9.2",
"babel-loader": "^9.1.3",
"babel-plugin-preval": "^5.1.0",
"css-loader": "^6.8.1",
"playroom": "^0.32.1",
"style-loader": "^3.3.3"
},
"dependencies": {
"react": "^18.0.0"
}
}
+61
View File
@@ -0,0 +1,61 @@
// Note: Hot module reloading for dev has been turned off for this environment
// Why? We embed playroom in an iframe, and that iframe is loaded from the public asset folder
// So we need to build into that public asset folder. Hence no dev mode and HMR
// Workaround: restart the next dev server after each change to the playroom config and associated files.
const path = require("path");
module.exports = {
baseUrl: "./",
components: path.resolve("./src/components.ts"),
outputPath: path.resolve("./dist"),
// Optional:
title: "Aksel Sandbox",
snippets: path.resolve("./src/snippets.ts"),
widths: [480, 768, 1024, 1280],
port: 9000,
frameComponent: path.resolve("./src/FrameComponent.tsx"),
useScope: path.resolve("./src/useScope.tsx"),
openBrowser: false,
paramType: "search", // default is 'hash'
iframeSandbox: "allow-scripts allow-same-origin",
exampleCode: `
<Heading>This is a sandbox!</Heading>
<HStack gap="10">
<Box> box 1</Box> <Box> box 2</Box> <Box> box 3</Box>
</HStack>
<Button>test</Button>
<BodyShort>This is a bodyshort</BodyShort>
`,
webpackConfig: () => ({
module: {
rules: [
{
test: /\.(js|ts|tsx)$/,
include: [path.resolve("./")],
use: [
{
loader: require.resolve("babel-loader"),
options: {
presets: [
[
require.resolve("@babel/preset-env"),
{ shippedProposals: true },
],
require.resolve("@babel/preset-react"),
require.resolve("@babel/preset-typescript"),
],
plugins: [require.resolve("babel-plugin-preval")],
},
},
],
},
{
test: /\.css$/i,
include: [require.resolve("@navikt/ds-css")],
use: ["style-loader", "css-loader"],
},
],
},
}),
};
@@ -0,0 +1,8 @@
import "@navikt/ds-css";
import * as React from "react";
const FrameComponent = ({ children }: { children: React.ReactNode }) => {
return <div id="sandbox-wrapper">{children}</div>;
};
export default FrameComponent;
+2
View File
@@ -0,0 +1,2 @@
export * from "@navikt/aksel-icons";
export * from "@navikt/ds-react";
+29
View File
@@ -0,0 +1,29 @@
const snippets = [
{
group: "Templates",
name: "Single Component",
code: `<Button variant="primary">Button</Button>`,
},
{
group: "Templates",
name: "Multiple Components",
code: ` <Button variant="primary">First</Button>
<Button>Second</Button>`,
},
{
group: "Templates",
name: "Component with state",
code: `{
(() => {
const [count, setCount] = React.useState(0);
return (
<Button variant="primary" onClick={() => setCount(count + 1)}>Count: {count}</Button>
)
})()
}
`,
},
];
export default snippets;
+2
View File
@@ -0,0 +1,2 @@
public/sandbox
test-results/
+3 -4
View File
@@ -1,8 +1,7 @@
import type { StorybookConfig } from "@storybook/nextjs";
import { readFileSync } from "fs";
import { join, dirname } from "path";
import { loadCsf } from "@storybook/csf-tools";
import type { StorybookConfig } from "@storybook/nextjs";
import { readFileSync } from "fs";
import { dirname, join } from "path";
/**
* This function is used to resolve the absolute path of a package.
@@ -199,3 +199,20 @@ export const StorybookIcon = ({ ...props }) => (
</g>
</svg>
);
export const CodeSandboxLogo = ({ ...props }) => (
<svg
width="24"
height="24"
fill="none"
viewBox="8 3 24 24"
aria-hidden="true"
focusable="false"
{...props}
>
<path
fill="currentColor"
d="M20.54 25a.681.681 0 00.158-.074l6.174-3.69c.734-.438 1.128-.885 1.128-2.084v-6.444c0-.248-.016-.447-.063-.629l-7.397 4.434zm-8.477-12.921l7.397 4.434V25a.514.514 0 01-.15-.074l-6.182-3.69c-.726-.438-1.128-.885-1.128-2.084v-6.444c0-.248.016-.447.063-.629zM20.004 7c.443 0 .9.143 1.359.406l.068.04 5.567 3.326c.155.088.284.177.394.282l.04.04-7.428 4.426-7.436-4.425c.11-.109.233-.198.378-.286l.063-.037 4.07-2.442h.002l1.488-.883c.434-.262.868-.415 1.296-.443l.071-.003z"
></path>
</svg>
);
@@ -1,6 +1,6 @@
import React from "react";
const Logo = (props) => (
const AkselLogo = (props) => (
<svg
width="119"
height="26"
@@ -43,4 +43,4 @@ const Logo = (props) => (
</svg>
);
export default Logo;
export default AkselLogo;
@@ -1 +0,0 @@
export * from "./Icons";
@@ -1,5 +1,5 @@
import { dateStr } from "components/website-modules/utils/date-string";
import { useEffect, useState } from "react";
import { dateStr } from "@/utils";
export const useFormatedDate = (date: string) => {
const [_date, setDate] = useState(null);
@@ -8,5 +8,6 @@ export const useFormatedDate = (date: string) => {
const handleDate = async () => setDate(await dateStr(date));
handleDate();
}, [date]);
return _date;
};
@@ -2,7 +2,7 @@ import { useEffect } from "react";
// https://stackoverflow.com/questions/38588346/anchor-a-tags-not-working-in-chrome-when-using/38588927#38588927
// https://github.com/vercel/next.js/discussions/13134
export function useScrollToHashOnPageLoad(): void {
export function useHashScroll(): void {
useEffect(() => {
if (window.location.hash) {
const hash = window.location.hash;
@@ -13,9 +13,3 @@ export function useScrollToHashOnPageLoad(): void {
}
}, []);
}
export const removeEmojies = (s: string) =>
s.replace(
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
""
);
@@ -0,0 +1,13 @@
import { createContext, useContext } from "react";
export const SanityDocIdContext = createContext<{ id?: string }>(null);
export function useSanityDocId() {
const ctx = useContext(SanityDocIdContext);
if (!ctx) {
console.error(
"useSanityDocId has to be used inside SanityDocIdContext.provider"
);
}
return ctx;
}
-22
View File
@@ -1,22 +0,0 @@
export * from "./assets";
export { default as Accordion } from "./sanity-modules/Accordion";
export { default as Alert } from "./sanity-modules/Alert";
export { default as Bilde } from "./sanity-modules/Bilde";
export * from "./sanity-modules/cards";
export * from "./sanity-modules/code";
export * from "./sanity-modules/code-examples";
export { default as DoDont } from "./sanity-modules/DoDont";
export { default as IntroKomponent } from "./sanity-modules/IntroSeksjon";
export { default as PropsSeksjon } from "./sanity-modules/props/PropsSeksjon";
export { default as PropTable } from "./sanity-modules/props/PropTabell";
export { default as SideModul } from "./sanity-modules/SideModul";
export { default as Tabell } from "./sanity-modules/Tabell";
export { default as TastaturModul } from "./sanity-modules/TastaturTabell";
export { default as Tips } from "./sanity-modules/Tips";
export { default as TokenTable } from "./sanity-modules/TokenTable";
export * from "./sanity-modules/UuFeedback";
export { default as Video } from "./sanity-modules/Video";
export * from "./website-modules/BreadCrumbs";
export { default as Feedback } from "./website-modules/feedback";
export * from "./website-modules/toc/TOC";
export * from "./website-modules/utils";
@@ -1,18 +1,13 @@
import { FigmaIcon, GithubIcon, SlackIcon } from "@/assets/Icons";
import AkselLogo from "@/assets/Logo";
import { amplitudeLogNavigation } from "@/logging";
import { BodyShort, Heading, Link } from "@navikt/ds-react";
import { FigmaIcon, GithubIcon, SlackIcon } from "components/assets";
import Logo from "components/assets/Logo";
import dynamic from "next/dynamic";
/* import FooterForm from "./FooterForm"; */
export const EditButton = dynamic(
() => import("components/website-modules/EditButton"),
{
ssr: false,
}
);
export const EditButton = dynamic(() => import("@/web/EditButton"), {
ssr: false,
});
/* FooterForm is disabled to better comply with policy */
const Footer = () => {
return (
<footer
@@ -26,7 +21,6 @@ const Footer = () => {
<Snarveier />
<SideLenker />
<Kontakt />
{/* <FooterForm /> */}
</div>
<EditButton />
</footer>
@@ -36,7 +30,7 @@ const Footer = () => {
function LogoBlock() {
return (
<div>
<Logo className="fill-white" />
<AkselLogo className="fill-white" />
<p className="mt-4">&copy; {new Date().getFullYear()} NAV</p>
<p>Arbeids- og velferdsetaten</p>
</div>
@@ -1,104 +0,0 @@
import { Button, Label, Textarea } from "@navikt/ds-react";
import { IdContext } from "components/website-modules/utils/contexts/id-context";
import { FeedbackT } from "@/types";
import { useRouter } from "next/router";
import { useContext, useEffect, useState } from "react";
const FooterForm = () => {
const idCtx = useContext(IdContext);
const [contactForm, setContactForm] = useState({
content: "",
hasWritten: false,
});
const [contentError, setContentError] = useState({ content: "" });
const [sent, setSent] = useState({ status: false });
const { asPath, basePath } = useRouter();
const handleSubmit = (e) => {
e.preventDefault();
let fail = false;
if (!contactForm?.content || contactForm?.content.trim().length === 0) {
setContentError({
...contentError,
content: "Melding kan ikke være tom. Fyll inn meldingen.",
});
fail = true;
}
if (fail) return;
setContentError({ content: "" });
const body: FeedbackT = {
message: contactForm.content,
url: `${basePath}${asPath}`,
type: "footer",
docId: idCtx?.id,
};
fetch("/api/feedback", {
method: "POST",
body: JSON.stringify(body),
});
setSent({ status: true });
setContactForm({ content: "", hasWritten: false });
};
useEffect(() => {
setSent({ status: false });
setContactForm({ content: "", hasWritten: false });
setContentError({ content: "" });
}, [asPath]);
return (
<>
<div
className="flex w-full max-w-md flex-col lg:col-span-2 xl:col-span-1"
data-theme="dark"
>
<div aria-live="polite">
{sent.status && (
<Label spacing as="p">
Meldingen er sendt til Aksel, takk for tilbakemeldingen!
</Label>
)}
</div>
{!sent.status && (
<form onSubmit={(e) => handleSubmit(e)} className="w-full">
<div className="mb-3 flex flex-col gap-4">
<Textarea
className="textarea-override"
error={contentError.content}
autoComplete="off"
label="Gi oss en tilbakemelding"
value={contactForm.content}
onChange={(e) => {
setContactForm({
...contactForm,
content: e.target.value,
hasWritten: true,
});
e.target.value &&
e.target.value.trim().length !== 0 &&
setContentError({ ...contentError, content: "" });
}}
description="Ikke skriv inn personopplysninger"
minRows={2}
/>
</div>
<Button size="small">Send melding</Button>
<style>{`.textarea-override {
--ac-textarea-bg: var(--a-surface-default);
--ac-textarea-text: var(--a-text-default);
}`}</style>
</form>
)}
</div>
</>
);
};
export default FooterForm;
@@ -10,12 +10,12 @@ import { useState } from "react";
const LinkElement = ({ name, href, onClick }) => {
const { asPath } = useRouter();
return (
<li className="flex h-11 items-center">
<li className="my-2 flex h-11 items-center">
<Link
href={href}
passHref
className={cl(
"hover:bg-surface-action-subtle-hover focus-visible:shadow-focus-inset relative flex h-full w-full items-center rounded px-2 focus:outline-none",
"hover:bg-surface-action-subtle-hover focus-visible:shadow-focus relative flex h-full w-full items-center rounded px-2 focus:outline-none",
{
"before:bg-surface-action-selected pl-4 font-semibold before:absolute before:left-0 before:h-6 before:w-1 before:rounded-full":
asPath.startsWith(href),
@@ -58,6 +58,7 @@ export const Hamburger = () => {
onClose={() => setOpen(false)}
header={{ heading: "Meny" }}
width="small"
closeOnBackdropClick
>
<Modal.Body>
<nav aria-label="hovedmeny">
@@ -82,6 +83,11 @@ export const Hamburger = () => {
name="Komponenter"
href="/komponenter"
/>
<LinkElement
onClick={() => setOpen(false)}
name="Mønster & Maler"
href="/monster-maler"
/>
<LinkElement
onClick={() => setOpen(false)}
name="Blogg"
@@ -1,33 +1,36 @@
import AkselLogo from "@/assets/Logo";
import { amplitudeLogNavigation } from "@/logging";
import { MagnifyingGlassIcon } from "@navikt/aksel-icons";
import { Button } from "@navikt/ds-react";
import cl from "clsx";
import Logo from "components/assets/Logo";
import { Hamburger } from "components/layout/header/Hamburger";
import dynamic from "next/dynamic";
import Link from "next/link";
import { useRouter } from "next/router";
import { Hamburger } from "./Hamburger";
export const GlobalSearch = dynamic(() => import("./search"), {
loading: () => (
<Button
variant="primary"
className="hover:bg-deepblue-700 bg-deepblue-600 h-11"
aria-keyshortcuts="Control+b"
icon={
<MagnifyingGlassIcon
className="pointer-events-none -mt-[1px] shrink-0 text-2xl"
aria-label="Åpne meny"
aria-hidden
/>
}
iconPosition="left"
>
Søk
</Button>
),
ssr: false,
});
export const GlobalSearch = dynamic(
() => import("../../website-modules/search/Search"),
{
loading: () => (
<Button
variant="primary"
className="hover:bg-deepblue-700 bg-deepblue-600 h-11"
aria-keyshortcuts="Control+b"
icon={
<MagnifyingGlassIcon
className="pointer-events-none -mt-[1px] shrink-0 text-2xl"
aria-label="Åpne meny"
aria-hidden
/>
}
iconPosition="left"
>
Søk
</Button>
),
ssr: false,
}
);
const LinkElement = ({ name, href, prefetch = undefined }) => {
const { asPath } = useRouter();
@@ -55,7 +58,7 @@ const LinkElement = ({ name, href, prefetch = undefined }) => {
);
};
export const Header = ({
const Header = ({
variant = "default",
}: {
variant?: "blogg" | "subtle" | "default" | "transparent";
@@ -90,7 +93,7 @@ export const Header = ({
}
className="focus-visible:shadow-focus grid h-full place-items-center rounded px-2 focus:outline-none"
>
<Logo className="text-deepblue-800" />
<AkselLogo className="text-deepblue-800" />
<span className="sr-only">Aksel</span>
</Link>
</div>
@@ -103,6 +106,7 @@ export const Header = ({
<LinkElement name="Grunnleggende" href="/grunnleggende" />
<LinkElement name="Ikoner" href="/ikoner" prefetch={false} />
<LinkElement name="Komponenter" href="/komponenter" />
<LinkElement name="Mønster & Maler" href="/monster-maler" />
<LinkElement name="Bloggen" href="/produktbloggen" />
</ul>
</nav>
@@ -116,3 +120,5 @@ export const Header = ({
</>
);
};
export default Header;
@@ -1 +0,0 @@
export { default as Footer } from "./footer/Footer";
@@ -1,136 +1,79 @@
import { amplitudeLogNavigation } from "@/logging";
import { AkselSidebarT } from "@/types";
import {
ChevronDownIcon,
SparklesIcon,
TestFlaskIcon,
TrashIcon,
} from "@navikt/aksel-icons";
import { Detail } from "@navikt/ds-react";
import cl from "clsx";
import { SidebarT } from "@/types";
import { StatusTag } from "@/web/StatusTag";
import { Box, Label, Show } from "@navikt/ds-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { useSection } from "./useSection";
const NavItem = ({
link,
const Sidebar = ({
kategori,
links,
}: {
link: { heading: string; slug: string; kategori: string; tag?: string };
kategori: "komponenter" | "grunnleggende" | "templates";
links: SidebarT;
}) => {
const { asPath } = useRouter();
const isActive = asPath.split("#")[0] === `/${link.slug}`;
return (
<li>
<Link
aria-current={isActive ? "page" : undefined}
href={`/${link.slug}`}
onClick={(e) => {
amplitudeLogNavigation("meny", e.currentTarget.getAttribute("href"));
}}
className={cl(
"hover:text-deepblue-800 focus-visible:shadow-focus text-medium pr-05 relative flex overflow-hidden py-1 pl-4 leading-snug before:rounded-full hover:before:transition-colors focus:outline-none focus-visible:z-10 focus-visible:rounded-sm",
{
"before:border-l-border-action-selected before:absolute before:left-0 before:top-1/2 before:h-6 before:-translate-y-1/2 before:border-l-[4px]":
isActive,
"text-deepblue-800 font-semibold": isActive,
"text-text-subtle": !isActive,
"before:absolute before:left-0 before:top-0 before:h-full before:border-l before:border-l-gray-200 hover:before:top-1/2 hover:before:h-6 hover:before:min-h-[70%] hover:before:-translate-y-1/2 hover:before:border-l-2 hover:before:border-l-gray-400":
!isActive,
}
)}
<Show asChild above="md">
<Box
paddingBlock="0 8"
paddingInline="2"
aria-label={kategori}
as="nav"
className="w-sidebar shrink-0 self-start"
>
{link.heading}
<span className="ml-auto capitalize">
{link?.tag === "beta" && (
<TestFlaskIcon
aria-hidden
className="text-violet-800"
fontSize="1.25rem"
/>
)}
{link?.tag === "new" && (
<SparklesIcon
aria-hidden
className="text-lightblue-700"
fontSize="1.25rem"
/>
)}
{link?.tag === "deprecated" && (
<TrashIcon
aria-hidden
className="text-text-default"
fontSize="1.25rem"
/>
)}
</span>
</Link>
</li>
<ul className="space-y-6">
{links.map((section) => (
<Section key={section.value} section={section} />
))}
</ul>
</Box>
</Show>
);
};
const Dropdown = ({
title,
links,
}: {
title: string;
links: AkselSidebarT;
}) => {
const [open, setOpen] = useState(true);
function Section({ section }: { section: SidebarT[number] }) {
const { asPath } = useRouter();
const isActive = (slug: string) => {
return asPath.split("#")[0] === `/${slug}`;
};
return (
<li className="peer w-full peer-data-[open=true]:mt-6" data-open={open}>
<button
onClick={() => setOpen((x) => !x)}
className="min-h-8 text-text-subtle hover:text-deepblue-800 group z-10 flex w-full cursor-pointer items-center justify-between focus:outline-none"
aria-expanded={open}
<li>
<Label
as="div"
size="small"
textColor="subtle"
className="min-h-8 flex items-center px-4"
>
<Detail
as="span"
className="group-focus-visible:shadow-focus group-hover:bg-surface-neutral-subtle group-active:bg-surface-neutral-subtle-hover mt-6 flex w-full items-center justify-between rounded-sm pl-2 font-semibold transition-colors first:mt-0"
>
{title}
<span className="flex h-6 w-6 items-center justify-center rounded">
<ChevronDownIcon
className={cl("text-base", { "rotate-180": open })}
aria-hidden
title={!open ? `åpne ${title}` : `lukk ${title}`}
/>
</span>
</Detail>
</button>
<ul hidden={!open} className="pl-2">
{links.map((z) => (
<NavItem link={z} key={z?.heading} />
{section.title}
</Label>
<ul className="space-y-px pl-3">
{section.pages.map((page) => (
<li
aria-current={isActive(page.slug) ? "page" : undefined}
key={page.slug}
data-active={isActive(page.slug)}
className="rounded-medium hover:bg-surface-hover min-h-8 data-active:bg-surface-selected data-active:text-deepblue-800 data-active:shadow-xsmall data-active:font-semibold relative mx-2 flex items-center transition-[background-color] ease-out"
>
<Link
prefetch={false}
href={`/${page.slug}`}
className="text-medium min-h-8 rounded-medium focus-visible:shadow-focus flex w-full items-center justify-between gap-2 break-words px-2 focus:outline-none focus-visible:z-10"
onClick={(e) => {
amplitudeLogNavigation(
"meny",
e.currentTarget.getAttribute("href")
);
}}
>
{page.heading}
<StatusTag size="xsmall" status={page.tag} />
</Link>
</li>
))}
</ul>
</li>
);
};
}
export const Sidebar = ({
kategori,
links,
}: {
kategori: "Komponenter" | "Grunnleggende";
links: AkselSidebarT;
}) => {
const sections = useSection({ kategori, links });
return (
<div
data-testid="ds-sidebar"
className="toc-ignore w-sidebar bg-surface-default z-[1002] hidden shrink-0 self-start overflow-x-auto pb-8 pl-6 pr-2 md:block"
>
<nav aria-label={kategori}>
<ul>
{sections.map((kat) => (
<Dropdown links={kat.pages} title={kat.title} key={kat.title} />
))}
</ul>
</nav>
</div>
);
};
export default Sidebar;
@@ -1,52 +0,0 @@
import { AkselSidebarT } from "@/types";
import { useMemo } from "react";
import {
grunnleggendeKategorier,
komponentKategorier,
} from "../../../sanity/config";
export const useSection = ({
kategori,
links,
}: {
kategori: "Komponenter" | "Grunnleggende";
links: AkselSidebarT;
}) => {
const sections = useMemo(
() =>
(kategori === "Komponenter"
? komponentKategorier
: grunnleggendeKategorier
)
.map((x) => ({
...x,
pages: links
.filter((y) => y?.kategori === x.value)
.sort((a, b) => {
if (a?.tag === "deprecated" && b?.tag === "deprecated") {
return 0;
} else if (a?.tag === "deprecated") {
return 1;
} else if (b.tag === "deprecated") {
return -1;
}
if (a.sidebarindex !== null || b.sidebarindex !== null) {
if (a.sidebarindex !== null && b.sidebarindex !== null) {
return a.sidebarindex - b.sidebarindex;
} else if (a.sidebarindex !== null) {
return -1;
} else {
return 1;
}
}
return a?.heading.localeCompare(b?.heading);
}),
}))
.filter((x) => !(!x.pages || x.pages.length === 0)),
[links, kategori]
);
return sections;
};
@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from "@storybook/react";
import { WithSidebar } from "./WithSidebar";
import { SanityBlockContent } from "@/sanity-block";
import { getBlocks } from "@/sb-util";
import type { Meta, StoryObj } from "@storybook/react";
import { WithSidebar } from "./WithSidebar";
const meta = {
title: "Layout/WithSidebar",
@@ -24,40 +24,53 @@ const children = (
</>
);
const baseItem = { slug: "/123", tag: "beta" as const, sidebarindex: null };
const sidebar = [
{
pages: [
{ ...baseItem, kategori: "primitives", heading: "f", sidebarindex: 0 },
{ ...baseItem, kategori: "primitives", heading: "e", sidebarindex: 1 },
{
...baseItem,
kategori: "primitives",
heading: "d",
tag: "deprecated" as const,
sidebarindex: 2,
},
],
title: "Primitives",
value: "primitives",
},
{
pages: [
{ ...baseItem, kategori: "core", heading: "b" },
{
...baseItem,
kategori: "core",
heading: "c",
tag: "deprecated" as const,
},
],
title: "Core",
value: "core",
},
];
export const KomponenterLanding: Story = {
args: {
children,
pageType: {
type: "Komponenter",
type: "komponenter",
title: "Heading",
rootUrl: "/komponenter",
rootTitle: "Komponenter",
},
pageProps: { content: "1" },
variant: "landingPage",
footer: "footer",
intro: "intro",
sidebar: [
{
heading: "Link 1",
slug: "#",
kategori: "core",
tag: "ready",
sidebarindex: null,
},
{
heading: "Link 2",
slug: "#",
kategori: "primitives",
tag: "ready",
sidebarindex: null,
},
{
heading: "Link e",
slug: "#",
kategori: "primitives",
tag: "ready",
sidebarindex: null,
},
],
sidebar,
},
};
@@ -65,35 +78,15 @@ export const Komponenter: Story = {
args: {
children,
pageType: {
type: "Komponenter",
type: "komponenter",
title: "Heading",
rootUrl: "/komponenter",
rootTitle: "Komponenter",
},
pageProps: { content: "1" },
variant: "page",
footer: "footer",
intro: "intro",
sidebar: [
{
heading: "Link 1",
slug: "#",
kategori: "core",
tag: "ready",
sidebarindex: null,
},
{
heading: "Link 2",
slug: "#",
kategori: "primitives",
tag: "ready",
sidebarindex: null,
},
{
heading: "Link 3",
slug: "#",
kategori: "primitives",
tag: "ready",
sidebarindex: null,
},
],
sidebar,
},
};
@@ -1,11 +1,10 @@
import Sidebar from "@/layout/sidebar/Sidebar";
import { urlFor } from "@/sanity/interface";
import { AkselSidebarT } from "@/types";
import { SidebarT, TableOfContentsT } from "@/types";
import { capitalize } from "@/utils";
import TableOfContents from "@/web/toc/TableOfContents";
import { Box, Detail, Heading, Link } from "@navikt/ds-react";
import cl from "clsx";
import { Sidebar } from "components/layout/sidebar/Sidebar";
import Feedback from "components/website-modules/feedback";
import { TableOfContentsv2 } from "components/website-modules/toc/TOCv2";
import Image from "next/legacy/image";
import NextLink from "next/link";
@@ -17,12 +16,16 @@ export const WithSidebar = ({
pageProps,
variant = "landingPage",
footer,
toc,
}: {
children: React.ReactNode;
sidebar: AkselSidebarT;
sidebar: SidebarT;
toc?: TableOfContentsT;
pageType: {
type: "Komponenter" | "Grunnleggende";
type: "komponenter" | "grunnleggende" | "templates";
title: string;
rootUrl: string;
rootTitle: string;
};
pageProps: any;
intro?: React.ReactNode;
@@ -30,13 +33,17 @@ export const WithSidebar = ({
variant?: "page" | "landingPage";
}) => {
return (
<Box background="bg-default" paddingBlock="6 24">
<Box
background="bg-default"
paddingBlock="6 24"
className="min-h-screen-header"
>
<div className="mx-auto flex w-full max-w-screen-2xl gap-6">
<Sidebar kategori={pageType.type} links={sidebar} />
<main
tabIndex={-1}
id="hovedinnhold"
className="md:max-w-screen-sidebar relative z-0 w-full px-4 focus:outline-none sm:pl-6 sm:pr-6 md:pl-0"
className="relative z-0 w-full px-4 focus:outline-none sm:pl-6 sm:pr-6 md:pl-0"
>
<div
className={cl(
@@ -53,13 +60,9 @@ export const WithSidebar = ({
<div className="z-[1]">
{variant === "page" && pageProps?.kategori && (
<Detail as="div" className="mb-2">
<NextLink
href={`/${pageType.type.toLowerCase()}`}
passHref
legacyBehavior
>
<NextLink href={pageType.rootUrl} passHref legacyBehavior>
<Link className="text-text-default">
{capitalize(pageType.type)}
{pageType.rootTitle}
</Link>
</NextLink>{" "}
/ {capitalize(pageProps.kategori)}
@@ -110,11 +113,14 @@ export const WithSidebar = ({
)}
</div>
<div className={cl("sm:px-6 md:px-10", { flex: variant === "page" })}>
{variant === "page" && <TableOfContentsv2 />}
<div
className={cl("sm:px-6 md:px-10", {
"flex gap-10": variant === "page",
})}
>
{variant === "page" && toc && <TableOfContents toc={toc} />}
<div className="w-full">
{children}
<Feedback docId={pageProps?._id} docType={pageProps?._type} />
{footer && <div className="w-full">{footer}</div>}
</div>
</div>
@@ -1,3 +1,3 @@
export { amplitudeLogNavigation } from "./event-tracking";
export { AmplitudeEvents } from "./events";
export { amplitude } from "./useAmplitude";
export { default as useAmplitudeInit, amplitude } from "./useAmplitude";
@@ -29,10 +29,11 @@ const useAmplitudeInit = () => {
useEffect(() => {
const isProdUrl = () => window.location.host === "aksel.nav.no";
const isExample = () => window.location.pathname.includes("eksempler/");
const isTemplate = () => window.location.pathname.includes("templates/");
const isPreview = () => !!document.getElementById("exit-preview-id");
const initAmplitude = async () => {
if (isExample()) {
if (isExample() || isTemplate()) {
return;
} else if (!isProdUrl()) {
mockAmplitude();
@@ -59,7 +60,7 @@ const useAmplitudeInit = () => {
.forEach(([event, eventData]) => amplitude.track(event, eventData))
)
.catch(async () => {
const { logger } = await import("logger");
const { logger } = await import("../../config/logger");
logger.error("Failed logging batched events");
});
};
@@ -1,35 +0,0 @@
import React from "react";
import { withErrorBoundary } from "@/error-boundary";
import TokenView from "./token-view";
type SpesialT = {
_key: string;
_type: "spesial_seksjon";
modul?: "ikonsok" | "token_kategori";
logs?: any[];
komponenter?: any;
farge?: any;
token?: { title: string; kategori: string };
};
const SideModul = ({ node }: { node: SpesialT }) => {
if (!node || !node.modul) {
return null;
}
const GetModule = () => {
switch (node.modul) {
case "ikonsok":
return null;
case "token_kategori":
return <TokenView token={node.token} />;
default: {
return null;
}
}
};
return <div className="mb-16">{GetModule()}</div>;
};
export default withErrorBoundary(SideModul, "SideModul");
Loaded 100 of 653 files, more files were not shown because too many files have changed in this diff. Show more