Compare commits

...
Author SHA1 Message Date
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
github-actions[bot]andgithub-actions[bot] fe15b4d275 Version Packages (#2414)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-10-30 10:57:37 +01:00
Halvor Haugan 28e095d5a2 Oppdater eksemplene på Popover (#2425) 2023-10-30 10:21:17 +01:00
dependabot[bot] 43377841ba ⬆️ Bump browserify-sign from 4.2.1 to 4.2.2 (#2434)
Bumps [browserify-sign](https://github.com/crypto-browserify/browserify-sign) from 4.2.1 to 4.2.2.
- [Changelog](https://github.com/browserify/browserify-sign/blob/main/CHANGELOG.md)
- [Commits](https://github.com/crypto-browserify/browserify-sign/compare/v4.2.1...v4.2.2)

---
updated-dependencies:
- dependency-name: browserify-sign
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-30 09:16:59 +01:00
Ken 2976ee9626 🐛 Bugfix routing i sidebar/eksempler (#2432) 2023-10-27 18:49:32 +02:00
Ken 23c135b437 🐛 Fikset lasting av toc (#2421) 2023-10-27 15:59:13 +02:00
Halvor Haugan 7f0498f250 🐛 Oppdater eksemplene på Loader (#2431) 2023-10-27 12:52:17 +02:00
Halvor Haugan 5075c8d0a6 ✨ Modal: Støtte for å lukke ved klikk utenfor (#2386) 2023-10-27 12:47:47 +02:00
Ken 792eeaba42 🐛 Fikset dom-validering av Accordion (#2426) 2023-10-27 12:36:45 +02:00
Halvor Haugan 0c7fc04935 🐛 Modal: Bedre støtte for Tooltip i Modal (#2429) 2023-10-27 10:47:24 +02:00
Halvor Haugan 0bb339e54a [aksel.nav.no] Legg til tittel på RelatertInnhold-modulen (#2423) 2023-10-25 16:51:40 +02:00
Halvor Haugan f5eb3adaea [aksel.nav.no] Få CodeSandbox til å fungere med TS (#2424) 2023-10-25 16:33:07 +02:00
Ken 77cf71f814 🐛 Tester fix for internlenker til anchors (#2420) 2023-10-25 15:07:07 +02:00
Vegard Haugstvedt f8c12b0757 Autocomplete should keep upper-/lowercase from the word being autocompleted (#2207)
* AutoComplete should keep upper-/lowercase from the word being autocompleted

* Fix issue where autocomplete doesn't format the casing of the last character in the word

* Added changeset

* Append the autocompleted text in the same casing used in the list, but preserve casing for the typed characters

* Preserve casing used while typing, then use the casing from the options when selecting a value

* Added custom options to test of casing when typing

* Correct changeset

* When allowNewValues is enabled, we should use the new value as is when pressing Enter, instead of using the first FilteredOption

* Change test case to use a partial match for a word in options. This tests both that we will use "add new" option if available and that we preserve casing for new options
2023-10-24 07:50:50 +02:00
Ken be8265e13e 🐛 Fikset versjon av nextjs (#2410) 2023-10-23 13:12:40 +02:00
Halvor Haugan 0c7d6fb461 Fiks shadowdom-eksempel (#2392) 2023-10-23 11:42:00 +02:00
Julian Nymark b97ac03c5a 🚧 test if this branch builds (#2368)
* 🚧 test if this branch builds

* ♻️ css -> types

* ♻️ remove double module imports
2023-10-23 10:07:54 +02:00
github-actions[bot]andgithub-actions[bot] 0ddb589138 Version Packages (#2408)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-10-20 17:18:00 +02:00
Ken 1440c99b96 [aksel.nav.no] 📝 Story for Table i aksel (#2406)
* 📝 Story for Table i aksel

* ♻️ Trukket args ut
2023-10-20 17:02:34 +02:00
Ken f8cf39c906 [aksel.nav.no] Lagt til muligheten til å ikke ha "Send innspill" modul på komponent-artikler (#2407)
* 🎨 Oversikt blir nå sortert basert på index

* 🎨 Kan nå skjule send innspill modul
2023-10-20 16:30:50 +02:00
Ken 158c032436 [aksel.nav.no] Refactor av amplitude-logging (#2396)
* ♻️ Refaktorert logging

* ♻️ Refaktorert logging

* 🔥 No need to lazyload hotjar

* 🐛 Sjekker nå produrl riktig

* ♻️ Bedre import/export av logging

* ✨ Batching evets logged pre-init

* ♻️ Cleanup av amplitude bruk
2023-10-20 16:05:38 +02:00
Halvor Haugan 6d6e6443dd 💄 Chips.Toggle: Ikon i uvalgt tilstand (#2395) 2023-10-20 15:54:05 +02:00
Ken e12ea8694e ✨ Monthpicker eksempel og bug (#2404) 2023-10-20 12:44:28 +02:00
github-actions[bot]andgithub-actions[bot] 2d4cb67643 Version Packages (#2403)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-10-20 10:52:13 +02:00
Ken f5533766c8 📝 Oppdatert link og linkpanel med nextjs eksempler 2023-10-20 10:28:24 +02:00
Ken 3d1b0ac204 🎨 Forms-border har nå outside shadow for background-bleed (#2388) 2023-10-20 09:21:42 +02:00
Ken a1678d3b19 🐛 Loader hadde legacyRef som ref-prop (#2391)
* 🐛 Loader hadde legacyRef som ref-prop

* Update .changeset/heavy-shoes-pay.md
2023-10-20 09:17:39 +02:00
github-actions[bot]andgithub-actions[bot] 7007d3c58e Version Packages (#2401)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-10-19 19:56:13 +02:00
Ken 8524346044 🐛 Modal + Popover fix (#2400)
* 🧪 SB-demo

* 🐛 Datepicker-popover vises ikke ritkig i Modal

* 📝 Modal + datepicker demo
2023-10-19 19:52:06 +02:00
Ken d5d8898659 🐛 Riktig CSP for hotjar 2023-10-19 18:28:24 +02:00
Ken 1d635e1dbd ⬇️ reverted til gammel nextjs-versjon (#2393) 2023-10-18 15:57:07 +02:00
Ken 6b3932b172 [aksel.nav.no] 📝 Oppdatert eksempler for Bleed og show/hide (#2385)
* 📝 Oppdatert eksempler for Bleed og show/hide

* ♻️ Refactor wrapper rundt bleed-eksempler
2023-10-18 12:50:12 +02:00
Ken 4d6ae1ae42 [aksel.nav.no] 🐛 Fikset next error ved redeploy av app (#2387)
* 🐛 Fikset next error ved redeploy av app

* 🎨 synket next-versjoner
2023-10-18 11:11:52 +02:00
238 changed files with 5905 additions and 2785 deletions

No files matched your search

+10
View File
@@ -1,5 +1,15 @@
# @navikt/aksel-icons
## 5.9.0
## 5.8.0
## 5.7.6
## 5.7.5
## 5.7.4
## 5.7.3
## 5.7.2
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "5.7.3",
"version": "5.9.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
+10
View File
@@ -1,5 +1,15 @@
# @navikt/aksel-stylelint
## 5.9.0
## 5.8.0
## 5.7.6
## 5.7.5
## 5.7.4
## 5.7.3
## 5.7.2
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "5.7.3",
"version": "5.9.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
"repository": {
@@ -35,8 +35,8 @@
"dev": "yarn watch:lint"
},
"devDependencies": {
"@navikt/ds-css": "^5.7.3",
"@navikt/ds-tokens": "^5.7.3",
"@navikt/ds-css": "^5.9.0",
"@navikt/ds-tokens": "^5.9.0",
"@types/jest": "^29.0.0",
"concurrently": "7.2.1",
"copyfiles": "2.4.1",
+35
View File
@@ -1,5 +1,40 @@
# @navikt/aksel
## 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
- Updated dependencies [[`0c7fc0493`](https://github.com/navikt/aksel/commit/0c7fc04935b96946b8ed1178c700077b6a1fcefd)]:
- @navikt/ds-css@5.8.0
## 5.7.6
### Patch Changes
- Updated dependencies [[`6d6e6443d`](https://github.com/navikt/aksel/commit/6d6e6443dd00464183b03fad6f822e5f3efab100)]:
- @navikt/ds-css@5.7.6
## 5.7.5
### Patch Changes
- Updated dependencies [[`3d1b0ac20`](https://github.com/navikt/aksel/commit/3d1b0ac2041bbc289536fa5a0dc65b2170dfc6fa)]:
- @navikt/ds-css@5.7.5
## 5.7.4
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@5.7.4
## 5.7.3
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "5.7.3",
"version": "5.9.0",
"description": "Aksel command line interface. Handles css-imports, codemods and more",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -29,7 +29,7 @@
},
"homepage": "https://aksel.nav.no/grunnleggende/kode/kommandolinje",
"dependencies": {
"@navikt/ds-css": "5.7.3",
"@navikt/ds-css": "5.9.0",
"axios": "1.3.6",
"chalk": "4.1.0",
"clipboardy": "^2.3.0",
+32
View File
@@ -1,5 +1,37 @@
# @navikt/ds-css
## 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
- :bug: Modal: Bedre støtte for Tooltip i Modal ([#2429](https://github.com/navikt/aksel/pull/2429))
## 5.7.6
### Patch Changes
- :lipstick: Chips.Toggle: Ikon i uvalgt tilstand ([#2395](https://github.com/navikt/aksel/pull/2395))
## 5.7.5
### Patch Changes
- Checkbox, Radio: Transparent-border fungerer nå bedre på mørkere bakgrunner ([#2388](https://github.com/navikt/aksel/pull/2388))
## 5.7.4
## 5.7.3
### Patch Changes
+14 -1
View File
@@ -20,7 +20,7 @@
cursor: pointer;
align-items: center;
justify-content: center;
gap: var(--a-spacing-1);
gap: var(--a-spacing-05);
margin: 0;
padding: 0 var(--a-spacing-3);
text-decoration: none;
@@ -97,6 +97,19 @@
}
}
.navds-chips--medium .navds-chips__toggle--with-checkmark {
padding-left: var(--a-spacing-1-alt);
}
.navds-chips--small .navds-chips__toggle--with-checkmark {
padding-left: var(--a-spacing-1);
}
.navds-chips--small .navds-chips__toggle-icon {
width: 1rem;
height: 1rem;
}
.navds-chips__removable {
gap: 0;
}
+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);
}
+38 -33
View File
@@ -31,11 +31,13 @@
.navds-radio__label::before {
content: "";
background-color: var(--ac-radio-checkbox-bg, var(--a-surface-default));
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default));
width: 1.5rem;
height: 1.5rem;
border-radius: var(--a-border-radius-medium);
border-radius: var(--a-border-radius-small);
flex-shrink: 0;
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default));
width: calc(1.5rem - 0.25rem);
height: calc(1.5rem - 0.25rem);
margin-block: 2px;
margin-inline: 2px;
}
.navds-radio__label::before {
@@ -64,36 +66,37 @@
.navds-checkbox--small > .navds-checkbox__label::before,
.navds-radio--small > .navds-radio__label::before {
width: 1.25rem;
height: 1.25rem;
width: calc(1.25rem - 0.25rem);
height: calc(1.25rem - 0.25rem);
}
.navds-checkbox__input:focus-visible + .navds-checkbox__label::before,
.navds-radio__input:focus-visible + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default)), var(--a-shadow-focus);
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default)),
0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)), 0 0 0 4px var(--a-border-focus);
}
@supports not selector(:focus-visible) {
.navds-checkbox__input:focus + .navds-checkbox__label::before,
.navds-radio__input:focus + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default)), var(--a-shadow-focus);
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default)), 0 0 0 4px var(--a-border-focus);
}
}
.navds-checkbox__input:hover:focus-visible + .navds-checkbox__label::before,
.navds-radio__input:hover:focus-visible + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)), var(--a-shadow-focus);
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)), 0 0 0 4px var(--a-border-focus);
}
@supports not selector(:focus-visible) {
.navds-checkbox__input:hover:focus + .navds-checkbox__label::before,
.navds-radio__input:hover:focus + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)), var(--a-shadow-focus);
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)), 0 0 0 4px var(--a-border-focus);
}
}
.navds-checkbox__input:indeterminate + .navds-checkbox__label::before {
box-shadow: none;
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action));
background-color: var(--ac-radio-checkbox-action, var(--a-surface-action));
}
@@ -114,7 +117,7 @@
}
.navds-checkbox__input:checked + .navds-checkbox__label::before {
box-shadow: none;
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action));
background-color: var(--ac-radio-checkbox-action, var(--a-surface-action));
}
@@ -146,31 +149,33 @@
.navds-checkbox__input:indeterminate:focus-visible + .navds-checkbox__label::before,
.navds-checkbox__input:checked:focus-visible + .navds-checkbox__label::before {
box-shadow: inset 0 0 0 1px var(--ac-radio-checkbox-bg, var(--a-surface-default)), var(--a-shadow-focus);
box-shadow: 0 0 0 1px var(--ac-radio-checkbox-action, var(--a-surface-action)),
0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)), 0 0 0 4px var(--a-border-focus);
}
@supports not selector(:focus-visible) {
.navds-checkbox__input:indeterminate:focus + .navds-checkbox__label::before,
.navds-checkbox__input:checked:focus + .navds-checkbox__label::before {
box-shadow: inset 0 0 0 1px var(--ac-radio-checkbox-bg, var(--a-surface-default)), var(--a-shadow-focus);
box-shadow: 0 0 0 1px var(--ac-radio-checkbox-action, var(--a-surface-action)),
0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)), 0 0 0 4px var(--a-border-focus);
}
}
.navds-radio__input:checked + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
inset 0 0 0 4px var(--ac-radio-checkbox-bg, var(--a-surface-default));
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
inset 0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default));
background-color: var(--ac-radio-checkbox-action, var(--a-surface-action));
}
.navds-radio__input:checked:focus-visible + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
inset 0 0 0 4px var(--ac-radio-checkbox-bg, var(--a-surface-default)), var(--a-shadow-focus);
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
inset 0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)), 0 0 0 4px var(--a-border-focus);
}
@supports not selector(:focus-visible) {
.navds-radio__input:checked:focus + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
inset 0 0 0 4px var(--ac-radio-checkbox-bg, var(--a-surface-default)), var(--a-shadow-focus);
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
inset 0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)), 0 0 0 4px var(--a-border-focus);
}
}
@@ -182,13 +187,13 @@
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus-visible)
+ .navds-checkbox__label::before,
.navds-radio__input:hover:not(:disabled):not(:checked):not(:focus-visible) + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action));
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action));
}
@supports not selector(:focus-visible) {
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus) + .navds-checkbox__label::before,
.navds-radio__input:hover:not(:disabled):not(:checked):not(:focus) + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action));
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action));
}
}
@@ -201,14 +206,14 @@
> .navds-checkbox__input:not(:hover):not(:disabled):not(:checked):not(:indeterminate)
+ .navds-checkbox__label::before,
.navds-radio--error > .navds-radio__input:not(:hover):not(:disabled):not(:checked) + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
}
.navds-checkbox--error
> .navds-checkbox__input:focus-visible:not(:hover):not(:disabled):not(:checked):not(:indeterminate)
+ .navds-checkbox__label::before,
.navds-radio--error > .navds-radio__input:focus-visible:not(:hover):not(:disabled):not(:checked) + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), var(--a-shadow-focus);
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), 0 0 0 4px var(--a-border-focus);
}
@supports not selector(:focus-visible) {
@@ -216,7 +221,7 @@
> .navds-checkbox__input:focus:not(:hover):not(:disabled):not(:checked):not(:indeterminate)
+ .navds-checkbox__label::before,
.navds-radio--error > .navds-radio__input:focus:not(:hover):not(:disabled):not(:checked) + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), var(--a-shadow-focus);
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), 0 0 0 4px var(--a-border-focus);
}
}
@@ -225,7 +230,7 @@
+ .navds-checkbox__label::before,
.navds-radio--error > .navds-radio__input:hover:not(:disabled):not(:checked):not(:focus-visible) + .navds-radio__label::before {
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
}
.navds-checkbox--error
@@ -233,7 +238,7 @@
+ .navds-checkbox__label::before,
.navds-radio--error > .navds-radio__input:focus-visible:hover:not(:disabled):not(:checked) + .navds-radio__label::before {
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), var(--a-shadow-focus);
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), 0 0 0 4px var(--a-border-focus);
}
@supports not selector(:focus-visible) {
@@ -242,7 +247,7 @@
+ .navds-checkbox__label::before,
.navds-radio--error > .navds-radio__input:hover:not(:disabled):not(:checked):not(:focus) + .navds-radio__label::before {
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
}
.navds-checkbox--error
@@ -250,7 +255,7 @@
+ .navds-checkbox__label::before,
.navds-radio--error > .navds-radio__input:focus:hover:not(:disabled):not(:checked) + .navds-radio__label::before {
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), var(--a-shadow-focus);
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), 0 0 0 4px var(--a-border-focus);
}
}
@@ -290,7 +295,7 @@
.navds-radio--readonly > .navds-radio__input:not(:disabled):not(:checked) + .navds-radio__label::before,
.navds-radio--readonly > .navds-radio__input:hover:not(:checked):not(:focus-visible) + .navds-radio__label::before {
background-color: var(--__ac-radio-checkbox-readonly-bg);
box-shadow: inset 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
box-shadow: 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
}
.navds-checkbox--readonly > .navds-checkbox__input:focus-visible + .navds-checkbox__label::before,
@@ -299,8 +304,8 @@
}
.navds-checkbox--readonly > .navds-checkbox__input:checked + .navds-checkbox__label::before {
box-shadow: inset 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
background-color: var(--__ac-radio-checkbox-readonly-bg);
box-shadow: 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
}
.navds-checkbox--readonly > .navds-checkbox__input:checked + .navds-checkbox__label > .navds-checkbox__icon {
@@ -308,12 +313,12 @@
}
.navds-radio--readonly > .navds-radio__input:checked + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--__ac-radio-checkbox-readonly-border), inset 0 0 0 4px var(--__ac-radio-checkbox-readonly-bg);
background-color: var(--a-icon-subtle);
box-shadow: 0 0 0 2px var(--__ac-radio-checkbox-readonly-border), inset 0 0 0 2px var(--__ac-radio-checkbox-readonly-bg);
}
.navds-checkbox--readonly > .navds-checkbox__input:indeterminate + .navds-checkbox__label::before {
box-shadow: inset 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
box-shadow: 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
background-color: var(--__ac-radio-checkbox-readonly-bg);
}
+12 -1
View File
@@ -23,7 +23,6 @@
.navds-modal--polyfilled {
top: 50%;
transform: translate(0, -50%);
overflow: auto;
/* From polyfill (dialog-polyfill/dist/dialog-polyfill.css): */
left: 0;
@@ -33,6 +32,10 @@
margin: auto;
}
.navds-modal--polyfilled .navds-modal--polyfilled {
overflow: auto;
}
.navds-modal--polyfilled:not([open]) {
display: none; /* from polyfill */
}
@@ -127,6 +130,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.7.3",
"version": "5.9.0",
"description": "CSS for NAV Designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -27,7 +27,7 @@
"css:get-version": "node config/get-version.js"
},
"devDependencies": {
"@navikt/ds-tokens": "^5.7.3",
"@navikt/ds-tokens": "^5.9.0",
"cssnano": "6.0.0",
"fast-glob": "3.2.11",
"lodash": "4.17.21",
+68
View File
@@ -1,5 +1,73 @@
# @navikt/ds-react
## 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
- :sparkles: Modal: Støtte for å lukke ved klikk utenfor ([#2386](https://github.com/navikt/aksel/pull/2386))
### Patch Changes
- Autocomplete in combobox will not change formatting of the letters while being typed, but will use the casing of the autocompleted word when selecting the option. ([#2207](https://github.com/navikt/aksel/pull/2207))
- Accordion: Fikset dom-validering ved bruk av `div` i `button`. ([#2426](https://github.com/navikt/aksel/pull/2426))
- :bug: Modal: Bedre støtte for Tooltip i Modal ([#2429](https://github.com/navikt/aksel/pull/2429))
- Updated dependencies []:
- @navikt/ds-tokens@5.8.0
- @navikt/aksel-icons@5.8.0
## 5.7.6
### Patch Changes
- :lipstick: Chips.Toggle: Ikon i uvalgt tilstand ([#2395](https://github.com/navikt/aksel/pull/2395))
- Updated dependencies []:
- @navikt/ds-tokens@5.7.6
- @navikt/aksel-icons@5.7.6
## 5.7.5
### Patch Changes
- Loader: Har nå riktig `ref`-type. ([#2391](https://github.com/navikt/aksel/pull/2391))
- Updated dependencies []:
- @navikt/ds-tokens@5.7.5
- @navikt/aksel-icons@5.7.5
## 5.7.4
### Patch Changes
- Datepicker: Fungerer nå bedre i Modal ([#2400](https://github.com/navikt/aksel/pull/2400))
- Updated dependencies []:
- @navikt/ds-tokens@5.7.4
- @navikt/aksel-icons@5.7.4
## 5.7.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "5.7.3",
"version": "5.9.0",
"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.7.3",
"@navikt/ds-tokens": "^5.7.3",
"@navikt/aksel-icons": "^5.9.0",
"@navikt/ds-tokens": "^5.9.0",
"@radix-ui/react-tabs": "1.0.0",
"@radix-ui/react-toggle-group": "1.0.0",
"clsx": "^1.2.1",
@@ -1,9 +1,9 @@
import { ChevronDownIcon } from "@navikt/aksel-icons";
import cl from "clsx";
import React, { forwardRef, useContext } from "react";
import { Heading } from "../typography";
import { AccordionContext } from "./AccordionContext";
import { AccordionItemContext } from "./AccordionItem";
import { ChevronDownIcon } from "@navikt/aksel-icons";
export interface AccordionHeaderProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -37,13 +37,13 @@ const AccordionHeader = forwardRef<HTMLButtonElement, AccordionHeaderProps>(
aria-expanded={itemContext.open}
type="button"
>
<div className="navds-accordion__icon-wrapper">
<span className="navds-accordion__icon-wrapper">
<ChevronDownIcon
className="navds-accordion__header-chevron"
title="Vis mer"
aria-hidden
/>
</div>
</span>
<Heading
size={accordionContext?.headingSize ?? "small"}
as="span"
+22 -12
View File
@@ -6,7 +6,7 @@ export interface ToggleChipsProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: string;
/**
* Toggles aria-pressed and visual-changes
* Toggles aria-pressed and visual changes
*/
selected?: boolean;
/**
@@ -44,28 +44,38 @@ export const ToggleChips: OverridableComponent<
className={cl(
"navds-chips__chip navds-chips__toggle",
className,
`navds-chips__toggle--${variant}`
`navds-chips__toggle--${variant}`,
{ "navds-chips__toggle--with-checkmark": checkmark }
)}
aria-pressed={selected}
>
{selected && checkmark && (
{checkmark && (
<svg
aria-hidden
className="navds-chips__toggle-icon"
width="10"
height="8"
viewBox="0 0 10 8"
width="1.25em"
height="1.25em"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
focusable={false}
role="img"
>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M9.51894 0.45851C9.81799 0.745107 9.8281 1.21987 9.5415 1.51893L3.7915 7.51893C3.51247 7.81009 3.05315 7.82848 2.75174 7.56056L0.501735 5.56056C0.192148 5.28537 0.164263 4.81132 0.439451 4.50173C0.71464 4.19214 1.18869 4.16426 1.49828 4.43944L3.20835 5.95951L8.45852 0.481072C8.74511 0.182015 9.21988 0.171913 9.51894 0.45851Z"
fill="currentColor"
/>
{selected ? (
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 18.125C14.4873 18.125 18.125 14.4873 18.125 10C18.125 5.51269 14.4873 1.875 10 1.875C5.51269 1.875 1.875 5.51269 1.875 10C1.875 14.4873 5.51269 18.125 10 18.125ZM14.128 7.72904C14.3695 7.44357 14.3339 7.01635 14.0485 6.7748C13.763 6.53326 13.3358 6.56886 13.0942 6.85432L8.60428 12.1606L6.41627 9.97263C6.15185 9.70822 5.72315 9.70822 5.45873 9.97263C5.19431 10.2371 5.19431 10.6658 5.45873 10.9302L8.16706 13.6385C8.30095 13.7724 8.48479 13.8441 8.67397 13.8362C8.86316 13.8284 9.0404 13.7416 9.16271 13.5971L14.128 7.72904Z"
fill="currentColor"
/>
) : (
<path
fillRule="evenodd"
clipRule="evenodd"
d="M10 3.125C6.20304 3.125 3.125 6.20304 3.125 10C3.125 13.797 6.20304 16.875 10 16.875C13.797 16.875 16.875 13.797 16.875 10C16.875 6.20304 13.797 3.125 10 3.125ZM1.875 10C1.875 5.51269 5.51269 1.875 10 1.875C14.4873 1.875 18.125 5.51269 18.125 10C18.125 14.4873 14.4873 18.125 10 18.125C5.51269 18.125 1.875 14.4873 1.875 10Z"
fill="var(--a-border-default)"
/>
)}
</svg>
)}
<span className="navds-chips__chip-text">{children}</span>
@@ -60,11 +60,11 @@ Default.argTypes = {
},
};
export const Toggle = () => {
export const Toggle = ({ size }) => {
const [selected, setSelected] = useState<number[]>([2, 4]);
return (
<div className="colgap">
<Chips>
<Chips size={size}>
{options.map((c, y) => (
<Chips.Toggle
selected={selected.includes(y)}
@@ -81,7 +81,7 @@ export const Toggle = () => {
</Chips.Toggle>
))}
</Chips>
<Chips>
<Chips size={size}>
{options.map((c, y) => (
<Chips.Toggle
variant="neutral"
@@ -102,6 +102,12 @@ export const Toggle = () => {
</div>
);
};
Toggle.argTypes = {
size: {
control: { type: "radio" },
options: ["medium", "small"],
},
};
export const ToggleNoCheckmark = () => {
const [selected, setSelected] = useState<number>(2);
@@ -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>
);
@@ -1,10 +1,10 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { Meta, StoryObj } from "@storybook/react";
import isSameDay from "date-fns/isSameDay";
import React, { useId, useState } from "react";
import { useDatepicker, useRangeDatepicker } from "..";
import { Button, HGrid, VStack } from "../..";
import { BodyLong, Button, HGrid, Modal, VStack } from "../..";
import DatePicker, { DatePickerProps } from "./DatePicker";
import isSameDay from "date-fns/isSameDay";
const disabledDays = [
new Date("Oct 10 2022"),
@@ -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>
);
@@ -432,3 +430,35 @@ export const WeekDayClick = () => {
</VStack>
);
};
export const ModalDemo = () => {
const { datepickerProps, inputProps } = useDatepicker({
fromDate: new Date("Aug 23 2019"),
toDate: new Date("Feb 23 2024"),
onDateChange: console.log,
});
return (
<Modal open header={{ heading: "Modal-demo" }}>
<Modal.Body style={{ position: "relative" }}>
<BodyLong spacing>
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
</BodyLong>
<DatePicker {...datepickerProps} dropdownCaption>
<DatePicker.Input
{...inputProps}
label="Velg dato"
description="Format: dd.mm.yyyy"
/>
</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>
);
@@ -1,8 +1,10 @@
import { Meta, StoryFn } from "@storybook/react";
import setYear from "date-fns/setYear";
import React, { useId, useState } from "react";
import { Button, DateInputProps } from "../..";
import { useMonthpicker } from "../hooks";
import MonthPicker, { MonthPickerProps } from "./MonthPicker";
import MonthPicker from "./MonthPicker";
import { MonthPickerProps } from "./types";
export default {
title: "ds-react/Monthpicker",
@@ -147,3 +149,28 @@ export const UserControlled = () => {
</div>
);
};
export const FollowYear = () => {
const { monthpickerProps, inputProps, selectedMonth, setSelected } =
useMonthpicker({
fromDate: new Date("Aug 23 2019"),
toDate: new Date("Aug 23 2025"),
onMonthChange: console.log,
});
const customYearChange = (yearDate?: Date) => {
monthpickerProps.onYearChange?.(yearDate);
if (selectedMonth && yearDate) {
setSelected(setYear(selectedMonth, yearDate.getFullYear()));
}
};
return (
<div className="min-h-96">
<MonthPicker {...monthpickerProps} onYearChange={customYearChange}>
<MonthPicker.Input {...inputProps} label="Velg månede" />
</MonthPicker>
{selectedMonth && <div className="pt-4">{selectedMonth.getMonth()}</div>}
</div>
);
};
@@ -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,80 @@
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={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,13 +88,28 @@ 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 &&
!isValueInList(searchTerm, filteredOptions)
filteredOptions.length > 0
) {
setValue(
`${searchTerm}${filteredOptions[0].substring(searchTerm.length)}`
@@ -116,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;
@@ -152,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,
@@ -257,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"> {
@@ -31,44 +32,55 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
allowNewValues,
currentOption,
filteredOptions,
isValueNew,
toggleIsListOpen,
isListOpen,
filteredOptionsIndex,
moveFocusUp,
moveFocusDown,
ariaDescribedBy,
moveFocusToInput,
moveFocusToEnd,
setFilteredOptionsIndex,
setIsMouseLastUsedInputDevice,
shouldAutocomplete,
virtualFocus,
} = useFilteredOptionsContext();
const onEnter = useCallback(
(event: React.KeyboardEvent) => {
const isTextInSelectedOptions = (text: string) => {
return selectedOptions.find(
(item) => item.toLocaleLowerCase() === text.toLocaleLowerCase()
);
};
if (currentOption) {
event.preventDefault();
// Selecting a value from the dropdown / FilteredOptions
toggleOption(currentOption, event);
if (!isMultiSelect && !selectedOptions.includes(currentOption))
if (!isMultiSelect && !isTextInSelectedOptions(currentOption)) {
toggleIsListOpen(false);
} else if (shouldAutocomplete && selectedOptions.includes(value)) {
}
} else if (shouldAutocomplete && isTextInSelectedOptions(value)) {
event.preventDefault();
// Trying to set the same value that is already set, so just clearing the input
clearInput(event);
} else if ((allowNewValues || shouldAutocomplete) && value !== "") {
event.preventDefault();
// Autocompleting or adding a new value
toggleOption(value, event);
if (!isMultiSelect && !selectedOptions.includes(value))
const selectedValue =
allowNewValues && isValueNew ? value : filteredOptions[0];
toggleOption(selectedValue, event);
if (
!isMultiSelect &&
!isTextInSelectedOptions(filteredOptions[0] || selectedValue)
) {
toggleIsListOpen(false);
}
}
},
[
allowNewValues,
clearInput,
currentOption,
filteredOptions,
isMultiSelect,
isValueNew,
selectedOptions,
shouldAutocomplete,
toggleIsListOpen,
@@ -89,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;
@@ -113,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();
}
}
},
@@ -128,11 +146,11 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
value,
selectedOptions,
removeSelectedOption,
moveFocusDown,
isListOpen,
filteredOptionsIndex,
moveFocusUp,
activeDecendantId,
setIsMouseLastUsedInputDevice,
toggleIsListOpen,
virtualFocus,
]
);
@@ -144,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 (
@@ -165,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"}
@@ -467,6 +467,50 @@ export const TestThatCallbacksOnlyFireWhenExpected: StoryObj<{
},
};
export const TestCasingWhenAutoCompleting = {
args: {
onChange: jest.fn(),
onClear: jest.fn(),
onToggleSelected: jest.fn(),
},
render: (props) => {
return (
<UNSAFE_Combobox
options={["Camel Case", "lowercase", "UPPERCASE"]}
label="Liker du best store eller små bokstaver?"
shouldAutocomplete
allowNewValues
{...props}
/>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByRole<HTMLInputElement>("combobox");
// With exisiting option
userEvent.click(input);
await userEvent.type(input, "cAmEl CaSe", { delay: 250 });
await sleep(250);
expect(input.value).toBe("cAmEl CaSe");
await userEvent.type(input, "{Enter}");
await sleep(250);
const chips = canvas.getAllByRole("list")[0];
const selectedUpperCaseChip = within(chips).getAllByRole("listitem")[0];
expect(selectedUpperCaseChip).toHaveTextContent("Camel Case"); // A weird issue is preventing the accessible name from being used in the test, even if it works in VoiceOver
// With custom option
userEvent.click(input);
await userEvent.type(input, "cAmEl{Backspace}", { delay: 250 });
await sleep(250);
expect(input.value).toBe("cAmEl");
await userEvent.type(input, "{Enter}");
await sleep(250);
const selectedNewValueChip = within(chips).getAllByRole("listitem")[0];
expect(selectedNewValueChip).toHaveTextContent("cAmEl"); // A weird issue is preventing the accessible name from being used in the test, even if it works in VoiceOver
},
};
export const TestHoverAndFocusSwitching: StoryObject = {
render: () => {
return (
@@ -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,10 +1,7 @@
import cl from "clsx";
import React, { forwardRef } from "react";
import {
ResponsiveProp,
SpacingScale,
getResponsiveProps,
} from "../utilities/css";
import { getResponsiveProps } from "../utilities/css";
import { ResponsiveProp, SpacingScale } from "../utilities/types";
import { Slot } from "../../util/Slot";
export type BleedSpacingInline = "0" | "full" | "px" | SpacingScale;
+1 -3
View File
@@ -1,13 +1,11 @@
import cl from "clsx";
import React, { forwardRef } from "react";
import { OverridableComponent } from "../../util/OverridableComponent";
import { getResponsiveProps } from "../utilities/css";
import {
SpaceDelimitedAttribute,
ResponsiveProp,
SpacingScale,
getResponsiveProps,
} from "../utilities/css";
import {
BackgroundToken,
BorderColorToken,
ShadowToken,
+2 -6
View File
@@ -1,11 +1,7 @@
import React, { forwardRef, HTMLAttributes } from "react";
import cl from "clsx";
import {
getResponsiveProps,
getResponsiveValue,
ResponsiveProp,
SpacingScale,
} from "../utilities/css";
import { getResponsiveProps, getResponsiveValue } from "../utilities/css";
import { ResponsiveProp, SpacingScale } from "../utilities/types";
export interface HGridProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
@@ -1,6 +1,6 @@
import cl from "clsx";
import React, { forwardRef, HTMLAttributes } from "react";
import { BreakpointsAlias } from "../utilities/css";
import { BreakpointsAlias } from "../utilities/types";
import { Slot } from "../../util/Slot";
export interface ResponsiveProps extends HTMLAttributes<HTMLDivElement> {
@@ -1,8 +1,8 @@
import React from "react";
import { Link } from "../../link";
import { Heading, Label } from "../../typography";
import { Box } from "../box";
import { VStack } from "../stack";
import { Link } from "../../link";
const LinkElement = ({ children }) => {
return (
@@ -1,12 +1,8 @@
import cl from "clsx";
import React, { forwardRef, HTMLAttributes } from "react";
import { OverridableComponent } from "../../util/OverridableComponent";
import {
getResponsiveProps,
getResponsiveValue,
ResponsiveProp,
SpacingScale,
} from "../utilities/css";
import { getResponsiveProps, getResponsiveValue } from "../utilities/css";
import { ResponsiveProp, SpacingScale } from "../utilities/types";
export interface StackProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
+1 -36
View File
@@ -1,39 +1,4 @@
export type BreakpointsAlias = "xs" | "sm" | "md" | "lg" | "xl";
export type SpacingScale =
| "0"
| "05"
| "1"
| "1-alt"
| "2"
| "3"
| "4"
| "5"
| "6"
| "7"
| "8"
| "9"
| "10"
| "11"
| "12"
| "14"
| "16"
| "18"
| "20"
| "24"
| "32";
export type SpaceDelimitedAttribute<T extends string> =
| T
| `${T} ${T}`
| `${T} ${T} ${T}`
| `${T} ${T} ${T} ${T}`;
type FixedResponsiveT<T> = {
[Breakpoint in BreakpointsAlias]?: T;
};
export type ResponsiveProp<T> = T | FixedResponsiveT<T>;
import { ResponsiveProp } from "./types";
export function getResponsiveValue<T = string>(
componentName: string,
@@ -3,6 +3,7 @@ import surfaceColors from "@navikt/ds-tokens/src/colors-surface.json";
import borderColors from "@navikt/ds-tokens/src/colors-border.json";
import borderRadii from "@navikt/ds-tokens/src/border.json";
import shadows from "@navikt/ds-tokens/src/shadow.json";
import Spacing from "@navikt/ds-tokens/src/spacing.json";
export type BackgroundToken =
| keyof typeof bgColors.a
@@ -12,3 +13,18 @@ export type BorderRadiiToken =
| keyof (typeof borderRadii.a)["border-radius"]
| "0";
export type ShadowToken = keyof typeof shadows.a.shadow;
export type BreakpointsAlias = "xs" | "sm" | "md" | "lg" | "xl";
export type SpacingScale = keyof (typeof Spacing)["a"]["spacing"];
export type SpaceDelimitedAttribute<T extends string> =
| T
| `${T} ${T}`
| `${T} ${T} ${T}`
| `${T} ${T} ${T} ${T}`;
type FixedResponsiveT<T> = {
[Breakpoint in BreakpointsAlias]?: T;
};
export type ResponsiveProp<T> = T | FixedResponsiveT<T>;
+8 -2
View File
@@ -2,7 +2,7 @@ import cl from "clsx";
import React, { forwardRef, SVGProps } from "react";
import { omit, useId } from "../util";
export interface LoaderProps extends SVGProps<SVGSVGElement> {
export interface LoaderProps extends Omit<SVGProps<SVGSVGElement>, "ref"> {
/**
* Changes Loader width/height
* 88px | 64px | 40px | 32px | 24px | 20px | 16px
@@ -31,6 +31,12 @@ export interface LoaderProps extends SVGProps<SVGSVGElement> {
* @default "neutral"
*/
variant?: "neutral" | "interaction" | "inverted";
/**
* Allows getting a ref to the component instance.
* Once the component unmounts, React will set `ref.current` to `null` (or call the ref with `null` if you passed a callback ref).
* @see https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom
*/
ref?: React.Ref<SVGSVGElement>;
}
/* Workaround for @types/react v17/v18 feil */
@@ -46,7 +52,7 @@ export type LoaderType = React.ForwardRefExoticComponent<
*
* @example
* ```jsx
* <Loader size="3xlarge" title="venter..." />
* <Loader size="3xlarge" title="Venter..." />
* ```
*/
export const Loader: LoaderType = forwardRef<SVGSVGElement, LoaderProps>(
@@ -1,6 +1,6 @@
import React from "react";
import { Loader } from "..";
import { Meta } from "@storybook/react";
import React from "react";
import { Loader, LoaderProps } from "..";
export default {
title: "ds-react/Loader",
component: Loader,
@@ -29,7 +29,7 @@ export default {
} as Meta;
export const Default = {
render: (props: any) => {
render: (props: LoaderProps) => {
return <Loader {...props} />;
},
+54 -21
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";
@@ -82,11 +83,13 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
open,
onBeforeClose,
onCancel,
closeOnBackdropClick,
width,
portal,
className,
"aria-labelledby": ariaLabelledby,
style,
onClick,
...rest
}: ModalProps,
ref
@@ -97,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");
}
@@ -108,6 +113,11 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
if (needPolyfill && modalRef.current && portalNode) {
dialogPolyfill.registerDialog(modalRef.current);
}
// We set autofocus on the dialog element to prevent the default behavior where first focusable element gets focus when modal is opened.
// This is mainly to fix an edge case where having a Tooltip as the first focusable element would make it activate when you open the modal.
// We have to use JS because it doesn't work to set it with a prop (React bug?)
// Currently doesn't seem to work in Chrome. See also Tooltip.tsx
if (modalRef.current && portalNode) modalRef.current.autofocus = true;
}, [modalRef, portalNode]);
useEffect(() => {
@@ -128,34 +138,57 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
const isWidthPreset =
typeof width === "string" && ["small", "medium"].includes(width);
const mergedClassName = cl("navds-modal", className, {
"navds-modal--polyfilled": needPolyfill,
"navds-modal--autowidth": !width,
[`navds-modal--${width}`]: isWidthPreset,
});
const mergedStyle = {
...style,
...(!isWidthPreset ? { width } : {}),
};
const mergedOnCancel: React.DialogHTMLAttributes<HTMLDialogElement>["onCancel"] =
(event) => {
if (onBeforeClose && onBeforeClose() === false) {
event.preventDefault();
} else if (onCancel) onCancel(event);
};
const mergedOnClick =
closeOnBackdropClick && !needPolyfill // closeOnBackdropClick has issues on polyfill when nesting modals (DatePicker)
? (event: React.MouseEvent<HTMLDialogElement>) => {
onClick && onClick(event);
if (
event.target === modalRef.current &&
(!onBeforeClose || onBeforeClose() !== false)
) {
modalRef.current.close();
}
}
: onClick;
const mergedAriaLabelledBy =
!ariaLabelledby && !rest["aria-label"] && header
? ariaLabelId
: ariaLabelledby;
const component = (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions
<dialog
{...rest}
ref={mergedRef}
className={cl("navds-modal", className, {
"navds-modal--polyfilled": needPolyfill,
"navds-modal--autowidth": !width,
[`navds-modal--${width}`]: isWidthPreset,
})}
style={{
...style,
...(!isWidthPreset ? { width } : {}),
}}
onCancel={(event) => {
// FYI: onCancel fires when you press Esc
if (onBeforeClose && onBeforeClose() === false) {
event.preventDefault();
} else if (onCancel) onCancel(event);
}}
aria-labelledby={
!ariaLabelledby && !rest["aria-label"] && header
? ariaLabelId
: ariaLabelledby
}
className={mergedClassName}
style={mergedStyle}
onCancel={mergedOnCancel} // FYI: onCancel fires when you press Esc
onClick={mergedOnClick}
aria-labelledby={mergedAriaLabelledBy}
>
<ModalContext.Provider
value={{
closeHandler: getCloseHandler(modalRef, header, onBeforeClose),
ref: modalRef,
}}
>
{header && (
@@ -2,5 +2,6 @@ import React from "react";
interface ModalContextProps {
closeHandler?: React.MouseEventHandler<HTMLButtonElement>;
ref: React.RefObject<HTMLDialogElement>;
}
export const ModalContext = React.createContext<ModalContextProps | null>(null);
+30 -2
View File
@@ -1,6 +1,6 @@
import React, { useRef, useState } from "react";
import { FileIcon } from "@navikt/aksel-icons";
import { BodyLong, Button, Heading } from "..";
import React, { useRef, useState } from "react";
import { BodyLong, Button, Heading, Tooltip } from "..";
import Modal from "./Modal";
export default {
@@ -27,6 +27,7 @@ export const WithUseRef = () => {
heading: "Title",
size: "small",
}}
closeOnBackdropClick
>
<Modal.Body>
<BodyLong spacing>
@@ -45,6 +46,7 @@ export const WithUseRef = () => {
onBeforeClose={() =>
window.confirm("Are you sure you want to close the modal?")
}
closeOnBackdropClick
aria-labelledby="heading123"
>
<Modal.Header>
@@ -111,6 +113,7 @@ export const WithUseState = () => {
e.stopPropagation(); // onClose wil propagate to parent modal if not stopped
setOpen2(false);
}}
closeOnBackdropClick
aria-label="Nested modal"
width={800}
>
@@ -165,3 +168,28 @@ export const MediumWithPortal = () => (
<Modal.Body>Lorem ipsum dolor sit amet.</Modal.Body>
</Modal>
);
export const WithTooltip = () => {
const ref = useRef<HTMLDialogElement>(null);
return (
<div>
<Button onClick={() => ref.current?.showModal()}>Open Modal</Button>
<Modal
open={ref.current ? undefined : true /* initially open */}
ref={ref}
>
<Modal.Body>
<div style={{ marginBottom: "1rem" }}>
<Tooltip content="This_is_the_first_tooltip">
<Button>Test 1</Button>
</Tooltip>
</div>
<Tooltip content="This is the second tooltip">
<Button>Test 2</Button>
</Tooltip>
</Modal.Body>
</Modal>
</div>
);
};
+7
View File
@@ -42,6 +42,13 @@ export interface ModalProps
* Called when the user presses the Esc key, unless `onBeforeClose()` returns `false`.
*/
onCancel?: React.ReactEventHandler<HTMLDialogElement>;
/**
* Whether to close when clicking on the backdrop.
*
* **WARNING:** Users may click outside by accident. Don't use if closing can cause data loss, or the modal contains important info.
* @default false
*/
closeOnBackdropClick?: boolean;
/**
* @default fit-content (up to 700px)
* */
+6 -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,7 +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 = isInDatepicker ? false : _flip;
const {
x,
@@ -150,7 +147,8 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
onOpenChange: () => onClose(),
middleware: [
flOffset(offset ?? (arrow ? 16 : 4)),
_flip && flip({ padding: 5, fallbackPlacements: ["bottom", "top"] }),
chosenFlip &&
flip({ padding: 5, fallbackPlacements: ["bottom", "top"] }),
shift({ padding: 12 }),
flArrow({ element: arrowRef, padding: 8 }),
],
@@ -158,11 +156,7 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
const { getFloatingProps } = useInteractions([
useClick(context),
useDismiss(context, {
bubbles: {
escapeKey: bubbleEscape,
},
}),
useDismiss(context),
]);
useClientLayoutEffect(() => {
+19 -7
View File
@@ -1,8 +1,8 @@
import {
arrow as flArrow,
autoUpdate,
flip,
FloatingPortal,
autoUpdate,
arrow as flArrow,
flip,
offset,
safePolygon,
shift,
@@ -14,16 +14,18 @@ import {
} from "@floating-ui/react";
import cl from "clsx";
import React, {
HTMLAttributes,
cloneElement,
forwardRef,
HTMLAttributes,
useContext,
useMemo,
useRef,
useState,
} from "react";
import { ModalContext } from "../modal/ModalContext";
import { useProvider } from "../provider";
import { Detail } from "../typography";
import { mergeRefs, useId } from "../util";
import { useProvider } from "../provider";
export interface TooltipProps extends HTMLAttributes<HTMLDivElement> {
/**
@@ -110,7 +112,11 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
) => {
const [open, setOpen] = useState(defaultOpen);
const arrowRef = useRef<HTMLDivElement | null>(null);
const rootElement = useProvider()?.rootElement;
const modalContext = useContext(ModalContext);
const providerRootElement = useProvider()?.rootElement;
const rootElement = modalContext
? modalContext.ref.current
: providerRootElement;
const {
x,
@@ -133,7 +139,13 @@ export const Tooltip = forwardRef<HTMLDivElement, TooltipProps>(
flip({ padding: 5, fallbackPlacements: ["bottom", "top"] }),
flArrow({ element: arrowRef, padding: 5 }),
],
whileElementsMounted: autoUpdate,
whileElementsMounted: modalContext
? (reference, floating, update) =>
// Reduces jumping in Chrome when used in a Modal and it's the first focusable element.
// Can be removed when autofocus starts working on <dialog> in Chrome. See also Modal.tsx
autoUpdate(reference, floating, update, { animationFrame: true })
: autoUpdate,
strategy: modalContext ? "fixed" : undefined,
});
const { getReferenceProps, getFloatingProps } = useInteractions([
@@ -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;
};
+10
View File
@@ -1,5 +1,15 @@
# @navikt/ds-tailwind
## 5.9.0
## 5.8.0
## 5.7.6
## 5.7.5
## 5.7.4
## 5.7.3
## 5.7.2
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tailwind",
"version": "5.7.3",
"version": "5.9.0",
"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.7.3",
"@navikt/ds-tokens": "^5.9.0",
"@types/jest": "^29.0.0",
"color": "4.2.3",
"jest": "^29.0.0",
+10
View File
@@ -1,5 +1,15 @@
# @navikt/ds-tokens
## 5.9.0
## 5.8.0
## 5.7.6
## 5.7.5
## 5.7.4
## 5.7.3
### Patch Changes
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tokens",
"version": "5.7.3",
"version": "5.9.0",
"description": "Design-tokens for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
+308 -242
View File
File diff suppressed because it is too large. Load diff
+5 -5
View File
@@ -15,11 +15,11 @@
"website"
],
"dependencies": {
"@navikt/aksel-icons": "^5.7.3",
"@navikt/ds-css": "^5.7.3",
"@navikt/ds-react": "^5.7.3",
"@navikt/ds-tailwind": "^5.7.3",
"@navikt/ds-tokens": "^5.7.3"
"@navikt/aksel-icons": "^5.9.0",
"@navikt/ds-css": "^5.9.0",
"@navikt/ds-react": "^5.9.0",
"@navikt/ds-tailwind": "^5.9.0",
"@navikt/ds-tokens": "^5.9.0"
},
"devDependencies": {
"prettier-plugin-tailwindcss": "^0.2.3"
@@ -1,3 +1,4 @@
import { amplitudeLogNavigation } from "@/logging";
import { BodyLong, Detail, Heading, Link } from "@navikt/ds-react";
import {
PortableText,
@@ -5,30 +6,26 @@ import {
PortableTextReactComponents,
} from "@portabletext/react";
import cl from "clsx";
import ExpansionCard from "components/sanity-modules/ExpansionCard";
import Accordion from "components/sanity-modules/accordion/Accordion";
import Alert from "components/sanity-modules/alert/Alert";
import Bilde from "components/sanity-modules/bilde/Bilde";
import InnholdsKort from "components/sanity-modules/cards/InnholdsKort";
import { InlineCode } from "components/website-modules/InlineCode";
import { KBD } from "components/website-modules/KBD";
import CodeExamples from "components/sanity-modules/code-examples/CodeExamples";
import SnippetLazy from "components/sanity-modules/code-snippet/SnippetLazy";
import DoDont from "components/sanity-modules/do-dont/DoDont";
import ExpansionCard from "components/sanity-modules/expansioncard/ExpansionCard";
import PropsSeksjon from "components/sanity-modules/props/PropsSeksjon";
import RelatertInnhold from "components/sanity-modules/relatert-innhold/RelatertInnhold";
import SideModul from "components/sanity-modules/side-modul/SideModul";
import Tabell from "components/sanity-modules/tabell/Tabell";
import TastaturModul from "components/sanity-modules/tastatur-tabell/TastaturTabell";
import Tips from "components/sanity-modules/tips/Tips";
import TokenTable from "components/sanity-modules/token-tabell/TokenTable";
import Video from "components/sanity-modules/video/Video";
import InlineCode from "components/website-modules/InlineCode";
import KBD from "components/website-modules/KBD";
import NextLink from "next/link";
import { Children } from "react";
import {
Accordion,
Alert,
Bilde,
CodeExamples,
DoDont,
PropsSeksjon,
RelatertInnhold,
SideModul,
Snippet,
Tabell,
TastaturModul,
Tips,
TokenTable,
UuFeedback,
Video,
logNav,
} from ".";
const serializers: Partial<PortableTextReactComponents> = {
types: {
@@ -40,7 +37,7 @@ const serializers: Partial<PortableTextReactComponents> = {
bilde: ({ value }) => <Bilde node={value} />,
alert: ({ value }) => <Alert node={value} />,
expansioncard: ({ value }) => <ExpansionCard node={value} />,
kode: ({ value }) => <Snippet node={value} />,
kode: ({ value }) => <SnippetLazy node={value} />,
tabell_v2: ({ value }) => <Tabell node={value} />,
accordion: ({ value }) => <Accordion node={value} />,
props_seksjon: ({ value }) => <PropsSeksjon node={value} />,
@@ -49,7 +46,6 @@ const serializers: Partial<PortableTextReactComponents> = {
video: ({ value }) => <Video node={value} />,
tips: ({ value }) => <Tips node={value} />,
kode_eksempler: ({ value }) => <CodeExamples node={value} />,
uufeedback: ({ value }) => <UuFeedback node={value} />,
},
unknownType: () => null,
block: {
@@ -130,14 +126,11 @@ const serializers: Partial<PortableTextReactComponents> = {
return (
<Link
as={NextLink}
href={href}
inlineText
onClick={(e) =>
logNav(
"link",
window.location.pathname,
e.currentTarget.getAttribute("href")
)
amplitudeLogNavigation("link", e.currentTarget.getAttribute("href"))
}
{...(href.startsWith("http") &&
!href.startsWith("https://aksel.nav.no/")
@@ -160,11 +153,7 @@ const serializers: Partial<PortableTextReactComponents> = {
href={href}
inlineText
onClick={(e) =>
logNav(
"link",
window.location.pathname,
e.currentTarget.getAttribute("href")
)
amplitudeLogNavigation("link", e.currentTarget.getAttribute("href"))
}
>
{text}
-18
View File
@@ -1,22 +1,4 @@
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";
@@ -4,7 +4,6 @@ import { capitalize } from "@/utils";
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";
@@ -111,12 +110,9 @@ export const WithSidebar = ({
</div>
<div className={cl("sm:px-6 md:px-10", { flex: variant === "page" })}>
{variant === "page" && (
<TableOfContentsv2 changedState={pageProps["content"]} />
)}
{variant === "page" && <TableOfContentsv2 />}
<div className="w-full">
{children}
<Feedback docId={pageProps?._id} docType={pageProps?._type} />
{footer && <div className="w-full">{footer}</div>}
</div>
</div>
@@ -1,7 +1,7 @@
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 { logNav } from "components/website-modules/utils/amplitude";
import dynamic from "next/dynamic";
/* import FooterForm from "./FooterForm"; */
@@ -119,11 +119,7 @@ function FooterLink({ children, href }) {
className="text-text-on-inverted focus:shadow-focus focus:text-text-default flex w-fit items-center gap-1 underline hover:no-underline focus:bg-blue-200 focus:shadow-blue-200"
href={href}
onClick={(e) =>
logNav(
"footer",
window.location.pathname,
e.currentTarget.getAttribute("href")
)
amplitudeLogNavigation("footer", e.currentTarget.getAttribute("href"))
}
>
{children}
@@ -1,11 +1,11 @@
import { MenuHamburgerIcon } from "@navikt/aksel-icons";
import { amplitudeLogNavigation } from "@/logging";
import { Modal } from "@navikt/ds-react";
import cl from "clsx";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { logNav } from "../..";
const LinkElement = ({ name, href, onClick }) => {
const { asPath } = useRouter();
@@ -23,9 +23,8 @@ const LinkElement = ({ name, href, onClick }) => {
}
)}
onClick={(e) => {
logNav(
amplitudeLogNavigation(
"header",
window.location.pathname,
e.currentTarget.getAttribute("href")
);
onClick();
@@ -1,12 +1,12 @@
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 { logNav } from "../..";
import { Button } from "@navikt/ds-react";
import dynamic from "next/dynamic";
import { MagnifyingGlassIcon } from "@navikt/aksel-icons";
export const GlobalSearch = dynamic(() => import("./search"), {
loading: () => (
@@ -46,11 +46,7 @@ const LinkElement = ({ name, href, prefetch = undefined }) => {
}
)}
onClick={(e) =>
logNav(
"header",
window.location.pathname,
e.currentTarget.getAttribute("href")
)
amplitudeLogNavigation("header", e.currentTarget.getAttribute("href"))
}
>
{name}
@@ -87,9 +83,8 @@ export const Header = ({
href="/"
passHref
onClick={(e) =>
logNav(
amplitudeLogNavigation(
"header",
window.location.pathname,
e.currentTarget.getAttribute("href")
)
}
@@ -1,7 +1,7 @@
import { searchOptions } from "@/types";
import { XMarkIcon } from "@navikt/aksel-icons";
import { Button, Chips, Search } from "@navikt/ds-react";
import { KBD } from "components/website-modules/KBD";
import KBD from "components/website-modules/KBD";
import { useContext, useRef } from "react";
import { useShortcut } from "./hooks";
import {
@@ -3,7 +3,7 @@ import { SearchHitT, searchOptions } from "@/types";
import { ChevronRightIcon } from "@navikt/aksel-icons";
import { Chips } from "@navikt/ds-react";
import cl from "clsx";
import { Tag } from "components/sanity-modules/frontpage-blocks/Tag";
import { Tag } from "components/sanity-modules/frontpage-blocks/latest-articles/Tag";
import { StatusTag } from "components/website-modules/StatusTag";
import Image from "next/legacy/image";
import { default as Link, default as NextLink } from "next/link";
@@ -1,7 +1,7 @@
import { GroupedHitsT, SearchHitT, searchOptions } from "@/types";
import { Heading } from "@navikt/ds-react";
import cl from "clsx";
import { Tag } from "components/sanity-modules/frontpage-blocks/Tag";
import { Tag } from "components/sanity-modules/frontpage-blocks/latest-articles/Tag";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Hit } from "./Hit";
@@ -1,10 +1,10 @@
import { amplitude, AmplitudeEvents } from "@/logging";
import { allArticleDocuments } from "@/sanity/config";
import { SearchResultsT } from "@/types";
import { debounce } from "@navikt/ds-react";
import { useMemo, useState } from "react";
import useSWRImmutable from "swr/immutable";
import { createSearchResult, formatResults, fuseSearch } from "../utils";
import { debounce } from "@navikt/ds-react";
import { logSearch } from "@/utils";
export const useSearch = () => {
const [fuseResults, setFuseResults] = useState<SearchResultsT>(null);
@@ -35,7 +35,7 @@ export const useSearch = () => {
...createSearchResult(formatedResults, rawResults),
query: value,
});
logSearch({
amplitude.track(AmplitudeEvents.søk, {
type: "standard",
searchedFromUrl: window.location.pathname,
query: value,
@@ -1,4 +1,4 @@
import { logSearch } from "@/utils";
import { AmplitudeEvents, amplitude } from "@/logging";
import { createContext, useCallback, useContext } from "react";
import { useSearch } from "../hooks";
import { SearchContext } from "./SearchProvider";
@@ -32,7 +32,7 @@ export const SearchLoggingProvider = ({
accuracy: (100 - index / context?.results?.totalHits).toFixed(0),
topResult: index <= context?.results?.topResults?.length,
};
logSearch(data);
amplitude.track(AmplitudeEvents.søk, data);
},
[
query,
@@ -1,5 +1,5 @@
import { amplitudeLogNavigation } from "@/logging";
import { AkselSidebarT } from "@/types";
import { logNav } from "@/utils";
import {
ChevronDownIcon,
SparklesIcon,
@@ -15,22 +15,19 @@ import { useSection } from "./useSection";
const NavItem = ({
link,
asPath,
}: {
link: { heading: string; slug: string; kategori: string; tag?: string };
asPath: string;
}) => {
const isActive = asPath === `/${link.slug}`;
const { asPath } = useRouter();
const isActive = asPath.split("#")[0] === `/${link.slug}`;
return (
<li>
<Link
aria-current={isActive ? "page" : undefined}
href={`/${link.slug}`}
onClick={(e) => {
logNav(
"meny",
window.location.pathname,
e.currentTarget.getAttribute("href")
);
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",
@@ -81,7 +78,6 @@ const Dropdown = ({
links: AkselSidebarT;
}) => {
const [open, setOpen] = useState(true);
const { asPath } = useRouter();
return (
<li className="peer w-full peer-data-[open=true]:mt-6" data-open={open}>
@@ -107,7 +103,7 @@ const Dropdown = ({
<ul hidden={!open} className="pl-2">
{links.map((z) => (
<NavItem link={z} key={z?.heading} asPath={asPath} />
<NavItem link={z} key={z?.heading} />
))}
</ul>
</li>
@@ -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");
@@ -1,98 +0,0 @@
import { withErrorBoundary } from "@/error-boundary";
import { TokenTableT } from "@/types";
import { ChevronDownIcon } from "@navikt/aksel-icons";
import core from "@navikt/ds-css/tokens.json";
import { BodyLong, Button, CopyButton, Label, Link } from "@navikt/ds-react";
import cl from "clsx";
import { AkselTable, AkselTableRow } from "components/website-modules/Table";
import NextLink from "next/link";
import { useState } from "react";
const TokenTable = ({ node }: { node: TokenTableT }) => {
const [open, setOpen] = useState(false);
const tokens: { [key: string]: string } | null =
node.title in core ? core[node.title] : null;
if (!tokens) {
return null;
}
const showMore = Object.keys(tokens).length > 8;
return (
<div className="mb-7">
<div
className={cl({
"after:to-bg-default relative max-h-80 overflow-y-hidden after:absolute after:inset-0 after:bg-gradient-to-b after:from-transparent after:via-transparent":
!open && showMore,
})}
>
<AkselTable th={[{ text: "Token" }, { text: "Fallback" }]}>
{Object.entries(tokens).map(([key, val]) => (
<AkselTableRow key={key} tr={[{ text: key }, { text: val }]} />
))}
</AkselTable>
{(open || !showMore) && (
<div className="border-border-subtle bg-surface-default relative -mt-9 w-full rounded-b-lg border border-t-0 p-2 pr-14">
<CopyButton
copyText={Object.entries(tokens).reduce(
(prev, cur) =>
prev +
`${cur[0]}: ${
cur[1].startsWith("--") ? `var(${cur[1]})` : `${cur[1]}`
};\n`,
""
)}
className="absolute right-2 top-2 z-10"
/>
<Label size="small" as="span" spacing>
Hva er dette?
</Label>
<BodyLong size="small">
Komponent-tokens gir deg muligheten til å sette opp theming eller
justere styling uten å måtte overskrive css-klasser. Les gjennom{" "}
<NextLink
href="/grunnleggende/styling/design-tokens#hec62d38bc813"
passHref
legacyBehavior
>
<Link>guiden vår</Link>
</NextLink>{" "}
eller utforsk alle{" "}
<NextLink
href="/grunnleggende/styling/design-tokens"
passHref
legacyBehavior
>
<Link>design tokens</Link>
</NextLink>
.
</BodyLong>
</div>
)}
</div>
{!open && showMore && (
<div className="bg-surface-default mx-auto grid w-fit -translate-y-[80%] place-content-center shadow-[0_0_8px_8px_var(--a-surface-default)]">
<Button
variant="secondary-neutral"
onClick={() => setOpen(true)}
className="group"
icon={
<ChevronDownIcon
className="group-hover:translate-y-05 transition-transform"
aria-hidden
fontSize="1.5rem"
/>
}
iconPosition="right"
>
Vis mer
</Button>
</div>
)}
</div>
);
};
export default withErrorBoundary(TokenTable, "Token-tabell");
@@ -1,83 +0,0 @@
import { FeedbackT } from "@/types";
import { IdContext } from "@/utils";
import { Button, Heading, Textarea } from "@navikt/ds-react";
import cl from "clsx";
import { useRouter } from "next/router";
import { useContext, useState } from "react";
export const UuFeedback = ({ node }: { node: { vis: boolean } }) => {
const [textValue, setTextValue] = useState("");
const [errorMsg, setErrorMsg] = useState<string | null>(null);
const [sent, setSent] = useState(false);
const idCtx = useContext(IdContext);
const { asPath, basePath } = useRouter();
if (!node?.vis) {
return null;
}
const fetchFeedback = () => {
const body: FeedbackT = {
message: textValue,
url: `${basePath}${asPath}`,
type: "uu",
docId: idCtx?.id,
};
fetch("/api/feedback", {
method: "POST",
body: JSON.stringify(body),
});
};
const handleSend = (e) => {
e.preventDefault();
if (textValue === "") {
setErrorMsg(
"Tilbakemeldingen kan ikke være tom. Legg til tekst i feltet."
);
return;
}
setErrorMsg(null);
setSent(true);
fetchFeedback();
setTextValue("");
};
return (
<div className="my-8">
{!sent && (
<form
className={cl(
"animate-fadeIn mt-4 flex w-full max-w-sm flex-col gap-4"
)}
>
<Textarea
error={errorMsg}
value={textValue}
onChange={(e) => setTextValue(e.target.value)}
maxLength={600}
minRows={3}
label="Tilbakemelding"
description="Ikke skriv inn navn eller andre personopplysninger"
autoComplete="off"
/>
<Button className="mr-auto" onClick={handleSend}>
Send inn tilbakemelding
</Button>
</form>
)}
<div aria-live="polite">
{sent && (
<>
<Heading size="small" as="p" className="mt-8">
Takk for tilbakemeldingen!
</Heading>
</>
)}
</div>
</div>
);
};
@@ -1,10 +1,13 @@
import { Accordion as DsAccordion } from "@navikt/ds-react";
import React from "react";
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { SanityBlockContent } from "@/sanity-block";
import { AccordionT } from "@/types";
import { Accordion as DsAccordion } from "@navikt/ds-react";
const Accordion = ({ node }: { node: AccordionT }) => {
type AccordionProps = {
node: AccordionT;
};
const Accordion = ({ node }: AccordionProps) => {
if (!node || node.list.length === 0) {
return null;
}
@@ -25,4 +28,10 @@ const Accordion = ({ node }: { node: AccordionT }) => {
);
};
export default withErrorBoundary(Accordion, "Accordion");
export default function Component(props: AccordionProps) {
return (
<ErrorBoundary boundaryName="Accordion">
<Accordion {...props} />
</ErrorBoundary>
);
}
@@ -1,8 +1,13 @@
import ErrorBoundary from "@/error-boundary";
import { SanityBlockContent } from "@/sanity-block";
import { AlertT } from "@/types";
import { Alert as DsAlert, Heading } from "@navikt/ds-react";
const Alert = ({ node }: { node: AlertT }) => {
type AlertProps = {
node: AlertT;
};
const Alert = ({ node }: AlertProps) => {
return (
<div className="mb-7 max-w-2xl">
<DsAlert variant={node.variant}>
@@ -23,4 +28,10 @@ const Alert = ({ node }: { node: AlertT }) => {
);
};
export default Alert;
export default function Component(props: AlertProps) {
return (
<ErrorBoundary boundaryName="Alert">
<Alert {...props} />
</ErrorBoundary>
);
}
@@ -1,12 +1,17 @@
/* eslint-disable @next/next/no-img-element */
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { urlFor } from "@/sanity/interface";
import { BildeT } from "@/types";
import { BodyLong, Link } from "@navikt/ds-react";
import cl from "clsx";
import NextLink from "next/link";
const Bilde = ({ node, className }: { node: BildeT; className?: string }) => {
type BildeProps = {
node: BildeT;
className?: string;
};
const Bilde = ({ node, className }: BildeProps) => {
if (!node || !node.asset) {
return null;
}
@@ -65,4 +70,10 @@ const Bilde = ({ node, className }: { node: BildeT; className?: string }) => {
);
};
export default withErrorBoundary(Bilde, "Bilde");
export default function Component(props: BildeProps) {
return (
<ErrorBoundary boundaryName="Bilde">
<Bilde {...props} />
</ErrorBoundary>
);
}
@@ -1,5 +1,6 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { amplitudeLogNavigation } from "@/logging";
import {
AkselGodPraksisDocT,
ResolveContributorsSingleT,
@@ -9,7 +10,15 @@ import {
import { BodyShort, Detail, Heading } from "@navikt/ds-react";
import { useFormatedDate } from "components/website-modules/utils/getDate";
import NextLink from "next/link";
import { abbrName, logNav } from "../..";
import { abbrName } from "../..";
type ArtikkelCardProps = ResolveContributorsSingleT<
ResolveTemaT<ResolveSlugT<AkselGodPraksisDocT>>
> & {
source?: string;
variant: string;
level?: "2" | "3";
};
const ArtikkelCard = ({
slug,
@@ -23,9 +32,7 @@ const ArtikkelCard = ({
tema,
level = "2",
...rest
}: ResolveContributorsSingleT<
ResolveTemaT<ResolveSlugT<AkselGodPraksisDocT>>
> & { source?: string; variant: string; level?: "2" | "3" }) => {
}: ArtikkelCardProps) => {
const date = useFormatedDate(
(rest as any)?.updateInfo?.lastVerified ?? publishedAt ?? _updatedAt
);
@@ -42,9 +49,8 @@ const ArtikkelCard = ({
passHref
className="after:absolute after:inset-0 after:z-10 after:rounded-lg focus:outline-none"
onClick={(e) =>
logNav(
amplitudeLogNavigation(
"artikkel-kort",
window.location.pathname,
e.currentTarget.getAttribute("href")
)
}
@@ -89,4 +95,10 @@ const ArtikkelCard = ({
);
};
export default withErrorBoundary(ArtikkelCard, "ArtikkelCard");
export default function Component(props: ArtikkelCardProps) {
return (
<ErrorBoundary boundaryName="ArtikkelCard">
<ArtikkelCard {...props} />
</ErrorBoundary>
);
}
@@ -1,15 +1,16 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { amplitudeLogNavigation } from "@/logging";
import { AkselBloggDocT, ResolveContributorsT, ResolveSlugT } from "@/types";
import { getAuthors } from "@/utils";
import { BodyLong, BodyShort, Heading, Link } from "@navikt/ds-react";
import { useFormatedDate } from "components/website-modules/utils/getDate";
import NextLink from "next/link";
import { getAuthors, logNav } from "@/utils";
const BloggCard = ({
blog,
}: {
type BloggCardProps = {
blog: ResolveContributorsT<ResolveSlugT<AkselBloggDocT>>;
}) => {
};
const BloggCard = ({ blog }: BloggCardProps) => {
const date = useFormatedDate(blog?.publishedAt ?? blog._createdAt);
return (
<li
@@ -21,9 +22,8 @@ const BloggCard = ({
<Link
className="text-deepblue-500 underline hover:no-underline"
onClick={(e) =>
logNav(
amplitudeLogNavigation(
"blogg-card",
window.location.pathname,
e.currentTarget.getAttribute("href")
)
}
@@ -46,4 +46,10 @@ const BloggCard = ({
);
};
export default withErrorBoundary(BloggCard, "BloggCard");
export default function Component(props: BloggCardProps) {
return (
<ErrorBoundary boundaryName="ArtikkelCard">
<BloggCard {...props} />
</ErrorBoundary>
);
}
@@ -1,13 +1,17 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { amplitudeLogNavigation } from "@/logging";
import { urlFor } from "@/sanity/interface";
import { AkselTemaT } from "@/types";
import { logNav } from "@/utils";
import { BodyShort } from "@navikt/ds-react";
import cl from "clsx";
import Image from "next/legacy/image";
import NextLink from "next/link";
const GodPraksisCard = ({ node }: { node: AkselTemaT }) => {
type GodPraksisCardProps = {
node: AkselTemaT;
};
const GodPraksisCard = ({ node }: GodPraksisCardProps) => {
if (
!node?.pictogram ||
!node?.slug?.current ||
@@ -33,11 +37,7 @@ const GodPraksisCard = ({ node }: { node: AkselTemaT }) => {
href={`/god-praksis/${node.slug.current}`}
passHref
onClick={(e) =>
logNav(
"card",
window.location.pathname,
e.currentTarget.getAttribute("href")
)
amplitudeLogNavigation("card", e.currentTarget.getAttribute("href"))
}
className="navds-heading--medium text-deepblue-700 navds-heading mb-4 underline after:absolute after:inset-0 after:rounded-lg focus:outline-none group-hover:no-underline"
>
@@ -51,4 +51,10 @@ const GodPraksisCard = ({ node }: { node: AkselTemaT }) => {
);
};
export default withErrorBoundary(GodPraksisCard, "GodPraksisCard");
export default function Component(props: GodPraksisCardProps) {
return (
<ErrorBoundary boundaryName="GodPraksisCard">
<GodPraksisCard {...props} />
</ErrorBoundary>
);
}
@@ -1,11 +1,15 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { amplitudeLogNavigation } from "@/logging";
import { urlFor } from "@/sanity/interface";
import { AkselTemaT } from "@/types";
import { logNav } from "@/utils";
import Image from "next/legacy/image";
import NextLink from "next/link";
const GodPraksisCardSimple = ({ node }: { node: Partial<AkselTemaT> }) => {
type GodPraksisCardSimpleProps = {
node: Partial<AkselTemaT>;
};
const GodPraksisCardSimple = ({ node }: GodPraksisCardSimpleProps) => {
if (!node?.pictogram || !node?.slug?.current || !node?.title) {
return null;
}
@@ -26,11 +30,7 @@ const GodPraksisCardSimple = ({ node }: { node: Partial<AkselTemaT> }) => {
href={`/god-praksis/${node.slug.current}`}
passHref
onClick={(e) =>
logNav(
"card",
window.location.pathname,
e.currentTarget.getAttribute("href")
)
amplitudeLogNavigation("card", e.currentTarget.getAttribute("href"))
}
className="navds-heading--small navds-link text-deepblue-700 navds-heading flex-wrap break-all no-underline hover:underline focus:outline-none"
>
@@ -40,4 +40,10 @@ const GodPraksisCardSimple = ({ node }: { node: Partial<AkselTemaT> }) => {
);
};
export default withErrorBoundary(GodPraksisCardSimple, "GodPraksisCardSimple");
export default function Component(props: GodPraksisCardSimpleProps) {
return (
<ErrorBoundary boundaryName="GodPraksisCardSimple">
<GodPraksisCardSimple {...props} />
</ErrorBoundary>
);
}
@@ -1,11 +1,15 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { amplitudeLogNavigation } from "@/logging";
import { SanityBlockContent } from "@/sanity-block";
import { InnholdsKortPrinsipperT } from "@/types";
import { logNav } from "@/utils";
import { Heading } from "@navikt/ds-react";
import NextLink from "next/link";
const InnholdsKort = ({ node }: { node: InnholdsKortPrinsipperT }) => {
type InnholdsKortProps = {
node: InnholdsKortPrinsipperT;
};
const InnholdsKort = ({ node }: InnholdsKortProps) => {
if (!node.title || !node.body || !node.lenke) {
return null;
}
@@ -23,9 +27,9 @@ const InnholdsKort = ({ node }: { node: InnholdsKortPrinsipperT }) => {
href={`/${node?.lenke}`}
passHref
onClick={(e) =>
logNav(
amplitudeLogNavigation(
"prinsipp-kort",
window.location.pathname,
e.currentTarget.getAttribute("href")
)
}
@@ -39,4 +43,10 @@ const InnholdsKort = ({ node }: { node: InnholdsKortPrinsipperT }) => {
);
};
export default withErrorBoundary(InnholdsKort, "InnholdsKort");
export default function Component(props: InnholdsKortProps) {
return (
<ErrorBoundary boundaryName="InnholdsKort">
<InnholdsKort {...props} />
</ErrorBoundary>
);
}
@@ -1 +0,0 @@
export { default as RelatertInnhold } from "./RelatertInnhold";
@@ -1,6 +1,6 @@
import { getKey } from "@/sb-util";
import type { Meta, StoryObj } from "@storybook/react";
import CodeExample from "./Examples";
import CodeExample from "./CodeExamples";
const meta = {
title: "Sanity-modules/CodeExample",
@@ -1,5 +1,5 @@
import { capitalize, Snippet } from "@/components";
import { withErrorBoundary } from "@/error-boundary";
import { capitalize } from "@/components";
import ErrorBoundary from "@/error-boundary";
import { CodeExamplesT } from "@/types";
import {
ExternalLinkIcon,
@@ -8,14 +8,19 @@ import {
} from "@navikt/aksel-icons";
import { BodyLong, Button, Chips, HStack } from "@navikt/ds-react";
import cl from "clsx";
import SnippetLazy from "components/sanity-modules/code-snippet/SnippetLazy";
import { useRouter } from "next/router";
import { useEffect, useRef, useState } from "react";
import { CodeSandbox } from "./CodeSandbox";
import { CodeSandbox } from "./parts/CodeSandbox";
const iframePadding = 192;
const iframeId = "example-iframe";
const ComponentExamples = ({ node }: { node: CodeExamplesT }) => {
type CodeExamplesProps = {
node: CodeExamplesT;
};
const ComponentExamples = ({ node }: CodeExamplesProps) => {
const [activeExample, setActiveExample] = useState(null);
const [frameState, setFrameState] = useState(300);
const [unloaded, setUnloaded] = useState(true);
@@ -30,7 +35,7 @@ const ComponentExamples = ({ node }: { node: CodeExamplesT }) => {
) as HTMLIFrameElement;
const exampleIframeDOM = exampleIframe?.contentDocument;
const exampleWrapper = exampleIframeDOM?.getElementById("ds-example");
if (exampleWrapper) {
if (exampleWrapper && exampleWrapper.offsetHeight) {
const newHeight = iframePadding + exampleWrapper.offsetHeight;
clearInterval(waitForExampleContentToRender);
setFrameState(newHeight < 300 ? 300 : newHeight);
@@ -93,15 +98,12 @@ const ComponentExamples = ({ node }: { node: CodeExamplesT }) => {
value={fil.navn}
selected={active === fil.navn}
id={`${node.dir.title.toLowerCase()}demo-${fil.navn}`}
onClick={() => {
setActiveExample(fil.navn);
onClick={async () => {
setUnloaded(true);
router.replace(
`#${node.dir.title.toLowerCase()}demo-${fil.navn}`,
undefined,
{
shallow: true,
}
await router.replace(
`${
router.asPath.split("#")[0]
}#${node.dir.title.toLowerCase()}demo-${fil.navn}`
);
}}
>
@@ -206,7 +208,7 @@ const ComponentExamples = ({ node }: { node: CodeExamplesT }) => {
</HStack>
</div>
<Snippet
<SnippetLazy
node={{
code: { code: fil.innhold.trim(), language: "tsx" },
}}
@@ -220,4 +222,10 @@ const ComponentExamples = ({ node }: { node: CodeExamplesT }) => {
);
};
export default withErrorBoundary(ComponentExamples, "Eksempler kode");
export default function Component(props: CodeExamplesProps) {
return (
<ErrorBoundary boundaryName="Kodeeksempler">
<ComponentExamples {...props} />
</ErrorBoundary>
);
}
@@ -1 +0,0 @@
export { default as CodeExamples } from "./Examples";
@@ -31,7 +31,7 @@ export const CodeSandbox = ({ code }: { code: string }) => {
} as any,
isBinary: false,
},
"App.js": {
"App.tsx": {
content: getCode(code),
isBinary: false,
},
@@ -54,7 +54,7 @@ export const CodeSandbox = ({ code }: { code: string }) => {
className="h-8"
>
<input type="hidden" name="parameters" value={parameters} />
<input type="hidden" name="query" value="module=App.js" />
<input type="hidden" name="query" value="module=App.tsx" />
<Button
variant="tertiary-neutral"
size="small"
@@ -1,4 +1,4 @@
import { withErrorBoundary } from "@/error-boundary";
import ErrorBoundary from "@/error-boundary";
import { CodeSnippetT } from "@/types";
import { ChevronRightIcon, TerminalIcon } from "@navikt/aksel-icons";
import { CopyButton, Label } from "@navikt/ds-react";
@@ -6,7 +6,11 @@ import cl from "clsx";
import { Highlight, Language } from "prism-react-renderer";
import theme from "./theme";
const CodeSnippet = ({ node: { code, title } }: { node: CodeSnippetT }) => {
type CodeSnippetProps = {
node: CodeSnippetT;
};
const CodeSnippet = ({ node: { code, title } }: CodeSnippetProps) => {
if (!code || !code.code) {
return null;
}
@@ -94,4 +98,10 @@ const CodeSnippet = ({ node: { code, title } }: { node: CodeSnippetT }) => {
);
};
export default withErrorBoundary(CodeSnippet, "Kode snippet");
export default function Component(props: CodeSnippetProps) {
return (
<ErrorBoundary boundaryName="Kodesnippet">
<CodeSnippet {...props} />
</ErrorBoundary>
);
}
@@ -0,0 +1,7 @@
import dynamic from "next/dynamic";
const SnippetLazy = dynamic(() => import("./Snippet"), {
ssr: false,
});
export default SnippetLazy;
@@ -1,5 +0,0 @@
import dynamic from "next/dynamic";
export const Snippet = dynamic(() => import("./Snippet"), {
ssr: false,
});
Loaded 100 of 238 files, more files were not shown because too many files have changed in this diff. Show more