Compare commits

...
Author SHA1 Message Date
github-actions[bot]andgithub-actions[bot] e32dbe5c5d Version Packages (#2071)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-22 15:23:42 +02:00
Ken de36269582 ✨ Oppdaterte shadow-tokens (#2041) 2023-06-22 15:12:42 +02:00
Julian Nymark 6cb6c52770 Changelog generator script (#2046) 2023-06-22 15:10:03 +02:00
Ken a877d24ed0 🐛 Fikset corder-bleed på komponentside-header 2023-06-21 17:10:43 +02:00
github-actions[bot]andgithub-actions[bot] a1b6332fa1 Version Packages (#2070)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-21 13:48:45 +02:00
Ken 99c08b8e4a Fikset bug i Popover/Helptext der popover gikk helt ut i kantene (#2069) 2023-06-21 13:44:59 +02:00
Ken d39ed906c6 🐛 Fikset ExpansionCard bug ved nesting (#2067) 2023-06-21 13:40:48 +02:00
github-actions[bot]andgithub-actions[bot] 3b31bd3d75 Version Packages (#2066)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-21 11:59:33 +02:00
Halvor HauganandKen b1625ba388 ✨ Chat: ny small-variant, justert design, forbedret UU (#2048)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2023-06-21 09:40:50 +02:00
Ken 269d06c277 📝 ErrorSummary prop-dokumentasjon 2023-06-20 15:47:52 +02:00
Ken 0c56273f3b 🐛 Fikset rekkefølge på button-stories 2023-06-20 15:41:13 +02:00
Ken ab6a96aed7 ♻️ Oppdatert komponent demoer (#2063) 2023-06-20 11:46:45 +02:00
Ken 9ee8a72082 🔥 Fjernet død kode, lazy-loader sanity-moduler bare brukt for preview 2023-06-19 19:53:50 +02:00
Ken c2e5a92d8e 🎉 Oppdatert søk (#2062) 2023-06-19 18:28:24 +02:00
Ken 6059aa59aa 🐛 Oppdatert playwright-config for mindre flaky tester 2023-06-19 16:21:13 +02:00
Ken b877ada408 🐛 Fikset playwright-tester 2023-06-19 15:11:59 +02:00
Ken 4e339f183e 🔊 Oppdatert amplitude (#2061) 2023-06-19 14:09:08 +02:00
Ken 1d62bf26ff 🛠️ Diverse småjustering for Aksel (#2060) 2023-06-19 13:19:57 +02:00
github-actions[bot]andgithub-actions[bot] da0fbc3299 Version Packages (#2057)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-19 13:02:23 +02:00
Ken 9f59e67e2a ⚡ Lazy-loading date-fns: sparer ~10kB first-load (#2058) 2023-06-19 12:53:28 +02:00
Ken ed7aafcac8 🎨 Oppdatert deploy image (#2059) 2023-06-19 12:53:08 +02:00
Ken dfbbe92a63 🎨 Helptext har nå en prop for wrapperClassName (#2056) 2023-06-19 11:48:53 +02:00
Ken 42b5af64ab 🐛 Timeline Period brakk ved bruk av JSODOM 2023-06-17 10:25:45 +02:00
Ken 329119fa04 💄 DoDont-ikon er nå høyrestilt 2023-06-16 16:21:19 +02:00
Ken 0fc80f45cb ♿ Fikset regression der sanity ikke lengre støttet skjermlesere 2023-06-16 16:07:37 +02:00
Ken 337b9fda13 🐛 Fikset inlinecode bruk i heading 2023-06-16 12:58:26 +02:00
github-actions[bot]andgithub-actions[bot] 659e835da9 Version Packages (#2055)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-15 16:38:08 +02:00
Ken e10f4375b1 Merge branch 'main' of https://github.com/navikt/aksel 2023-06-15 16:34:25 +02:00
Ken 9a8bb26c36 🐛 Timeline brakk ved bruk av JSDOM-tester 2023-06-15 16:33:40 +02:00
github-actions[bot]andgithub-actions[bot] 69aea38784 Version Packages (#2053)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-15 15:55:48 +02:00
Ken 8f16d1571e 🎨 Oppdatert ikoner (#2054) 2023-06-15 15:51:47 +02:00
Marianne Røsvik e2265c9a78 Added two new icons (#2047) 2023-06-15 15:06:48 +02:00
Ken 925aa7a140 🐛 OverridableComponent omitter nå 'as' (#2051) 2023-06-15 14:46:34 +02:00
Ken 7c17e40cac 🐛 Datepicker og Monthpicker lukker nå popover ved escape (#2052) 2023-06-15 14:46:07 +02:00
github-actions[bot]andgithub-actions[bot] 3c8889ea88 Version Packages (#2050)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-13 16:49:54 +02:00
Ken e060961050 🐛 Fikset bruk av text-subtle på skjemaelementer (#2049) 2023-06-13 16:45:35 +02:00
Ken 18aa4ee7fb Merge branch 'main' of https://github.com/navikt/aksel 2023-06-09 14:36:38 +02:00
Ken f44edd76a2 Merge branch 'next' 2023-06-09 14:31:11 +02:00
github-actions[bot]andgithub-actions[bot] 1fa636bdd5 Version Packages (#2044)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-06-09 14:16:43 +02:00
Ken 44a75acb6b 🐛 className ble ikke riktig forwardet til period 2023-06-09 14:14:15 +02:00
Ken 550faee900 💄 Checkboard-pattern for transparent visning av tokens 2023-06-09 13:53:29 +02:00
Karl O 88b6174b0b 📝 La til next-logger så backend logging får rett log-level (#2042) 2023-06-09 13:34:31 +02:00
Ken fbf016b3bd ✨ Forbedringer for Aksel.nav.no UI (#2043) 2023-06-09 13:05:59 +02:00
09fa503b98 initial template for PR (#2019)
* initial template for PR

* update template

* update template

* 📝 Oppdatert template

---------

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2023-06-09 09:55:28 +02:00
Ken f5688485f5 ⬆️ Oppdatert Sanity 2023-06-08 18:43:14 +02:00
224 changed files with 7216 additions and 4851 deletions

No files matched your search

+17
View File
@@ -0,0 +1,17 @@
### :memo: PR Checklist
<!-- Remove non-relevant fields -->
- [ ] :sparkles: New component? Add CSS-mappings to `@navikt/core/css/config/_mappings.js`.
- [ ] :wastebasket: Are you deprecating a CSS class? (are you removing the last known reference to a CSS class?). Add it to `@navikt/aksel-stylelint/src/deprecations.ts` with a suitable description.
- [ ] :balance_scale: Is the feature/component/code sufficiently complex to warrant tests? Add some unit-tests :test_tube:
- [ ] :balance_scale: Does the feature/component add some complex user-interactions? Consider adding some [interaction-tests with storybook](https://storybook.js.org/docs/react/writing-tests/interaction-testing) :test_tube:
- [ ] :boom: Are there breaking changes? Consider adding a codemod for easier migration.
- [ ] :test_tube: Are all variations and cases shown in the storybook stories? Stories are used for [visual regression testing](https://www.chromatic.com/docs/), so make sure they are complete.
- [ ] Run `yarn changeset` for version and changelog generation
+1 -1
View File
@@ -13,7 +13,7 @@ jobs:
playwright:
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.33.0-focal
image: mcr.microsoft.com/playwright:v1.35.1-jammy
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
+4
View File
@@ -28,6 +28,10 @@ jobs:
node-version: 16.13.0
cache: yarn
- uses: denoland/setup-deno@v1
with:
deno-version: v1.x
- name: Restore cache
uses: actions/cache@v3
with:
+1 -1
View File
@@ -14,7 +14,7 @@ jobs:
name: Smoketest Aksel
runs-on: ubuntu-latest
container:
image: mcr.microsoft.com/playwright:v1.33.0-focal
image: mcr.microsoft.com/playwright:v1.35.1-jammy
steps:
- uses: actions/setup-node@v3
with:
+2 -1
View File
@@ -150,4 +150,5 @@ playwright-report/
/test-results/
backup-*.tar.gz
sitemap-urls.json
*.tar.gz
*.tar.gz
searchindex.json
+23
View File
@@ -1,5 +1,28 @@
# @navikt/aksel-icons
## 4.4.0
## 4.3.0
## 4.2.0
## 4.1.7
## 4.1.6
## 4.1.5
### Patch Changes
- [#2054](https://github.com/navikt/aksel/pull/2054) [`8f16d1571`](https://github.com/navikt/aksel/commit/8f16d1571ec7cf452c09618a9a918ee2a634c75a) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :tada: Nye ikoner
- ChevronRightLast
- ChevronLeftLast
## 4.1.4
## 4.1.3
## 4.1.2
## 4.1.1
+1 -1
View File
@@ -15,5 +15,5 @@ keywords:
- Sammenføye
- Kildekontroll
variant: Stroke
updated_at: 08.03.2023
updated_at: 24.05.2023
created_at: 07.03.2023
@@ -5,5 +5,5 @@ keywords:
- left
- venstre
variant: Stroke
updated_at: 08.03.2023
updated_at: 14.06.2023
created_at: 07.03.2023
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5 6.25C7.5 5.83579 7.16421 5.5 6.75 5.5C6.33579 5.5 6 5.83579 6 6.25V17.75C6 18.1642 6.33579 18.5 6.75 18.5C7.16421 18.5 7.5 18.1642 7.5 17.75V6.25ZM17.7803 7.03033C18.0732 6.73744 18.0732 6.26256 17.7803 5.96967C17.4874 5.67678 17.0126 5.67678 16.7197 5.96967L11.2197 11.4697C11.079 11.6103 11 11.8011 11 12C11 12.1989 11.079 12.3897 11.2197 12.5303L16.7197 18.0303C17.0126 18.3232 17.4874 18.3232 17.7803 18.0303C18.0732 17.7374 18.0732 17.2626 17.7803 16.9697L12.8107 12L17.7803 7.03033Z" fill="#262626"/>
</svg>

After

Width:  |  Height:  |  Size: 664 B

@@ -0,0 +1,12 @@
name: ChevronLeftFirst
category: Arrows
sub_category: Chevron
keywords:
- left
- venstre
- side
- first
- først
variant: Stroke
updated_at: 15.06.2023
created_at: 15.06.2023
@@ -6,5 +6,5 @@ keywords:
- høyre
- side
variant: Stroke
updated_at: 08.03.2023
updated_at: 09.06.2023
created_at: 07.03.2023
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M18 6.25C18 5.83579 17.6642 5.5 17.25 5.5C16.8358 5.5 16.5 5.83579 16.5 6.25V17.75C16.5 18.1642 16.8358 18.5 17.25 18.5C17.6642 18.5 18 18.1642 18 17.75V6.25ZM7.28033 6.21967C6.98744 5.92678 6.51256 5.92678 6.21967 6.21967C5.92678 6.51256 5.92678 6.98744 6.21967 7.28033L11.1893 12.25L6.21967 17.2197C5.92678 17.5126 5.92678 17.9874 6.21967 18.2803C6.51256 18.5732 6.98744 18.5732 7.28033 18.2803L12.7803 12.7803C13.0732 12.4874 13.0732 12.0126 12.7803 11.7197L7.28033 6.21967Z" fill="#262626"/>
</svg>

After

Width:  |  Height:  |  Size: 648 B

@@ -0,0 +1,10 @@
name: ChevronRightLast
category: Arrows
sub_category: Chevron
keywords:
- right
- høyre
- side
variant: Stroke
updated_at: 15.06.2023
created_at: 15.06.2023
+1 -1
View File
@@ -15,5 +15,5 @@ keywords:
- markup-språk
- kildekode
variant: Stroke
updated_at: 08.03.2023
updated_at: 24.05.2023
created_at: 07.03.2023
+1 -1
View File
@@ -13,5 +13,5 @@ keywords:
- dra og slippe
- skifte
variant: Stroke
updated_at: 08.03.2023
updated_at: 26.05.2023
created_at: 07.03.2023
+1 -1
View File
@@ -23,5 +23,5 @@ keywords:
- milepæler
- hendelser
variant: Stroke
updated_at: 08.03.2023
updated_at: 31.05.2023
created_at: 07.03.2023
+1 -1
View File
@@ -7,5 +7,5 @@ keywords:
- delete
- slett
variant: Stroke
updated_at: 08.03.2023
updated_at: 24.05.2023
created_at: 07.03.2023
+1 -1
View File
@@ -6,5 +6,5 @@ keywords:
- uploading
- send
variant: Stroke
updated_at: 08.03.2023
updated_at: 08.06.2023
created_at: 07.03.2023
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "4.1.2",
"version": "4.4.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
+26
View File
@@ -1,5 +1,31 @@
# @navikt/aksel-stylelint
## 4.4.0
## 4.3.0
## 4.2.0
### Minor Changes
- [#2048](https://github.com/navikt/aksel/pull/2048) [`b1625ba38`](https://github.com/navikt/aksel/commit/b1625ba3882b430d678d0623922d7fb56aa724b3) Thanks [@HalvorHaugan](https://github.com/HalvorHaugan)! - Oppdatert Chat
- :sparkles: Ny prop `size` som kan settes til "small" for en mer kompakt layout
- :sparkles: Ny prop `variant` som erstatter `backgroundColor` og `avatarBgColor`
- :sparkles: `avatar` er nå valgfritt
- :wheelchair: Forbedret UU
- :lipstick: Oppdatert utseende
## 4.1.7
## 4.1.6
## 4.1.5
## 4.1.4
## 4.1.3
## 4.1.2
## 4.1.1
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "4.1.2",
"version": "4.4.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": "^4.1.2",
"@navikt/ds-tokens": "^4.1.2",
"@navikt/ds-css": "^4.4.0",
"@navikt/ds-tokens": "^4.4.0",
"@types/jest": "^29.0.0",
"concurrently": "7.2.1",
"copyfiles": "2.4.1",
+56
View File
@@ -1,5 +1,61 @@
# @navikt/aksel
## 4.4.0
### Patch Changes
- Updated dependencies [[`de3626958`](https://github.com/navikt/aksel/commit/de3626958266d1271840fa341c36500ab66b573d)]:
- @navikt/ds-css@4.4.0
## 4.3.0
### Patch Changes
- Updated dependencies [[`99c08b8e4`](https://github.com/navikt/aksel/commit/99c08b8e4ab33acd20a21719986452652ffe3c57), [`d39ed906c`](https://github.com/navikt/aksel/commit/d39ed906c61149ac823ebab2e8974a105e592163)]:
- @navikt/ds-css@4.3.0
## 4.2.0
### Patch Changes
- Updated dependencies [[`b1625ba38`](https://github.com/navikt/aksel/commit/b1625ba3882b430d678d0623922d7fb56aa724b3)]:
- @navikt/ds-css@4.2.0
## 4.1.7
### Patch Changes
- Updated dependencies [[`42b5af64a`](https://github.com/navikt/aksel/commit/42b5af64ab35d0f2d126d41f8fc3e61fe2834b36)]:
- @navikt/ds-css@4.1.7
## 4.1.6
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@4.1.6
## 4.1.5
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@4.1.5
## 4.1.4
### Patch Changes
- Updated dependencies [[`e06096105`](https://github.com/navikt/aksel/commit/e060961050c75ec815e5c125001f1ff99106e298)]:
- @navikt/ds-css@4.1.4
## 4.1.3
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@4.1.3
## 4.1.2
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "4.1.2",
"version": "4.4.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": "4.1.2",
"@navikt/ds-css": "4.4.0",
"axios": "1.3.6",
"chalk": "4.1.0",
"clipboardy": "^2.3.0",
+46
View File
@@ -1,5 +1,51 @@
# @navikt/ds-css
## 4.4.0
### Minor Changes
- [#2041](https://github.com/navikt/aksel/pull/2041) [`de3626958`](https://github.com/navikt/aksel/commit/de3626958266d1271840fa341c36500ab66b573d) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Shadow-tokens er oppdatert til mer tydeligere varianter
## 4.3.0
### Minor Changes
- [#2069](https://github.com/navikt/aksel/pull/2069) [`99c08b8e4`](https://github.com/navikt/aksel/commit/99c08b8e4ab33acd20a21719986452652ffe3c57) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Popover og Helptext har nå luft mot siden av skjerm på mindre flater
### Patch Changes
- [#2067](https://github.com/navikt/aksel/pull/2067) [`d39ed906c`](https://github.com/navikt/aksel/commit/d39ed906c61149ac823ebab2e8974a105e592163) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :bug: Nested ExpansionCard fikk styling fra parent
## 4.2.0
### Minor Changes
- [#2048](https://github.com/navikt/aksel/pull/2048) [`b1625ba38`](https://github.com/navikt/aksel/commit/b1625ba3882b430d678d0623922d7fb56aa724b3) Thanks [@HalvorHaugan](https://github.com/HalvorHaugan)! - Oppdatert Chat
- :sparkles: Ny prop `size` som kan settes til "small" for en mer kompakt layout
- :sparkles: Ny prop `variant` som erstatter `backgroundColor` og `avatarBgColor`
- :sparkles: `avatar` er nå valgfritt
- :wheelchair: Forbedret UU
- :lipstick: Oppdatert utseende
## 4.1.7
### Patch Changes
- [`42b5af64a`](https://github.com/navikt/aksel/commit/42b5af64ab35d0f2d126d41f8fc3e61fe2834b36) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Fikset jsdom-problemer ved rendring av Timeline
## 4.1.6
## 4.1.5
## 4.1.4
### Patch Changes
- [#2049](https://github.com/navikt/aksel/pull/2049) [`e06096105`](https://github.com/navikt/aksel/commit/e060961050c75ec815e5c125001f1ff99106e298) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :bug: Subtle-description på form-elementer brakk bruk av Readmore
## 4.1.3
## 4.1.2
## 4.1.1
+30 -16
View File
@@ -1,14 +1,12 @@
.navds-chat {
display: flex;
align-items: flex-end;
gap: var(--a-spacing-4);
padding-right: var(--a-spacing-16);
gap: var(--a-spacing-3);
max-width: 40.75rem;
}
.navds-chat--right {
flex-direction: row-reverse;
padding-right: 0;
padding-left: var(--a-spacing-16);
}
.navds-chat__bubble-wrapper {
@@ -26,15 +24,19 @@
.navds-chat__avatar {
align-items: center;
background: var(--ac-chat-avatar-bg, var(--a-bg-subtle));
color: var(--ac-chat-avatar-color, var(--a-text-default));
box-shadow: var(--a-shadow-xsmall);
background-color: var(--ac-chat-avatar-bg, var(--a-surface-neutral-subtle));
border: 1px solid var(--ac-chat-border-color, var(--a-border-subtle));
border-radius: var(--a-border-radius-full);
color: var(--ac-chat-avatar-color, var(--a-text-default));
display: flex;
flex-shrink: 0;
justify-content: center;
overflow: hidden;
height: 3rem;
width: 3rem;
height: 2.5rem;
width: 2.5rem;
font-size: 1.06rem;
letter-spacing: 0.024rem;
}
.navds-chat__avatar svg {
@@ -44,11 +46,11 @@
}
.navds-chat__bubble {
padding: 1rem;
box-shadow: var(--a-shadow-small);
padding: var(--a-spacing-4);
box-shadow: var(--a-shadow-xsmall);
width: fit-content;
max-width: 37.5rem;
background-color: var(--ac-chat-bubble-bg, var(--a-bg-subtle));
background-color: var(--ac-chat-bubble-bg, var(--a-surface-neutral-subtle));
border: 1px solid var(--ac-chat-border-color, var(--a-border-subtle));
border-radius: var(--a-border-radius-xlarge);
border-bottom-left-radius: 2px;
display: flex;
@@ -61,11 +63,27 @@
border-bottom-right-radius: 2px;
}
.navds-chat--small .navds-chat__bubble {
padding: var(--a-spacing-3);
}
.navds-chat--info .navds-chat__bubble,
.navds-chat--info .navds-chat__avatar {
background-color: var(--a-surface-info-subtle);
}
.navds-chat--neutral .navds-chat__bubble,
.navds-chat--neutral .navds-chat__avatar {
background-color: var(--a-surface-default);
}
.navds-chat__top-text {
color: var(--ac-chat-top-text, var(--a-text-default));
display: flex;
gap: var(--a-spacing-2);
align-items: baseline;
font-weight: normal;
margin: 0;
}
.navds-chat--right .navds-chat__top-text {
@@ -87,7 +105,3 @@
.navds-chat--right .navds-chat__top-text--left {
align-self: flex-start;
}
.navds-chat__name {
font-weight: var(--a-font-weight-bold);
}
+4
View File
@@ -278,3 +278,7 @@
visibility: hidden;
pointer-events: none;
}
.navds-date__popover:where(.navds-popover) {
border: none;
}
+6 -3
View File
@@ -77,7 +77,7 @@
margin-top: var(--a-spacing-1);
}
.navds-expansioncard--small :where(.navds-expansioncard__title--small) {
.navds-expansioncard--small > :where(.navds-expansioncard__header) :where(.navds-expansioncard__title--small) {
margin-top: var(--a-spacing-05);
}
@@ -115,7 +115,7 @@
transition: transform 150ms ease-in-out;
}
.navds-expansioncard--open :where(.navds-expansioncard__header-chevron) {
.navds-expansioncard--open > :where(.navds-expansioncard__header) :where(.navds-expansioncard__header-chevron) {
transform: translateY(0) rotate(180deg);
}
@@ -135,7 +135,10 @@
}
}
.navds-expansioncard--open :where(.navds-expansioncard__header-button):hover :where(.navds-expansioncard__header-chevron) {
.navds-expansioncard--open
> :where(.navds-expansioncard__header)
> :where(.navds-expansioncard__header-button):hover
:where(.navds-expansioncard__header-chevron) {
transform: translateY(-1px) rotate(180deg);
}
+1 -1
View File
@@ -33,7 +33,7 @@
.navds-help-text__popover.navds-popover {
background-color: var(--ac-help-text-popover-bg, var(--a-surface-info-subtle));
max-width: 65ch;
max-width: min(65ch, calc(100vw - 1.5rem));
--ac-popover-border: var(--a-border-info);
}
+1 -1
View File
@@ -13,7 +13,7 @@
}
.navds-link-panel:hover {
box-shadow: var(--a-shadow-medium);
box-shadow: var(--a-shadow-small);
border-color: var(--ac-link-panel-hover-border, var(--a-border-action));
}
+1
View File
@@ -9,6 +9,7 @@
position: relative;
overflow: auto;
max-height: 100%;
box-shadow: var(--a-shadow-xlarge);
}
.navds-modal__content {
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "4.1.2",
"version": "4.4.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": "^4.1.2",
"@navikt/ds-tokens": "^4.4.0",
"cssnano": "6.0.0",
"fast-glob": "3.2.11",
"lodash": "4.17.21",
+1
View File
@@ -5,6 +5,7 @@
border: 1px solid;
border-color: var(--ac-popover-border, var(--a-border-default));
border-radius: var(--a-border-radius-medium);
max-width: calc(100vw - var(--a-spacing-6));
}
.navds-popover__content {
+1
View File
@@ -41,6 +41,7 @@
border-left: 2px solid var(--ac-read-more-line, var(--a-border-divider));
margin-left: 13px;
padding-left: 13px;
color: var(--a-text-default);
}
.navds-read-more--small .navds-read-more__content {
+4
View File
@@ -61,6 +61,10 @@
list-style-type: none;
}
.navds-timeline__row-periods:focus {
outline: none;
}
.navds-timeline__period {
height: 100%;
border-radius: var(--a-border-radius-full);
+3 -2
View File
@@ -80,9 +80,10 @@
"--ac-button-tertiary-neutral-active-hover-bg": "--a-surface-neutral-active"
},
"chat": {
"--ac-chat-avatar-bg": "--a-bg-subtle",
"--ac-chat-border-color": "--a-border-subtle",
"--ac-chat-avatar-bg": "--a-surface-neutral-subtle",
"--ac-chat-avatar-color": "--a-text-default",
"--ac-chat-bubble-bg": "--a-bg-subtle",
"--ac-chat-bubble-bg": "--a-surface-neutral-subtle",
"--ac-chat-top-text": "--a-text-default"
},
"chips": {
+86
View File
@@ -1,5 +1,91 @@
# @navikt/ds-react
## 4.4.0
### Minor Changes
- [#2041](https://github.com/navikt/aksel/pull/2041) [`de3626958`](https://github.com/navikt/aksel/commit/de3626958266d1271840fa341c36500ab66b573d) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Shadow-tokens er oppdatert til mer tydeligere varianter
### Patch Changes
- Updated dependencies []:
- @navikt/aksel-icons@4.4.0
## 4.3.0
### Minor Changes
- [#2069](https://github.com/navikt/aksel/pull/2069) [`99c08b8e4`](https://github.com/navikt/aksel/commit/99c08b8e4ab33acd20a21719986452652ffe3c57) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Popover og Helptext har nå luft mot siden av skjerm på mindre flater
### Patch Changes
- Updated dependencies []:
- @navikt/aksel-icons@4.3.0
## 4.2.0
### Minor Changes
- [#2048](https://github.com/navikt/aksel/pull/2048) [`b1625ba38`](https://github.com/navikt/aksel/commit/b1625ba3882b430d678d0623922d7fb56aa724b3) Thanks [@HalvorHaugan](https://github.com/HalvorHaugan)! - Oppdatert Chat
- :sparkles: Ny prop `size` som kan settes til "small" for en mer kompakt layout
- :sparkles: Ny prop `variant` som erstatter `backgroundColor` og `avatarBgColor`
- :sparkles: `avatar` er nå valgfritt
- :wheelchair: Forbedret UU
- :lipstick: Oppdatert utseende
### Patch Changes
- Updated dependencies []:
- @navikt/aksel-icons@4.2.0
## 4.1.7
### Patch Changes
- [`42b5af64a`](https://github.com/navikt/aksel/commit/42b5af64ab35d0f2d126d41f8fc3e61fe2834b36) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Fikset jsdom-problemer ved rendring av Timeline
- [#2056](https://github.com/navikt/aksel/pull/2056) [`dfbbe92a6`](https://github.com/navikt/aksel/commit/dfbbe92a63210278d9dd4d154c01f116ad921fc1) Thanks [@KenAJoh](https://github.com/KenAJoh)! - La til `wrapperClassname`-prop for HelpText
- Updated dependencies []:
- @navikt/aksel-icons@4.1.7
## 4.1.6
### Patch Changes
- [`9a8bb26c3`](https://github.com/navikt/aksel/commit/9a8bb26c36cfbb1a95cf28a043d2df3dbfe7185d) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :bug: Timeline-period brakk ved bruk av jsdom-tester
- Updated dependencies []:
- @navikt/aksel-icons@4.1.6
## 4.1.5
### Patch Changes
- [#2051](https://github.com/navikt/aksel/pull/2051) [`925aa7a14`](https://github.com/navikt/aksel/commit/925aa7a140bc0cd63e8884bea0bd478838c1aa17) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :bug: OverridableComponent fungerer nå med komponenter som allerede bruker 'as'-prop
- [#2052](https://github.com/navikt/aksel/pull/2052) [`7c17e40ca`](https://github.com/navikt/aksel/commit/7c17e40cac6703f00ce756276e9616deb73f6098) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :bug: floating-ui sendte ikke escape-events opp gjennom dom-treet
- Updated dependencies [[`8f16d1571`](https://github.com/navikt/aksel/commit/8f16d1571ec7cf452c09618a9a918ee2a634c75a)]:
- @navikt/aksel-icons@4.1.5
## 4.1.4
### Patch Changes
- Updated dependencies []:
- @navikt/aksel-icons@4.1.4
## 4.1.3
### Patch Changes
- [`44a75acb6`](https://github.com/navikt/aksel/commit/44a75acb6b904d614a01344f42111761a48cb7e2) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :bug: Classname ble ikke riktig forwardet til ikke-klikkbar Period i Timeline
- Updated dependencies []:
- @navikt/aksel-icons@4.1.3
## 4.1.2
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "4.1.2",
"version": "4.4.0",
"description": "Aksel react-components for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -38,7 +38,7 @@
},
"dependencies": {
"@floating-ui/react": "0.24.1",
"@navikt/aksel-icons": "^4.1.2",
"@navikt/aksel-icons": "^4.4.0",
"@radix-ui/react-tabs": "1.0.0",
"@radix-ui/react-toggle-group": "1.0.0",
"clsx": "^1.2.1",
+9 -5
View File
@@ -17,6 +17,7 @@ export interface BubbleProps extends HTMLAttributes<HTMLDivElement> {
timestamp?: string;
/**
* Background color on bubble
* @deprecated Use `variant` on Chat instead
*/
backgroundColor?: string;
/**
@@ -46,17 +47,20 @@ const Bubble = forwardRef<HTMLDivElement, BubbleProps>(
{...rest}
>
{(timestamp || name) && (
<div
<h3
className={cl(
`navds-chat__top-text`,
toptextPosition && `navds-chat__top-text--${toptextPosition}`
)}
>
{name && <Detail className="navds-chat__name">{name}</Detail>}
{timestamp && (
<Detail className="navds-chat__timestamp">{timestamp}</Detail>
{name && <Detail as="span">{name}</Detail>}
{name && timestamp && (
<Detail as="span" aria-hidden>
&bull;
</Detail>
)}
</div>
{timestamp && <Detail as="span">{timestamp}</Detail>}
</h3>
)}
{children}
</div>
+70 -52
View File
@@ -1,7 +1,10 @@
import React, { forwardRef, HTMLAttributes } from "react";
import cl from "clsx";
import Bubble, { BubbleProps } from "./Bubble";
import { BodyLong, BodyShort } from "../typography";
import Bubble from "./Bubble";
import { BodyLong } from "../typography";
export const POSITIONS = ["left", "right"] as const;
export const SIZES = ["medium", "small"] as const;
export interface ChatProps extends HTMLAttributes<HTMLDivElement> {
/**
@@ -17,27 +20,40 @@ export interface ChatProps extends HTMLAttributes<HTMLDivElement> {
*/
timestamp?: string;
/**
* Avatar for messenger. Regular text for initials works to
* Avatar for messenger. Regular text for initials works too, but it will be hidden for screen readers.
*/
avatar: React.ReactNode;
avatar?: React.ReactNode;
/**
* Changes background color on avatar and bubbles.
* Avoid using the same background as the surface behind Chat.
* @default "subtle"
*/
variant?: "subtle" | "info" | "neutral";
/**
* Background color on bubbles
* @deprecated Use `variant` instead
*/
backgroundColor?: string;
/**
* Background color for avatar
* @deprecated Use `variant` instead
*/
avatarBgColor?: string;
/**
* Positions avatar and Bubbles
* Positions avatar and bubbles
* @default "left"
*/
position?: "left" | "right";
position?: (typeof POSITIONS)[number];
/**
* Hoizontal position of toptext
* @default Same as chat
* @default Same as position
*/
toptextPosition?: "left" | "right";
toptextPosition?: (typeof POSITIONS)[number];
/**
* Affects padding and font size in bubbles
* @default "medium"
*/
size?: (typeof SIZES)[number];
}
interface ChatComponent
@@ -47,24 +63,23 @@ interface ChatComponent
/**
* @see 🏷️ {@link BubbleProps}
*/
Bubble: React.ForwardRefExoticComponent<
BubbleProps & React.RefAttributes<HTMLDivElement>
>;
Bubble: typeof Bubble;
}
/**
* A component for displaying chat messages.
* A component for communicating dialogs between two parties.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/chat)
* @see 🏷️ {@link ChatProps}
*
* @example
* ```jsx
* <Chat>
* <Chat.Bubble avatar="A" name="Alice">Hello!</Chat.Bubble>
* <Chat avatar="A" name="Alice" timestamp="01.01.21 14:00">
* <Chat.Bubble>Hello!</Chat.Bubble>
* <Chat.Bubble>How can I help you?</Chat.Bubble>
* </Chat>
* <Chat>
* <Chat.Bubble avatar="B" name="Bob">Hi there!</Chat.Bubble>
* <Chat avatar="B" name="Bob" timestamp="01.01.21 14:01" position="right">
* <Chat.Bubble>Hi there!</Chat.Bubble>
* </Chat>
* ```
*/
@@ -77,51 +92,54 @@ export const Chat = forwardRef<HTMLDivElement, ChatProps>(
timestamp,
avatar,
position = "left",
variant = "subtle",
avatarBgColor,
backgroundColor,
toptextPosition,
size = "medium",
...rest
},
}: ChatProps,
ref
) => {
return (
<div
ref={ref}
className={cl(
"navds-chat",
className,
`navds-chat--${position} navds-chat--top-text-${
toptextPosition ?? position
}`
)}
{...rest}
>
<BodyShort
as="div"
) => (
<div
ref={ref}
className={cl(
"navds-chat",
className,
`navds-chat--${position}`,
`navds-chat--top-text-${toptextPosition ?? position}`,
`navds-chat--${size}`,
`navds-chat--${variant}`
)}
{...rest}
>
{avatar && (
<div
className="navds-chat__avatar"
aria-hidden
style={{ backgroundColor: avatarBgColor }}
>
{avatar}
</BodyShort>
<ol className="navds-chat__bubble-wrapper">
{React.Children.map(children, (child, i) => {
if (React.isValidElement(child)) {
return (
<BodyLong as="li">
{React.cloneElement(child, {
name: name && i === 0 ? name : undefined,
timestamp: timestamp && i === 0 ? timestamp : undefined,
backgroundColor,
...child.props,
})}
</BodyLong>
);
}
})}
</ol>
</div>
);
}
</div>
)}
<ol className="navds-chat__bubble-wrapper">
{React.Children.map(children, (child, i) => {
if (React.isValidElement(child)) {
return (
<BodyLong as="li" size={size}>
{React.cloneElement(child, {
name: name && i === 0 ? name : undefined,
timestamp: timestamp && i === 0 ? timestamp : undefined,
backgroundColor,
...child.props,
})}
</BodyLong>
);
}
})}
</ol>
</div>
)
) as ChatComponent;
Chat.Bubble = Bubble;
+150 -128
View File
@@ -1,6 +1,7 @@
import React from "react";
import { Chat } from "../index";
import { Meta } from "@storybook/react";
import { POSITIONS, SIZES } from "./Chat";
export default {
title: "ds-react/Chat",
@@ -16,19 +17,25 @@ export default {
type: "text",
},
},
position: {
control: { type: "radio" },
options: POSITIONS,
},
toptextPosition: {
control: { type: "radio" },
options: POSITIONS,
},
size: {
control: { type: "radio" },
options: SIZES,
},
},
} as Meta;
} satisfies Meta<typeof Chat>;
export const Default = {
render: (props) => {
return (
<Chat
avatar={props?.avatar ?? "ON"}
name={props?.name ?? "Ola Normann"}
timestamp={props?.timestamp ?? "01.01.21 14:00"}
avatarBgColor={props?.avatarBgColor}
backgroundColor={props?.backgroundColor}
>
<Chat {...props}>
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
@@ -48,141 +55,156 @@ export const Default = {
},
};
export const Farger = () => {
return (
export const Small = () => (
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 14:00" size="small">
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
</Chat.Bubble>
</Chat>
);
export const Colors = () => (
<Chat
avatar="NAV"
name="NAV"
timestamp="01.01.21 14:00"
avatarBgColor="var(--a-surface-alt-1-subtle)"
backgroundColor="var(--a-surface-warning-subtle)"
>
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
<Chat.Bubble backgroundColor="lightYellow">
Tempor fugiat amet eu sint in in ullamco.
</Chat.Bubble>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
</Chat.Bubble>
</Chat>
);
export const Variants = () => (
<div className="colgap">
<Chat avatar="NAV" name="NAV" timestamp="01.01.21 14:00" variant="info">
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
</Chat>
<Chat
avatar="KN"
name="Kari Nordmann"
timestamp="01.01.21 14:03"
variant="neutral"
position="right"
>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
</Chat>
<Chat
avatar="ON"
name="Ola Nordmann"
timestamp="01.01.21 14:07"
variant="subtle"
position="right"
>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
</Chat.Bubble>
</Chat>
</div>
);
export const Position = () => (
<div className="colgap">
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 14:00">
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
</Chat.Bubble>
</Chat>
<Chat
avatar="KH"
name="Kari Høyli"
timestamp="01.01.21 14:32"
position="right"
variant="info"
>
<Chat.Bubble>
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
<Chat.Bubble>
Enim velit deserunt do quis. Eu fugiat magna esse dolore ad sunt sit est
dolore incididunt. Occaecat cupidatat magna.
</Chat.Bubble>
</Chat>
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 15:00">
<Chat.Bubble>
Exercitation irure Lorem cupidatat culpa anim cillum esse ullamco qui
dolore laborum et Lorem. Labore sunt duis id Lorem voluptate commodo ea
esse. Dolore esse aliqua proident ea ad commodo ut dolore voluptate
labore sunt aute.
</Chat.Bubble>
</Chat>
</div>
);
export const ToptextPosition = () => (
<div className="colgap">
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 14:00"
avatarBgColor="var(--a-lightblue-200)"
backgroundColor="var(--a-lightblue-100)"
toptextPosition="right"
>
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
</Chat.Bubble>
</Chat>
);
};
export const Position = () => {
return (
<div className="colgap">
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 14:00"
backgroundColor="rgba(255, 249, 240, 1)"
avatarBgColor="rgba(255, 236, 204, 1)"
>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint
laboris labore nisi ut.
</Chat.Bubble>
</Chat>
<Chat
avatar="KH"
name="Kari Høyli"
timestamp="01.01.21 14:32"
position="right"
backgroundColor="rgba(230, 240, 255, 1)"
avatarBgColor="rgba(204, 225, 255, 1)"
>
<Chat.Bubble>
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
<Chat.Bubble>
Enim velit deserunt do quis. Eu fugiat magna esse dolore ad sunt sit
est dolore incididunt. Occaecat cupidatat magna.
</Chat.Bubble>
</Chat>
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 15:00"
backgroundColor="rgba(255, 249, 240, 1)"
avatarBgColor="rgba(255, 236, 204, 1)"
>
<Chat.Bubble>
Exercitation irure Lorem cupidatat culpa anim cillum esse ullamco qui
dolore laborum et Lorem. Labore sunt duis id Lorem voluptate commodo
ea esse. Dolore esse aliqua proident ea ad commodo ut dolore voluptate
labore sunt aute.
</Chat.Bubble>
</Chat>
</div>
);
};
export const ToptextPosition = () => {
return (
<div className="colgap">
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 14:00"
backgroundColor="rgba(255, 249, 240, 1)"
avatarBgColor="rgba(255, 236, 204, 1)"
toptextPosition="right"
>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint
laboris labore nisi ut.
</Chat.Bubble>
</Chat>
<Chat
avatar="KH"
name="Kari Høyli"
timestamp="01.01.21 14:32"
position="right"
backgroundColor="rgba(230, 240, 255, 1)"
avatarBgColor="rgba(204, 225, 255, 1)"
toptextPosition="left"
>
<Chat.Bubble>
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
<Chat.Bubble>
Enim velit deserunt do quis. Eu fugiat magna esse dolore ad sunt sit
est dolore incididunt. Occaecat cupidatat magna.
</Chat.Bubble>
</Chat>
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 15:00"
backgroundColor="rgba(255, 249, 240, 1)"
avatarBgColor="rgba(255, 236, 204, 1)"
>
<Chat.Bubble toptextPosition="right">
Exercitation irure Lorem cupidatat culpa anim cillum esse ullamco qui
dolore laborum et Lorem. Labore sunt duis id Lorem voluptate commodo
ea esse. Dolore esse aliqua proident ea ad commodo ut dolore voluptate
labore sunt aute.
</Chat.Bubble>
</Chat>
</div>
);
};
export const Avatar = () => {
return (
<Chat avatar={<Illustration />} timestamp="01.01.21 14:00">
<Chat
name="Kari Høyli"
timestamp="01.01.21 14:32"
position="right"
variant="info"
toptextPosition="left"
>
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
Enim velit deserunt do quis. Eu fugiat magna esse dolore ad sunt sit est
dolore incididunt. Occaecat cupidatat magna.
</Chat.Bubble>
</Chat>
);
};
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 15:00">
<Chat.Bubble toptextPosition="right">
Exercitation irure Lorem cupidatat culpa anim cillum esse ullamco qui
dolore laborum et Lorem. Labore sunt duis id Lorem voluptate commodo ea
esse. Dolore esse aliqua proident ea ad commodo ut dolore voluptate
labore sunt aute.
</Chat.Bubble>
</Chat>
</div>
);
export const Avatar = () => (
<Chat avatar={<Illustration />} timestamp="01.01.21 14:00">
<Chat.Bubble>
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
</Chat.Bubble>
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint laboris
labore nisi ut.
</Chat.Bubble>
</Chat>
);
const Illustration = () => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 54 93">
@@ -114,6 +114,11 @@ export interface DatePickerDefaultProps
* @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;
}
export type DatePickerProps = DatePickerDefaultProps & ConditionalModeProps;
@@ -180,6 +185,7 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
onClose,
onOpenToggle,
strategy = "absolute",
bubbleEscape = false,
...rest
},
ref
@@ -251,6 +257,8 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
role="dialog"
ref={ref}
strategy={strategy}
className="navds-date__popover"
bubbleEscape={bubbleEscape}
>
<DayPicker
locale={getLocaleFromString(locale)}
@@ -319,6 +319,7 @@ export const useDatepicker = (
onOpenToggle: () => handleOpen(!open),
disabled,
disableWeekends,
bubbleEscape: true,
ref: setDaypickerRef,
};
@@ -312,6 +312,7 @@ export const useMonthpicker = (
open,
onOpenToggle: () => handleOpen(!open),
disabled,
bubbleEscape: true,
ref: setMonthpickerRef,
};
@@ -544,6 +544,7 @@ export const useRangeDatepicker = (
onOpenToggle: () => setOpen((x) => !x),
disabled,
disableWeekends,
bubbleEscape: true,
ref: setDaypickerRef,
};
@@ -86,6 +86,11 @@ 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;
}
interface MonthPickerComponent
@@ -153,6 +158,7 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
year,
onYearChange,
strategy = "absolute",
bubbleEscape = false,
},
ref
) => {
@@ -202,8 +208,9 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
role="dialog"
ref={ref}
id={ariaId}
className="navds-date"
className="navds-date navds-date__popover"
strategy={strategy}
bubbleEscape={bubbleEscape}
>
<RootProvider
locale={getLocaleFromString(locale)}
@@ -29,6 +29,18 @@ interface ErrorSummaryComponent
extends React.ForwardRefExoticComponent<
ErrorSummaryProps & React.RefAttributes<HTMLDivElement>
> {
/**
* Link to error
*
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
*
* @example
* ```jsx
* <ErrorSummary.Item href="#1">
* Felt må fylles ut med alder
* </ErrorSummary.Item>
* ```
*/
Item: ErrorSummaryItemType;
}
@@ -8,6 +8,10 @@ export interface ErrorSummaryItemProps
* Link text
*/
children: React.ReactNode;
/**
* Link to errormessage
*/
href?: string;
}
export type ErrorSummaryItemType = OverridableComponent<
@@ -29,6 +29,10 @@ export interface HelpTextProps
| "right-end"
| "left-start"
| "left-end";
/**
* Classname for wrapper
*/
wrapperClassName?: string;
}
/**
@@ -53,6 +57,7 @@ export const HelpText = forwardRef<HTMLButtonElement, HelpTextProps>(
strategy = "absolute",
title = "hjelp",
onClick,
wrapperClassName,
...rest
},
ref
@@ -62,7 +67,7 @@ export const HelpText = forwardRef<HTMLButtonElement, HelpTextProps>(
const [open, setOpen] = useState(false);
return (
<div className="navds-help-text">
<div className={cl("navds-help-text", wrapperClassName)}>
<button
{...rest}
ref={mergedRef}
@@ -1,5 +1,5 @@
import React, { useEffect, useRef } from "react";
import { HelpText } from "..";
import { BodyLong, Heading, HelpText } from "..";
export default {
title: "ds-react/HelpText",
@@ -65,3 +65,36 @@ export const Open = () => {
</HelpText>
);
};
export const WrapperClassName = {
render: () => {
return (
<div>
<BodyLong spacing>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta
perspiciatis vero voluptatum, asperiores cumque, deserunt accusantium
ullam ipsa accusamus officia cupiditate quae unde esse culpa, ratione
ab quam ea quas?
</BodyLong>
<Heading level="2" size="medium">
67 år og 1 måneder øklasdjkl askdak døkasøk daøkdkølasøkld asølkdøka
<HelpText wrapperClassName="testClass">
Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta
perspiciatis vero voluptatum, asperiores cumque, deserunt
accusantium ullam ipsa accusamus officia cupiditate quae unde esse
culpa, ratione ab quam ea quas?
</HelpText>
<style>{`.testClass {display: inline-block; margin-left: 0.5rem; vertical-align: middle;}`}</style>
</Heading>
<BodyLong spacing>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Soluta
perspiciatis vero voluptatum, asperiores cumque, deserunt accusantium
ullam ipsa accusamus officia cupiditate quae unde esse culpa, ratione
ab quam ea quas?
</BodyLong>
</div>
);
},
};
+15 -5
View File
@@ -1,8 +1,8 @@
import {
arrow as flArrow,
autoUpdate,
flip,
arrow as flArrow,
offset as flOffset,
flip,
shift,
useClick,
useDismiss,
@@ -11,8 +11,8 @@ import {
} from "@floating-ui/react";
import cl from "clsx";
import React, {
forwardRef,
HTMLAttributes,
forwardRef,
useCallback,
useMemo,
useRef,
@@ -72,6 +72,11 @@ 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;
}
interface PopoverComponent
@@ -113,6 +118,7 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
placement = "top",
offset,
strategy: userStrategy = "absolute",
bubbleEscape = false,
...rest
},
ref
@@ -135,15 +141,19 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
onOpenChange: onClose,
middleware: [
flOffset(offset ?? (arrow ? 16 : 4)),
shift(),
flip({ padding: 5, fallbackPlacements: ["bottom", "top"] }),
shift({ padding: 12 }),
flArrow({ element: arrowRef, padding: 8 }),
],
});
const { getFloatingProps } = useInteractions([
useClick(context),
useDismiss(context),
useDismiss(context, {
bubbles: {
escapeKey: bubbleEscape,
},
}),
]);
useClientLayoutEffect(() => {
+6 -24
View File
@@ -11,15 +11,9 @@ import {
useHover,
useInteractions,
} from "@floating-ui/react";
import { useEventListener, mergeRefs } from "../util";
import { format } from "date-fns";
import React, {
forwardRef,
useCallback,
useMemo,
useRef,
useState,
} from "react";
import React, { forwardRef, useMemo, useRef, useState } from "react";
import { mergeRefs } from "../util";
import { useTimelineContext } from "./hooks/useTimelineContext";
import { position } from "./utils/calc";
import { TimelineComponentTypes } from "./utils/types.internal";
@@ -83,22 +77,6 @@ export const Pin = forwardRef<HTMLButtonElement, TimelinePinProps>(
[ref, refs.setReference]
);
useEventListener(
"focusin",
useCallback(
(e: FocusEvent) => {
if (
![refs.domReference.current, refs?.floating?.current].some(
(element) => element?.contains(e.target as Node)
)
) {
open && setOpen(false);
}
},
[open, refs.domReference, refs?.floating]
)
);
const staticSide = {
top: "bottom",
right: "left",
@@ -139,6 +117,10 @@ export const Pin = forwardRef<HTMLButtonElement, TimelinePinProps>(
ref={refs.setFloating}
{...getFloatingProps({
tabIndex: -1,
onBlur: (e) =>
e.target.previousSibling !== document.activeElement &&
open &&
setOpen(false),
})}
style={{
...floatingStyles,
@@ -72,6 +72,7 @@ export const TimelineRow = forwardRef<HTMLOListElement, TimelineRowProps>(
>
<ol
{...rest}
tabIndex={-1}
ref={ref}
aria-label={
periods.length === 0
@@ -11,9 +11,9 @@ import {
useHover,
useInteractions,
} from "@floating-ui/react";
import { useEventListener, mergeRefs } from "../../util";
import cl from "clsx";
import React, { useCallback, useMemo, useRef, useState } from "react";
import React, { useMemo, useRef, useState } from "react";
import { mergeRefs } from "../../util";
import { usePeriodContext } from "../hooks/usePeriodContext";
import { useRowContext } from "../hooks/useRowContext";
import { useTimelineContext } from "../hooks/useTimelineContext";
@@ -85,22 +85,6 @@ const ClickablePeriod = React.memo(
[periodRef, refs.setReference]
);
useEventListener(
"focusin",
useCallback(
(e: FocusEvent) => {
if (
![refs.domReference.current, refs?.floating?.current].some(
(element) => element?.contains(e.target as Node)
)
) {
open && setOpen(false);
}
},
[open, refs.domReference, refs?.floating]
)
);
const staticSide = {
top: "bottom",
right: "left",
@@ -165,6 +149,10 @@ const ClickablePeriod = React.memo(
ref={refs.setFloating}
{...getFloatingProps({
tabIndex: -1,
onBlur: (e) =>
e.target.previousSibling !== document.activeElement &&
open &&
setOpen(false),
})}
style={{
...floatingStyles,
@@ -26,7 +26,7 @@ const NonClickablePeriod = ({
{...restProps}
className={cl(
getConditionalClasses(cropped, direction, status),
restProps?.classname
restProps?.className
)}
style={{
width: `${width}%`,
@@ -7,6 +7,6 @@ export interface OverridableComponent<Component, Element extends HTMLElement> {
props: {
as: As;
} & Component &
Omit<React.ComponentPropsWithRef<As>, keyof Component>
Omit<React.ComponentPropsWithRef<As>, keyof Component | "as">
): ReturnType<React.FC>;
}
+16
View File
@@ -1,5 +1,21 @@
# @navikt/ds-tailwind
## 4.4.0
## 4.3.0
## 4.2.0
## 4.1.7
## 4.1.6
## 4.1.5
## 4.1.4
## 4.1.3
## 4.1.2
## 4.1.1
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tailwind",
"version": "4.1.2",
"version": "4.4.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": "^4.1.2",
"@navikt/ds-tokens": "^4.4.0",
"@types/jest": "^29.0.0",
"color": "4.2.3",
"jest": "^29.0.0",
+20
View File
@@ -1,5 +1,25 @@
# @navikt/ds-tokens
## 4.4.0
### Minor Changes
- [#2041](https://github.com/navikt/aksel/pull/2041) [`de3626958`](https://github.com/navikt/aksel/commit/de3626958266d1271840fa341c36500ab66b573d) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Shadow-tokens er oppdatert til mer tydeligere varianter
## 4.3.0
## 4.2.0
## 4.1.7
## 4.1.6
## 4.1.5
## 4.1.4
## 4.1.3
## 4.1.2
## 4.1.1
+81 -49
View File
@@ -47,27 +47,27 @@
"name": "--a-font-family",
"value": "\"Source Sans Pro\", Arial, sans-serif"
},
{ "name": "--a-font-weight-bold", "value": "600" },
{ "name": "--a-font-weight-regular", "value": "400" },
{ "name": "--a-font-line-height-heading-2xlarge", "value": "3.25rem" },
{ "name": "--a-font-line-height-heading-xlarge", "value": "2.5rem" },
{ "name": "--a-font-line-height-heading-large", "value": "2.25rem" },
{ "name": "--a-font-line-height-heading-medium", "value": "2rem" },
{ "name": "--a-font-line-height-heading-small", "value": "1.75rem" },
{ "name": "--a-font-line-height-heading-xsmall", "value": "1.5rem" },
{ "name": "--a-font-line-height-xlarge", "value": "1.75rem" },
{ "name": "--a-font-line-height-large", "value": "1.5rem" },
{ "name": "--a-font-line-height-medium", "value": "1.25rem" },
{ "name": "--a-font-size-heading-2xlarge", "value": "2.5rem" },
{ "name": "--a-font-size-heading-xlarge", "value": "2rem" },
{ "name": "--a-font-size-heading-large", "value": "1.75rem" },
{ "name": "--a-font-size-heading-medium", "value": "1.5rem" },
{ "name": "--a-font-size-heading-small", "value": "1.25rem" },
{ "name": "--a-font-size-heading-xsmall", "value": "1.125rem" },
{ "name": "--a-font-size-xlarge", "value": "1.25rem" },
{ "name": "--a-font-size-large", "value": "1.125rem" },
{ "name": "--a-font-weight-bold", "value": "600" },
{ "name": "--a-font-size-small", "value": "0.875rem" },
{ "name": "--a-font-size-medium", "value": "1rem" },
{ "name": "--a-font-size-small", "value": "0.875rem" }
{ "name": "--a-font-size-large", "value": "1.125rem" },
{ "name": "--a-font-size-xlarge", "value": "1.25rem" },
{ "name": "--a-font-size-heading-xsmall", "value": "1.125rem" },
{ "name": "--a-font-size-heading-small", "value": "1.25rem" },
{ "name": "--a-font-size-heading-medium", "value": "1.5rem" },
{ "name": "--a-font-size-heading-large", "value": "1.75rem" },
{ "name": "--a-font-size-heading-xlarge", "value": "2rem" },
{ "name": "--a-font-size-heading-2xlarge", "value": "2.5rem" },
{ "name": "--a-font-line-height-medium", "value": "1.25rem" },
{ "name": "--a-font-line-height-large", "value": "1.5rem" },
{ "name": "--a-font-line-height-xlarge", "value": "1.75rem" },
{ "name": "--a-font-line-height-heading-xsmall", "value": "1.5rem" },
{ "name": "--a-font-line-height-heading-small", "value": "1.75rem" },
{ "name": "--a-font-line-height-heading-medium", "value": "2rem" },
{ "name": "--a-font-line-height-heading-large", "value": "2.25rem" },
{ "name": "--a-font-line-height-heading-xlarge", "value": "2.5rem" },
{ "name": "--a-font-line-height-heading-2xlarge", "value": "3.25rem" }
],
"global-base": [
{ "name": "--a-transparent", "value": "rgba(255, 255, 255, 0)" },
@@ -195,26 +195,34 @@
{ "name": "--a-purple-900", "value": "rgba(31, 20, 47, 1)" }
],
"semantic-border": [
"semantic-border-default": [
{ "name": "--a-border-default", "value": "rgba(0, 0, 0, 0.44)" },
{ "name": "--a-border-strong", "value": "rgba(0, 0, 0, 0.65)" },
{ "name": "--a-border-divider", "value": "rgba(0, 0, 0, 0.19)" },
{ "name": "--a-border-subtle", "value": "rgba(0, 0, 0, 0.1)" },
{ "name": "--a-border-subtle-hover", "value": "rgba(0, 0, 0, 0.19)" },
{ "name": "--a-border-on-inverted", "value": "rgba(229, 229, 229, 1)" },
{ "name": "--a-border-on-inverted-subtle", "value": "rgba(89, 89, 89, 1)" }
],
"semantic-border-action": [
{ "name": "--a-border-action", "value": "rgba(0, 103, 197, 1)" },
{ "name": "--a-border-action-selected", "value": "rgba(0, 91, 130, 1)" },
{ "name": "--a-border-selected", "value": "rgba(0, 103, 197, 1)" },
{ "name": "--a-border-selected", "value": "rgba(0, 103, 197, 1)" }
],
"semantic-border-status": [
{ "name": "--a-border-success", "value": "rgba(6, 137, 58, 1)" },
{ "name": "--a-border-danger", "value": "rgba(195, 0, 0, 1)" },
{ "name": "--a-border-warning", "value": "rgba(199, 115, 0, 1)" },
{ "name": "--a-border-info", "value": "rgba(54, 141, 168, 1)" },
{ "name": "--a-border-on-inverted", "value": "rgba(229, 229, 229, 1)" },
{ "name": "--a-border-on-inverted-subtle", "value": "rgba(89, 89, 89, 1)" },
{ "name": "--a-border-info", "value": "rgba(54, 141, 168, 1)" }
],
"semantic-border-focus": [
{ "name": "--a-border-focus", "value": "rgba(0, 52, 125, 1)" },
{
"name": "--a-border-focus-on-inverted",
"value": "rgba(153, 195, 255, 1)"
},
}
],
"semantic-border-alt": [
{ "name": "--a-border-alt-1", "value": "rgba(130, 105, 162, 1)" },
{ "name": "--a-border-alt-2", "value": "rgba(127, 137, 0, 1)" },
{ "name": "--a-border-alt-3", "value": "rgba(0, 91, 130, 1)" }
@@ -230,18 +238,18 @@
"name": "--a-text-action-on-action-subtle",
"value": "rgba(0, 86, 180, 1)"
},
{ "name": "--a-text-on-warning", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-text-on-info", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-text-on-alt-2", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-text-on-inverted", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-text-on-neutral", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-text-on-action", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-text-on-success", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-text-on-danger", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-text-on-warning", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-text-on-info", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-text-on-alt-1", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-text-on-alt-2", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-text-on-alt-3", "value": "rgba(255, 255, 255, 1)" }
],
"semantic-surface": [
"semantic-surface-default": [
{ "name": "--a-surface-default", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-surface-hover", "value": "rgba(0, 0, 0, 0.05)" },
{ "name": "--a-surface-active", "value": "rgba(0, 0, 0, 0.1)" },
@@ -251,7 +259,9 @@
{ "name": "--a-surface-backdrop", "value": "rgba(0, 0, 0, 0.65)" },
{ "name": "--a-surface-inverted-hover", "value": "rgba(64, 64, 64, 1)" },
{ "name": "--a-surface-inverted-active", "value": "rgba(89, 89, 89, 1)" },
{ "name": "--a-surface-inverted", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-surface-inverted", "value": "rgba(38, 38, 38, 1)" }
],
"semantic-surface-action": [
{ "name": "--a-surface-action-subtle", "value": "rgba(230, 240, 255, 1)" },
{
"name": "--a-surface-action-subtle-hover",
@@ -264,7 +274,9 @@
{
"name": "--a-surface-action-selected-hover",
"value": "rgba(0, 80, 119, 1)"
},
}
],
"semantic-surface-neutral": [
{ "name": "--a-surface-neutral-subtle", "value": "rgba(0, 0, 0, 0.05)" },
{
"name": "--a-surface-neutral-subtle-hover",
@@ -274,7 +286,9 @@
{ "name": "--a-surface-neutral", "value": "rgba(89, 89, 89, 1)" },
{ "name": "--a-surface-neutral-hover", "value": "rgba(64, 64, 64, 1)" },
{ "name": "--a-surface-neutral-selected", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-surface-neutral-active", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-surface-neutral-active", "value": "rgba(38, 38, 38, 1)" }
],
"semantic-surface-success": [
{ "name": "--a-surface-success-subtle", "value": "rgba(204, 241, 214, 1)" },
{
"name": "--a-surface-success-subtle-hover",
@@ -285,7 +299,9 @@
"value": "rgba(153, 222, 173, 1)"
},
{ "name": "--a-surface-success", "value": "rgba(6, 137, 58, 1)" },
{ "name": "--a-surface-success-hover", "value": "rgba(0, 124, 46, 1)" },
{ "name": "--a-surface-success-hover", "value": "rgba(0, 124, 46, 1)" }
],
"semantic-surface-danger": [
{ "name": "--a-surface-danger-subtle", "value": "rgba(255, 194, 194, 1)" },
{
"name": "--a-surface-danger-subtle-hover",
@@ -297,24 +313,30 @@
},
{ "name": "--a-surface-danger", "value": "rgba(195, 0, 0, 1)" },
{ "name": "--a-surface-danger-hover", "value": "rgba(173, 0, 0, 1)" },
{ "name": "--a-surface-danger-active", "value": "rgba(140, 0, 0, 1)" },
{ "name": "--a-surface-danger-active", "value": "rgba(140, 0, 0, 1)" }
],
"semantic-surface-warning": [
{ "name": "--a-surface-warning-subtle", "value": "rgba(255, 236, 204, 1)" },
{
"name": "--a-surface-warning-moderate",
"value": "rgba(255, 215, 153, 1)"
},
{ "name": "--a-surface-warning", "value": "rgba(255, 145, 0, 1)" },
{
"name": "--a-surface-warning-subtle-hover",
"value": "rgba(255, 215, 153, 1)"
},
{
"name": "--a-surface-warning-moderate",
"value": "rgba(255, 215, 153, 1)"
},
{ "name": "--a-surface-warning", "value": "rgba(255, 145, 0, 1)" }
],
"semantic-surface-info": [
{ "name": "--a-surface-info-subtle", "value": "rgba(216, 249, 255, 1)" },
{
"name": "--a-surface-info-subtle-hover",
"value": "rgba(181, 241, 255, 1)"
},
{ "name": "--a-surface-info-moderate", "value": "rgba(181, 241, 255, 1)" },
{ "name": "--a-surface-info", "value": "rgba(102, 203, 236, 1)" },
{ "name": "--a-surface-info", "value": "rgba(102, 203, 236, 1)" }
],
"semantic-surface-alt": [
{ "name": "--a-surface-alt-1", "value": "rgba(130, 105, 162, 1)" },
{ "name": "--a-surface-alt-1-subtle", "value": "rgba(224, 216, 233, 1)" },
{ "name": "--a-surface-alt-1-moderate", "value": "rgba(192, 178, 210, 1)" },
@@ -337,18 +359,22 @@
"value": "rgba(241, 241, 241, 1)"
}
],
"semantic-icon": [
"semantic-icon-defaults": [
{ "name": "--a-icon-default", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-icon-subtle", "value": "rgba(0, 0, 0, 0.65)" },
{ "name": "--a-icon-on-neutral", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-icon-on-inverted", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-icon-on-inverted", "value": "rgba(255, 255, 255, 1)" }
],
"semantic-icon-action": [
{ "name": "--a-icon-action", "value": "rgba(0, 103, 197, 1)" },
{ "name": "--a-icon-action-selected", "value": "rgba(0, 91, 130, 1)" },
{
"name": "--a-icon-action-on-action-subtle",
"value": "rgba(0, 86, 180, 1)"
},
{ "name": "--a-icon-on-action", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-icon-on-action", "value": "rgba(255, 255, 255, 1)" }
],
"semantic-icon-status": [
{ "name": "--a-icon-success", "value": "rgba(6, 137, 58, 1)" },
{ "name": "--a-icon-on-success", "value": "rgba(255, 255, 255, 1)" },
{ "name": "--a-icon-danger", "value": "rgba(195, 0, 0, 1)" },
@@ -356,11 +382,14 @@
{ "name": "--a-icon-warning", "value": "rgba(199, 115, 0, 1)" },
{ "name": "--a-icon-on-warning", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-icon-info", "value": "rgba(35, 107, 125, 1)" },
{ "name": "--a-icon-on-info", "value": "rgba(38, 38, 38, 1)" },
{ "name": "--a-icon-on-info", "value": "rgba(38, 38, 38, 1)" }
],
"semantic-icon-alt": [
{ "name": "--a-icon-alt-1", "value": "rgba(99, 70, 137, 1)" },
{ "name": "--a-icon-alt-2", "value": "rgba(127, 137, 0, 1)" },
{ "name": "--a-icon-alt-3", "value": "rgba(51, 128, 165, 1)" }
],
"semantic-data-surface": [
{
"name": "--a-data-surface-1",
@@ -445,22 +474,25 @@
{ "name": "--a-border-radius-full", "value": "9999px" }
],
"shadow": [
{ "name": "--a-shadow-xsmall", "value": "0 1px 2px 0 rgba(0, 0, 0, 0.12)" },
{
"name": "--a-shadow-xsmall",
"value": "0px 1px 3px 0px rgba(0, 0, 0, 0.15), 0px 0px 1px 0px rgba(0, 0, 0, 0.20)"
},
{
"name": "--a-shadow-small",
"value": "0 1px 2px -1px rgba(0, 0, 0, 0.1), 0 1px 3px 0 rgba(0, 0, 0, 0.1)"
"value": "0px 3px 8px 0px rgba(0, 0, 0, 0.10), 0px 1px 3px 0px rgba(0, 0, 0, 0.10), 0px 0px 1px 0px rgba(0, 0, 0, 0.18)"
},
{
"name": "--a-shadow-medium",
"value": "0 2px 4px -2px rgba(0, 0, 0, 0.1), 0 4px 6px -1px rgba(0, 0, 0, 0.1)"
"value": "0px 5px 12px 0px rgba(0, 0, 0, 0.13), 0px 1px 3px 0px rgba(0, 0, 0, 0.10), 0px 0px 1px 0px rgba(0, 0, 0, 0.15)"
},
{
"name": "--a-shadow-large",
"value": "0 4px 6px -4px rgba(0, 0, 0, 0.1), 0 10px 15px -3px rgba(0, 0, 0, 0.1)"
"value": "0px 2px 5px 0px rgba(0, 0, 0, 0.15), 0px 10px 16px 0px rgba(0, 0, 0, 0.12), 0px 0px 1px 0px rgba(0, 0, 0, 0.12)"
},
{
"name": "--a-shadow-xlarge",
"value": "0 8px 10px -6px rgba(0, 0, 0, 0.1), 0 25px 50px -5px rgba(0, 0, 0, 0.1)"
"value": "0px 2px 5px 0px rgba(0, 0, 0, 0.15), 0px 10px 24px 0px rgba(0, 0, 0, 0.18), 0px 0px 1px 0px rgba(0, 0, 0, 0.08)"
},
{ "name": "--a-shadow-focus", "value": "0 0 0 3px rgba(0, 52, 125, 1)" },
{
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tokens",
"version": "4.1.2",
"version": "4.4.0",
"description": "Design-tokens for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
+5 -5
View File
@@ -2,19 +2,19 @@
"a": {
"shadow": {
"xsmall": {
"value": "0 1px 2px 0 rgba(0, 0, 0, 0.12)"
"value": "0px 1px 3px 0px rgba(0, 0, 0, 0.15), 0px 0px 1px 0px rgba(0, 0, 0, 0.20)"
},
"small": {
"value": "0 1px 2px -1px rgba(0, 0, 0, 0.1), 0 1px 3px 0 rgba(0, 0, 0, 0.1)"
"value": "0px 3px 8px 0px rgba(0, 0, 0, 0.10), 0px 1px 3px 0px rgba(0, 0, 0, 0.10), 0px 0px 1px 0px rgba(0, 0, 0, 0.18)"
},
"medium": {
"value": "0 2px 4px -2px rgba(0, 0, 0, 0.1), 0 4px 6px -1px rgba(0, 0, 0, 0.1)"
"value": "0px 5px 12px 0px rgba(0, 0, 0, 0.13), 0px 1px 3px 0px rgba(0, 0, 0, 0.10), 0px 0px 1px 0px rgba(0, 0, 0, 0.15)"
},
"large": {
"value": "0 4px 6px -4px rgba(0, 0, 0, 0.1), 0 10px 15px -3px rgba(0, 0, 0, 0.1)"
"value": "0px 2px 5px 0px rgba(0, 0, 0, 0.15), 0px 10px 16px 0px rgba(0, 0, 0, 0.12), 0px 0px 1px 0px rgba(0, 0, 0, 0.12)"
},
"xlarge": {
"value": "0 8px 10px -6px rgba(0, 0, 0, 0.1), 0 25px 50px -5px rgba(0, 0, 0, 0.1)"
"value": "0px 2px 5px 0px rgba(0, 0, 0, 0.15), 0px 10px 24px 0px rgba(0, 0, 0, 0.18), 0px 0px 1px 0px rgba(0, 0, 0, 0.08)"
},
"focus": {
"inverted": {
@@ -10,9 +10,8 @@ const Shadow = ({ token }) => (
<div className="shadow" style={{ boxShadow: token }} />
<style>{`
.shadow {
width: 4rem;
height: 4rem;
background: #f7f7f7;
width: 12rem;
height: 6rem;
border-radius: 16px;
}
`}</style>
@@ -23,7 +22,7 @@ export const XSmall = () => <Shadow token="var(--a-shadow-xsmall)" />;
export const Small = () => <Shadow token="var(--a-shadow-small)" />;
export const Medium = () => <Shadow token="var(--a-shadow-medium)" />;
export const Large = () => <Shadow token="var(--a-shadow-large)" />;
export const XxLarge = () => <Shadow token="var(--a-shadow-xlarge)" />;
export const XLarge = () => <Shadow token="var(--a-shadow-xlarge)" />;
export const Focus = () => <Shadow token="var(--a-shadow-focus)" />;
export const FocusInverted = () => (
<Shadow token="var(--a-shadow-focus-inverted)" />
+1108
View File
File diff suppressed because it is too large. Load diff
+5 -12
View File
@@ -1,19 +1,12 @@
FROM node:18.1-alpine3.14
RUN apk add --no-cache libc6-compat
FROM gcr.io/distroless/nodejs:18 as runtime
ENV NODE_ENV production
WORKDIR /app
COPY aksel.nav.no/website/.next/standalone ./
# COPY aksel.nav.no/website/package.json .
# COPY aksel.nav.no/website/.next/ .next/
# COPY aksel.nav.no/website/public/ public
# COPY aksel.nav.no/website/.env .
# COPY aksel.nav.no/website/next.config.js .
# COPY node_modules/ node_modules/
# COPY @navikt/ @navikt/
# CMD yarn start
COPY aksel.nav.no/website/next-logger.config.js ./
ENV NODE_OPTIONS '-r next-logger'
EXPOSE 3000
CMD ["node", "aksel.nav.no/website/server.js"]
CMD ["aksel.nav.no/website/server.js"]
+4 -8
View File
@@ -17,14 +17,6 @@ spec:
path: /api/isAlive
initialDelay: 10
resources:
limits:
cpu: 2000m
memory: 2048Mi
requests:
cpu: 500m
memory: 1024Mi
accessPolicy:
outbound:
external:
@@ -35,3 +27,7 @@ spec:
- host: cdn.sanity.io
rules: []
env:
filesFrom:
- emptyDir:
medium: Memory
mountPath: /app/aksel.nav.no/website/.next/cache
+4 -7
View File
@@ -21,13 +21,6 @@ spec:
path: /api/isAlive
initialDelay: 20
timeout: 1
resources:
limits:
cpu: 2000m
memory: 2048Mi
requests:
cpu: 500m
memory: 1024Mi
accessPolicy:
outbound:
external:
@@ -46,3 +39,7 @@ spec:
age: 7
numNewerVersions: 3
withState: ANY
filesFrom:
- emptyDir:
medium: Memory
mountPath: /app/aksel.nav.no/website/.next/cache
+5 -5
View File
@@ -15,11 +15,11 @@
"website"
],
"dependencies": {
"@navikt/aksel-icons": "^4.1.2",
"@navikt/ds-css": "^4.1.2",
"@navikt/ds-react": "^4.1.2",
"@navikt/ds-tailwind": "^4.1.2",
"@navikt/ds-tokens": "^4.1.2"
"@navikt/aksel-icons": "^4.4.0",
"@navikt/ds-css": "^4.4.0",
"@navikt/ds-react": "^4.4.0",
"@navikt/ds-tailwind": "^4.4.0",
"@navikt/ds-tokens": "^4.4.0"
},
"devDependencies": {
"prettier-plugin-tailwindcss": "^0.2.3"
@@ -25,20 +25,8 @@ import {
UuFeedback,
Video,
} from ".";
export const InlineCode = (props: React.HTMLAttributes<HTMLElement>) => (
<code
className="bg-surface-alt-3-subtle/50 py-05 rounded px-1 leading-normal [font-size:_calc(16em_/_18)]"
{...props}
/>
);
export const KBD = (props: React.HTMLAttributes<HTMLElement>) => (
<kbd
className="text-text-default bg-surface-default py-05 text-medium whitespace-nowrap rounded px-1 leading-normal shadow-[0_0_0_1px_var(--a-border-strong)]"
{...props}
/>
);
import { KBD } from "components/website-modules/KBD";
import { InlineCode } from "components/website-modules/InlineCode";
const serializers = {
types: {
@@ -113,6 +101,7 @@ const serializers = {
spacing
level="4"
size="small"
id={`h${node._key}`}
{...textProps}
/>
);
@@ -169,7 +158,7 @@ const serializers = {
return (
<BodyLong
as="li"
className="ml-5 mb-3 max-w-[calc(theme(spacing.text)_-_1em)]"
className="mb-3 ml-5 max-w-[calc(theme(spacing.text)_-_1em)]"
>
{props.children}
</BodyLong>
@@ -1,13 +0,0 @@
import "../dist/tw.css";
import { Provider } from "@navikt/ds-react";
import { IdContext } from "components/website-modules/utils/contexts";
export const NextDecorator = ({ children }: { children: React.ReactNode }) => (
<Provider>
<IdContext.Provider value={{ id: "storybook-testid" }}>
<div id="__next" className="aksel antialiased">
{children}
</div>
</IdContext.Provider>
</Provider>
);
+1 -3
View File
@@ -13,15 +13,13 @@ 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 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/VideoEmbed";
export * from "./website-modules/BreadCrumbs";
export { default as EditButton } from "./website-modules/EditButton";
export { default as Feedback } from "./website-modules/feedback";
export * from "./website-modules/H2";
export { default as PreviewBanner } from "./website-modules/PreviewBanner";
export * from "./website-modules/TilToppen";
export * from "./website-modules/TOC";
export * from "./website-modules/utils";
@@ -75,13 +75,13 @@ export const WithSidebar = ({
<>
<Header />
<div className="bg-bg-default">
<div className="mx-auto mt-6 mb-24 flex w-full max-w-screen-2xl gap-6">
<div className="mx-auto mb-24 mt-6 flex w-full max-w-screen-2xl gap-6">
<Sidebar kategori={pageType.type} links={sidebar} />
<div className="relative w-full">
<main
tabIndex={-1}
id="hovedinnhold"
className="min-h-screen-header md:max-w-screen-sidebar relative w-full px-4 focus:outline-none sm:pl-6 sm:pr-6 md:pl-0"
className="min-h-screen-header md:max-w-screen-sidebar relative z-0 w-full px-4 focus:outline-none sm:pl-6 sm:pr-6 md:pl-0"
>
<div
className={cl(
@@ -89,7 +89,9 @@ export const WithSidebar = ({
{
"bg-surface-subtle flex items-center justify-between":
variant === "page",
"bg-deepblue-700 grid py-[4.25rem] pb-6":
"bg-deepblue-700/80 grid py-[4.25rem] pb-6":
variant !== "page",
"before:from-deepblue-700 before:absolute before:inset-0 before:-z-10 before:rounded-xl before:bg-gradient-to-br before:via-blue-500 before:to-violet-700":
variant !== "page",
}
)}
@@ -115,18 +117,17 @@ export const WithSidebar = ({
size="xlarge"
className={cl({
"text-deepblue-800": variant === "page",
"text-text-on-action bg-deepblue-700/80 w-fit":
variant !== "page",
"text-text-on-action w-fit": variant !== "page",
})}
>
{pageType.title}
</Heading>
<div
className={cl({
"bg-deepblue-700/80 w-fit": variant !== "page",
"w-fit": variant !== "page",
})}
>
{intro && intro}
{intro}
</div>
</div>
{variant === "page" && pageProps.status?.bilde && (
@@ -149,17 +150,15 @@ export const WithSidebar = ({
</div>
)}
{variant === "landingPage" && (
<div className="pointer-events-none absolute top-0 right-0 hidden sm:block">
<HeaderCube className="text-deepblue-300 z-0 max-h-full" />
<div className="pointer-events-none absolute right-0 top-0 hidden sm:block">
<HeaderCube className="text-deepblue-300 z-[-1] max-h-full" />
</div>
)}
</div>
<div className={cl("sm:px-6 md:px-10", { flex: withToc })}>
{withToc && (
<TableOfContentsv2
changedState={pageProps["content"]}
hideToc={false}
/>
<TableOfContentsv2 changedState={pageProps["content"]} />
)}
<div className="w-full">
{children}
@@ -2,8 +2,15 @@ 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 { EditButton } from "../..";
import FooterForm from "./FooterForm";
import dynamic from "next/dynamic";
export const EditButton = dynamic(
() => import("components/website-modules/EditButton"),
{
ssr: false,
}
);
const Footer = () => {
return (
@@ -1,24 +0,0 @@
import Footer from "./Footer";
import { Meta } from "@storybook/react";
import { NextDecorator } from "../../StoryDecorator";
const MetaData: Meta = {
component: Footer,
tags: ["autodocs"],
decorators: [
(Story) => (
<NextDecorator>
<Story />
</NextDecorator>
),
],
parameters: {
layout: "fullscreen",
},
};
export default MetaData;
export const FooterDemo = {
render: () => <Footer />,
};
@@ -1,10 +1,33 @@
import Logo from "components/assets/Logo";
import Link from "next/link";
import { logNav } from "../..";
import cl from "clsx";
import { useRouter } from "next/router";
import Logo from "components/assets/Logo";
import { Hamburger } from "components/layout/header/Hamburger";
import { GlobalSearch } from "./search";
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: () => (
<Button
variant="primary"
className="hover:bg-deepblue-700 bg-deepblue-600 h-11"
aria-keyshortcuts="Control+b"
icon={
<MagnifyingGlassIcon
className="pointer-events-none -mt-[1px] shrink-0 text-2xl"
aria-label="Åpne meny"
aria-hidden
/>
}
iconPosition="left"
>
Søk
</Button>
),
ssr: false,
});
const LinkElement = ({ name, href, prefetch = undefined }) => {
const { asPath } = useRouter();
@@ -59,7 +82,7 @@ export const Header = ({
"mx-auto flex h-full max-w-screen-2xl items-center pr-4 lg:pr-6"
)}
>
<div className="flex h-11 items-center pr-4 pl-4 sm:pr-6 sm:pl-4">
<div className="flex h-11 items-center pl-4 pr-4 sm:pl-4 sm:pr-6">
<Link
href="/"
passHref
@@ -88,7 +111,9 @@ export const Header = ({
<LinkElement name="Bloggen" href="/produktbloggen" />
</ul>
</nav>
<GlobalSearch />
<div className="z-[1050] ml-auto mr-4 flex justify-center lg:ml-0 lg:mr-0">
<GlobalSearch />
</div>
<div className="mr-2 h-full lg:hidden">
<Hamburger />
</div>
@@ -1,3 +1,4 @@
/* stylelint-disable aksel/design-token-no-global-override */
.modalOverlay {
z-index: var(--a-z-index-modal);
@apply bg-surface-backdrop fixed inset-0 grid place-items-center lg:hidden;
@@ -5,7 +6,7 @@
.modalOverlaySearch {
z-index: 3000;
@apply bg-surface-backdrop fixed inset-0 grid place-items-center;
@apply bg-deepblue-900/20 fixed inset-0 grid place-items-center;
}
.akselTertiaryButton {
@@ -0,0 +1,36 @@
import { searchOptions } from "@/types";
import { Skeleton } from "@navikt/ds-react";
import { useContext } from "react";
import { SearchResultContext } from "./providers";
export const Error = () => {
const { isValidating, error } = useContext(SearchResultContext);
if (error) {
return (
<p aria-live="assertive" className="mx-auto w-fit px-6 py-24">
Noe gikk galt! Last siden på nytt eller ta kontakt med Aksel.
</p>
);
}
if (!isValidating) {
return null;
}
return (
<div className="grid w-full gap-2 p-1">
<Skeleton height="3rem" variant="rounded" />
<div className="flex flex-wrap gap-2 px-4 pb-4 md:px-10">
{Object.keys(searchOptions)
.slice(0, Object.keys(searchOptions).length - 1)
.map((x) => (
<Skeleton variant="rounded" height="1.75rem">
{searchOptions[x].display}
</Skeleton>
))}
</div>
</div>
);
};
@@ -0,0 +1,110 @@
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 { useContext, useRef } from "react";
import { useShortcut } from "./hooks";
import {
SearchContext,
SearchNavigationContext,
SearchResultContext,
} from "./providers";
export const SearchForm = () => {
const { open, setOpen, tags, setTags, query, setQuery, os } =
useContext(SearchContext);
const { update, results, reset, isValidating, error } =
useContext(SearchResultContext);
const { close } = useContext(SearchNavigationContext);
const inputRef = useRef(null);
useShortcut(open, setOpen, inputRef);
const handleSearchStart = (value: string) => {
setQuery(value);
value === "" ? reset() : update(value, tags);
};
const noHits = (key: string) => {
return !Object.hasOwn(results?.groupedHits ?? {}, key);
};
const noHitsAndQuery = (key: string) => {
return query.length > 0 && !tags.find((x) => x === key);
};
if (isValidating || error) {
return null;
}
const chipsToShow = Object.entries(searchOptions)
.filter((x) => !x[1].hidden)
.filter(
([key]) =>
!(noHitsAndQuery(key) && noHits(key) && results?.hits[key] === 0)
);
return (
<form
role="search"
onSubmit={(e) => e.preventDefault()}
className="bg-surface-default grid w-full gap-2"
>
<div className="flex flex-row-reverse items-center gap-1 p-1">
<Button
variant="tertiary-neutral"
icon={<XMarkIcon title="Lukk søk" />}
onClick={close}
type="button"
/>
<Search
label={
<span className="flex items-center gap-2">
<span>Søk i hele Aksel</span>
{os === "mac" ? <KBD>CMD + B</KBD> : <KBD>CTRL + B</KBD>}
</span>
}
aria-autocomplete="both"
variant="simple"
value={query}
onChange={(v) => handleSearchStart(v)}
onClear={() => {
setQuery("");
reset();
}}
ref={inputRef}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
autoFocus
id="aksel-search-input"
clearButton={false}
placeholder="Søk gjennom hele aksel"
className="border-none pr-0 focus:shadow-none"
/>
</div>
{chipsToShow.length !== 0 && (
<Chips className="px-4 pb-4 md:px-10">
{chipsToShow.map(([key, val]) => (
<Chips.Toggle
key={key}
variant="neutral"
selected={tags.includes(key as keyof typeof searchOptions)}
type="button"
onClick={() => {
const newTags = (tags as string[]).includes(key)
? tags.filter((y) => y !== key)
: [...tags, key];
setTags(newTags as Array<keyof typeof searchOptions>);
update(query, newTags);
}}
>
{val.display}
</Chips.Toggle>
))}
</Chips>
)}
</form>
);
};
@@ -1,109 +0,0 @@
import { Label } from "@navikt/ds-react";
import { GroupedHitsT, SearchHitT, searchOptions } from "@/types";
import { Hit, IconHit, IconPageHit } from "./Hit";
import React from "react";
export function Group({
groups,
query,
startIndex,
logSuccess,
}: {
groups: GroupedHitsT;
query: string;
startIndex: number;
logSuccess: (index: number, url: string) => void;
}) {
if (Object.keys(groups).length === 0) {
return null;
}
return (
<>
{Object.entries(groups)
.sort((a, b) => searchOptions[a[0]].index - searchOptions[b[0]].index)
.map(([key, val], index, arr) => {
const prev = arr.slice(0, index);
const total =
prev.reduce((prev, cur) => prev + cur[1].length, 0) + startIndex;
return (
<GroupComponent
logSuccess={logSuccess}
startIndex={total}
key={key}
heading={`${searchOptions[key].display} (${val.length})`}
hits={val}
query={query}
/>
);
})}
</>
);
}
export function GroupComponent({
heading,
hits,
query,
startIndex,
logSuccess,
}: {
heading: React.ReactNode;
hits: SearchHitT[];
query: string;
startIndex: number;
logSuccess: (index: number, url: string) => void;
}) {
return (
<div>
<div className="z-10 mt-4 rounded bg-teal-100 p-2">
<Label className="text-text-default" as="h3">
{heading}
</Label>
</div>
<ul className="mt-2">
{hits.map((x, xi) => {
switch (x.item._type) {
case "icon":
return (
<React.Fragment key={x.item.name + xi}>
<IconHit
key={x.item.name}
hit={x}
query={query}
index={startIndex + xi}
logSuccess={logSuccess}
/>
</React.Fragment>
);
case "icon_page":
return (
<React.Fragment key={x.item.heading + xi}>
<IconPageHit
key={x.item.heading}
hit={x}
query={query}
index={startIndex + xi}
logSuccess={logSuccess}
/>
</React.Fragment>
);
default:
return (
<React.Fragment key={x.item._id}>
<Hit
key={x.item._id}
hit={x}
query={query}
index={startIndex + xi}
logSuccess={logSuccess}
/>
</React.Fragment>
);
}
})}
</ul>
</div>
);
}
@@ -1,212 +1,145 @@
import { isNew } from "@/utils";
import { Tag } from "@navikt/ds-react";
import cl from "classnames";
import Image from "next/legacy/image";
import { StatusTag } from "components/website-modules/StatusTag";
import { SearchHitT } from "@/types";
import NextLink from "next/link";
import { urlFor } from "@/sanity/interface";
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 { StatusTag } from "components/website-modules/StatusTag";
import Image from "next/legacy/image";
import { default as Link, default as NextLink } from "next/link";
import { forwardRef, useContext } from "react";
import { SearchContext, SearchLoggingContext } from "./providers";
import { highlightMatches, highlightStr } from "./utils";
export function Hit({
hit,
query,
index,
logSuccess,
}: {
hit: SearchHitT;
query: string;
index: number;
logSuccess: (index: number, url: string) => void;
}) {
if (hit.item._type === "icon" || hit.item._type === "icon_page") {
return;
export const Hit = forwardRef<
HTMLLIElement,
{
hit: SearchHitT | Omit<SearchHitT, "score" | "anchor">;
index: number;
simple?: boolean;
tag?: Partial<keyof typeof searchOptions>;
}
>(({ hit, index, simple = false, tag }, ref) => {
const { query } = useContext(SearchContext);
const { logSuccess } = useContext(SearchLoggingContext);
const getHref = () => {
if ("anchor" in hit) {
return `/${hit.item.slug}${hit?.anchor ? `#h${hit.anchor}` : ""}`;
}
return `/${hit.item.slug}`;
};
return (
<li
ref={ref}
className={cl(
"focus-within:shadow-focus border-border-subtle group relative flex cursor-pointer scroll-mt-12 items-center justify-between gap-4 rounded border-b px-2 last-of-type:border-b-0 focus-within:z-10 hover:bg-gray-100"
"border-border-subtle relative flex scroll-my-10 items-center justify-between gap-3 border-b px-2 last-of-type:border-b-0"
)}
>
<div className="px-2 py-6">
<NextLink
href={`/${hit.item.slug}`}
passHref
onClick={() => logSuccess(index, `/${(hit.item as any).slug}`)}
className="flex items-center gap-2 text-xl font-semibold after:absolute after:inset-0 focus:outline-none"
>
<span className="group-hover:underline">
{highlightStr(hit.item.heading, query)}
</span>
<StatusTag status={hit?.item?.status?.tag} aria-hidden />
</NextLink>
<span className="font-regular text-text-subtle text-lg" aria-hidden>
{hit.highlight.shouldHightlight ? (
<div>{highlightStr(hit.highlight.description, query)}</div>
) : (
<div>{hit.highlight.description}</div>
)}
</span>
<span className="mt-4 flex gap-2 empty:mt-0">
{hit.item?.tema &&
hit.item?.tema.map((x) => (
<Tag variant="alt3" size="xsmall" key={x}>
{x}
</Tag>
))}
</span>
</div>
<div className="hidden aspect-square w-24 sm:block">
{hit.item?.status?.bilde && (
<Image
src={urlFor(hit.item.status.bilde).auto("format").url()}
decoding="sync"
width="96"
height="96"
layout="fixed"
objectFit="contain"
alt={hit.item?.heading + " thumbnail"}
aria-hidden
/>
)}
</div>
</li>
);
}
export function IconHit({
hit,
query,
index,
logSuccess,
}: {
hit: SearchHitT;
query: string;
index: number;
logSuccess: (index: number, url: string) => void;
}) {
if (hit.item._type !== "icon") {
return;
}
return (
<li
className={cl(
"focus-within:shadow-focus border-border-subtle group relative flex cursor-pointer scroll-mt-12 items-center justify-between gap-4 rounded border-b px-2 last-of-type:border-b-0 focus-within:z-10 hover:bg-gray-100"
)}
>
<div className="px-2 py-2">
<NextLink
href={`/ikoner/${hit.item.name}`}
passHref
onClick={() => logSuccess(index, `/${(hit.item as any).name}`)}
className="flex items-center gap-2 text-xl font-semibold after:absolute after:inset-0 focus:outline-none"
>
<span className="group-hover:underline">
{highlightStr(`${hit.item.name}Icon`, query)}
</span>
{isNew(hit.item?.created_at ?? "") && (
<Tag variant="info" size="small" aria-hidden>
Ny
</Tag>
)}
</NextLink>
<span className="font-regular text-text-subtle text-lg" aria-hidden>
<div>{highlightStr(hit.item.sub_category, query)}</div>
</span>
</div>
<div className="hidden aspect-square w-24 place-content-center sm:grid">
<Image
src={`https://raw.githubusercontent.com/navikt/aksel/main/%40navikt/aksel-icons/icons/${hit.item.name}.svg`}
decoding="sync"
width="24"
height="24"
layout="fixed"
objectFit="contain"
alt={hit.item.name + " thumbnail"}
aria-hidden
/>
</div>
</li>
);
}
export function IconPageHit({
hit,
query,
index,
logSuccess,
}: {
hit: SearchHitT;
query: string;
index: number;
logSuccess: (index: number, url: string) => void;
}) {
if (hit.item._type !== "icon_page") {
return;
}
return (
<li
className={cl(
"focus-within:shadow-focus border-border-subtle group relative flex cursor-pointer scroll-mt-12 items-center justify-between gap-4 rounded border-b px-2 last-of-type:border-b-0 focus-within:z-10 hover:bg-gray-100"
)}
>
<div className="px-2 py-2">
<NextLink
href="/ikoner"
passHref
onClick={() => logSuccess(index, `/ikoner`)}
className="flex items-center gap-2 text-xl font-semibold after:absolute after:inset-0 focus:outline-none"
>
<span className="group-hover:underline">
{highlightStr(`Ikoner`, query)}
</span>
</NextLink>
<span className="font-regular text-text-subtle text-lg" aria-hidden>
<div>{highlightStr(hit.item.description, query)}</div>
</span>
</div>
<div className="hidden aspect-square place-items-center sm:grid">
<Image
src="/images/thumbnail/ikoner/thumbnail.svg"
decoding="sync"
width="96"
height="96"
layout="fixed"
objectFit="contain"
alt=""
aria-hidden
/>
</div>
</li>
);
}
function splitStr(str: string, query: string) {
const regexStr = query.split(" ").join("|");
return str.split(new RegExp(`(${regexStr})`, "gi"));
}
function highlightStr(str: string, query: string) {
return (
<span>
{splitStr(str, query).map((part, i) => (
<div className="w-full truncate px-2 py-4">
<span
key={i}
className={cl({
"text-text-default bg-teal-200/80": query
.split(" ")
.map((x) => x.toLowerCase())
.includes(part.toLowerCase()),
"flex flex-col gap-1": simple,
"flex items-center gap-2": !simple,
})}
>
{part}
<NextLink
href={getHref()}
onClick={() => logSuccess(index, `/${(hit.item as any).slug}`)}
className="focus-visible:shadow-focus focus-visible:bg-border-focus focus-visible:text-text-on-action group scroll-my-32 text-xl font-semibold focus:outline-none"
>
<span className="group-hover:underline">
{highlightStr(hit.item.heading, query, tag)}
</span>
</NextLink>
{simple ? (
<Tag
type={hit.item._type}
size="xsmall"
text={hit.item.tema ? hit.item.tema[0] : undefined}
inline
aria-hidden
/>
) : (
<StatusTag status={hit?.item?.status?.tag} aria-hidden />
)}
</span>
))}
</span>
{!simple && <HeadingLinks index={index} hit={hit} />}
</div>
{!simple && (
<div className="hidden aspect-square w-24 sm:block">
{hit.item?.status?.bilde && (
<Image
src={urlFor(hit.item.status.bilde).auto("format").url()}
decoding="sync"
width="96"
height="96"
layout="fixed"
objectFit="contain"
alt={hit.item?.heading + " thumbnail"}
aria-hidden
/>
)}
</div>
)}
</li>
);
});
const highlightedHeadings = ["eksempler", "props", "tokens", "retningslinjer"];
function HeadingLinks({
hit,
index,
}: {
hit: SearchHitT | Omit<SearchHitT, "score" | "anchor">;
index: number;
}) {
const { logSuccess } = useContext(SearchLoggingContext);
const Description = () => {
if (hit.matches[0].key !== "heading") {
return highlightMatches(hit.matches[0]);
}
return (
<span className="font-regular text-text-subtle max-w-full text-lg">
{hit.description}
</span>
);
};
if (hit.item._type === "komponent_artikkel") {
return (
<>
<Description />
<Chips className="mt-3" size="small">
{hit.item.lvl2
.filter(
(x) =>
!!x.id && highlightedHeadings.includes(x.text.toLowerCase())
)
.map((x) => (
<Link
prefetch={false}
key={x.text}
href={`/${hit.item.slug}${`#h${x.id}`}`}
onClick={() =>
logSuccess(index, `/${(hit.item as any).slug}`, x.text)
}
className="min-h-6 bg-surface-neutral-subtle focus-visible:shadow-focus hover:bg-surface-neutral-subtle-hover ring-border-subtle flex items-center justify-center rounded-full px-2 ring-1 ring-inset focus:outline-none"
>
<span>{x.text}</span>
<ChevronRightIcon aria-hidden className="-mr-1" />
</Link>
))}
</Chips>
</>
);
}
return <Description />;
}
@@ -0,0 +1,132 @@
import { GroupedHitsT, SearchHitT, searchOptions } from "@/types";
import { Heading } from "@navikt/ds-react";
import { Tag } from "components/sanity-modules/frontpage-blocks/Tag";
import React, { useEffect, useMemo, useRef, useState } from "react";
import { Hit } from "./Hit";
import cl from "clsx";
export function CollectionMapper({
groups,
startIndex,
}: {
groups: GroupedHitsT;
startIndex: number;
}) {
if (Object.keys(groups).length === 0) {
return null;
}
return (
<>
{Object.entries(groups)
.sort((a, b) => searchOptions[a[0]].index - searchOptions[b[0]].index)
.map(
(
[key, val]: [key: keyof typeof searchOptions, val: SearchHitT[]],
index,
arr
) => {
const prev = arr.slice(0, index);
const total =
prev.reduce((prev, cur) => prev + cur[1].length, 0) + startIndex;
return (
<Collection
startIndex={total}
key={key}
heading={`${searchOptions[key].display} (${val.length})`}
tag={key}
hits={val}
/>
);
}
)}
</>
);
}
export function Collection({
heading,
hits,
startIndex,
simple = false,
tag,
}: {
heading?: React.ReactNode;
hits: SearchHitT[] | Omit<SearchHitT, "score" | "anchor">[];
simple?: boolean;
startIndex: number;
tag?: Partial<keyof typeof searchOptions>;
}) {
const [intersected, setIntersected] = useState(false);
const item = useRef(null);
const split = useMemo(() => {
if (hits.length <= (simple ? 20 : 5)) {
return { initial: hits, lazy: null };
}
return { initial: hits.slice(0, 4), lazy: hits.slice(4 + 1) };
}, [hits, simple]);
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
entry.isIntersecting && setIntersected(entry.isIntersecting);
});
item.current && observer.observe(item.current);
return () => observer.disconnect();
}, []);
return (
<div>
{heading && tag && (
<h3
className={cl("sticky top-0 z-10 p-2 px-4 md:px-10", {
"bg-deepblue-50": ["komponent_artikkel", "ds_artikkel"].includes(
tag
),
"bg-violet-50": ["aksel_prinsipp", "aksel_standalone"].includes(
tag
),
"bg-teal-50": tag === "aksel_artikkel",
"bg-pink-50": tag === "aksel_blogg",
})}
>
<Tag hTag="span" type={tag} size="small" inline count={hits.length} />
</h3>
)}
{heading && !tag && (
<Heading
className="bg-surface-subtle sticky top-0 z-10 p-2 px-4 md:px-10"
size="small"
level="3"
>
{heading}
</Heading>
)}
<ul className="mt-2 px-0 md:px-6">
{split.initial.map((x, xi) => (
<Hit
key={xi}
hit={x}
index={startIndex + xi}
ref={xi === split.initial.length - 1 ? item : null}
simple={simple}
tag={tag}
/>
))}
{split.lazy &&
intersected &&
split.lazy.map((x, xi) => (
<Hit
key={xi}
hit={x}
index={startIndex + xi}
simple={simple}
tag={tag}
/>
))}
</ul>
</div>
);
}
@@ -0,0 +1,89 @@
import { searchOptions } from "@/types";
import { ChangeLogIconOutline } from "components/assets";
import { useContext } from "react";
import { Collection, CollectionMapper } from "./HitCollection";
import { SearchContext, SearchResultContext } from "./providers";
import { Heading, Label } from "@navikt/ds-react";
export const Results = () => {
const { tags, query } = useContext(SearchContext);
const { results, isValidating, error, mostResent } =
useContext(SearchResultContext);
if (isValidating || error) {
return null;
}
return (
<div className="flex h-full flex-col overflow-y-auto">
{!results && mostResent && (
<div id="aksel-search-results" aria-label="Nyeste artikler">
<Collection
startIndex={0}
heading={
<span className="flex items-center gap-2">
Nyeste artikler
<ChangeLogIconOutline className="shrink-0" aria-hidden />
</span>
}
simple
hits={mostResent}
/>
</div>
)}
{!results?.totalHits && query && (
<Heading
size="medium"
as="p"
className="mx-auto w-fit px-6 py-24"
aria-live="polite"
aria-atomic
>
<span className="text-text-subtle">{`Ingen treff på "`}</span>
<span className="break-all">{query}</span>
<span className="text-text-subtle">"</span>
{tags.length > 0 && (
<span className="text-text-subtle">{` i ${tags
.map((x) => searchOptions[x].display.toLowerCase())
.join(", ")}`}</span>
)}
</Heading>
)}
{results?.totalHits > 0 && (
<div id="aksel-search-results" aria-label="Søkeresultater">
<Label as="p" className="sr-only" aria-live="polite">
{`${results?.totalHits} treff på "${results.query}"${
tags.length > 0
? ` i ${tags
.map((x) => searchOptions[x].display.toLowerCase())
.join(", ")}`
: ""
}`}
</Label>
<div className="pb-4">
{results?.topResults.length > 0 && (
<Collection
startIndex={1}
heading={
<span className="flex items-center gap-2">
Beste treff
<ChangeLogIconOutline className="shrink-0" />
</span>
}
hits={results?.topResults}
/>
)}
<CollectionMapper
startIndex={
results.topResults.length > 0
? results.topResults.length + 1
: 1
}
groups={results.groupedHits}
/>
</div>
</div>
)}
</div>
);
};
@@ -0,0 +1,53 @@
import { MagnifyingGlassIcon } from "@navikt/aksel-icons";
import { Button } from "@navikt/ds-react";
import cl from "clsx";
import { useContext, useEffect } from "react";
import ReactModal from "react-modal";
import styles from "../header.module.css";
import { Error } from "./Error";
import { SearchForm } from "./Form";
import { Results } from "./Results";
import { SearchContext, SearchNavigationContext } from "./providers";
export const Search = () => {
const { open, setOpen } = useContext(SearchContext);
const { close } = useContext(SearchNavigationContext);
useEffect(() => {
ReactModal.setAppElement("#__next");
}, []);
return (
<>
<Button
variant="primary"
className="hover:bg-deepblue-700 bg-deepblue-600 h-11"
aria-keyshortcuts="Control+b"
icon={
<MagnifyingGlassIcon
className="pointer-events-none -mt-[1px] shrink-0 text-2xl"
aria-label="Åpne meny"
aria-hidden
/>
}
iconPosition="left"
onClick={() => setOpen(true)}
>
Søk
</Button>
<ReactModal
isOpen={open}
onRequestClose={close}
aria={{ modal: true }}
contentLabel="Søk"
className="bg-surface-default focus-visible:shadow-focus animate-searchModal shadow-xlarge absolute inset-0 mx-auto my-auto flex h-[90%] max-h-[52rem] w-full max-w-[90%] flex-col overflow-x-hidden rounded-lg focus:outline-none md:h-[80%] lg:max-w-4xl"
overlayClassName={cl(styles.modalOverlaySearch, "backdrop-blur-[2px]")}
htmlOpenClassName="overflow-hidden"
>
<Error />
<SearchForm />
<Results />
</ReactModal>
</>
);
};
@@ -0,0 +1,102 @@
import { SearchResultsT } from "@/types";
import { createSearchResult } from "../utils";
describe("createSearchResult", () => {
let result;
let rawResults;
beforeEach(() => {
result = [
{
item: { _type: "komponent_artikkel" },
score: 0.05,
matches: [],
},
{
item: { _type: "aksel_artikkel" },
score: 0.1,
matches: [],
},
{
item: { _type: "ds_artikkel" },
score: 0.15,
matches: [],
},
{
item: { _type: "aksel_blogg" },
score: 0.2,
matches: [],
},
{
item: { _type: "aksel_prinsipp" },
score: 0.25,
matches: [],
},
{
item: { _type: "aksel_standalone" },
score: 0.3,
matches: [],
},
];
rawResults = [
{ item: { _type: "komponent_artikkel" }, score: 0.05 },
{ item: { _type: "aksel_artikkel" }, score: 0.1 },
{ item: { _type: "ds_artikkel" }, score: 0.15 },
{ item: { _type: "aksel_blogg" }, score: 0.2 },
{ item: { _type: "aksel_prinsipp" }, score: 0.25 },
{ item: { _type: "aksel_standalone" }, score: 0.3 },
];
});
it("should group hits by type", () => {
const expected = {
komponent_artikkel: [
{ item: { _type: "komponent_artikkel" }, score: 0.05, matches: [] },
],
aksel_artikkel: [
{ item: { _type: "aksel_artikkel" }, score: 0.1, matches: [] },
],
ds_artikkel: [
{ item: { _type: "ds_artikkel" }, score: 0.15, matches: [] },
],
aksel_blogg: [
{ item: { _type: "aksel_blogg" }, score: 0.2, matches: [] },
],
aksel_prinsipp: [
{ item: { _type: "aksel_prinsipp" }, score: 0.25, matches: [] },
],
aksel_standalone: [
{ item: { _type: "aksel_standalone" }, score: 0.3, matches: [] },
],
};
const actual: SearchResultsT = createSearchResult(result, rawResults);
expect(actual.groupedHits).toEqual(expected);
});
it("should return top 3 results with score < 0.1", () => {
const expected = [
{ item: { _type: "komponent_artikkel" }, score: 0.05, matches: [] },
];
const actual: SearchResultsT = createSearchResult(result, rawResults);
expect(actual.topResults).toEqual(expected);
});
it("should return total number of hits", () => {
const expected = 6;
const actual: SearchResultsT = createSearchResult(result, rawResults);
expect(actual.totalHits).toEqual(expected);
});
it("should return number of hits by type", () => {
const expected = {
komponent_artikkel: 1,
aksel_artikkel: 1,
ds_artikkel: 1,
aksel_blogg: 1,
aksel_prinsipp: 1,
aksel_standalone: 1,
};
const actual: SearchResultsT = createSearchResult(result, rawResults);
expect(actual.hits).toEqual(expected);
});
});
@@ -0,0 +1,2 @@
export * from "./useSearch";
export * from "./useShortcut";
@@ -0,0 +1,56 @@
import { allArticleDocuments } from "@/sanity/config";
import { SearchResultsT } from "@/types";
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);
const { data, error, isValidating } = useSWRImmutable(
`/searchindex.json`,
(query) => fetch(query).then((res) => res.json())
);
const updateResults = useMemo(
() =>
debounce((value: string, tags: string[]) => {
if (!value) {
return;
}
const rawResults = fuseSearch(data, value);
const formatedResults = formatResults(
rawResults.filter((x) =>
(tags.length > 0 ? tags : allArticleDocuments).includes(
x.item._type
)
)
);
setFuseResults({
...createSearchResult(formatedResults, rawResults),
query: value,
});
logSearch({
type: "standard",
searchedFromUrl: window.location.pathname,
query: value,
filter: tags,
});
}),
[data]
);
return {
results: fuseResults,
update: updateResults,
error,
isValidating,
reset: () => setFuseResults(null),
rawData: data,
};
};
@@ -0,0 +1,28 @@
import { MutableRefObject, useEffect } from "react";
export const useShortcut = (
open: boolean,
setOpen: (value: boolean) => void,
inputRef: MutableRefObject<HTMLInputElement>
) => {
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (
event.key === "b" &&
(event.metaKey || event.ctrlKey) &&
!event.shiftKey
) {
event.preventDefault();
if (open) {
inputRef.current?.focus();
} else {
setOpen(true);
}
}
};
document.addEventListener("keydown", listener);
return () => document.removeEventListener("keydown", listener);
}, [inputRef, open, setOpen]);
};
@@ -1,372 +1,21 @@
import { useDebounce, logSearch } from "@/utils";
import { Search } from "./Search";
import {
Button,
Checkbox,
CheckboxGroup,
Detail,
Loader,
Search,
} from "@navikt/ds-react";
import { ChangeLogIconOutline } from "components/assets";
import { SearchLogT, searchOptions, SearchResultsT } from "@/types";
import { useRouter } from "next/router";
import React, { useCallback, useEffect, useRef, useState } from "react";
import ReactModal from "react-modal";
import { Group, GroupComponent } from "./Group";
import styles from "../header.module.css";
import { MagnifyingGlassIcon } from "@navikt/aksel-icons";
SearchProvider,
SearchResultProvider,
SearchLoggingProvider,
SearchNavigationProvider,
} from "./providers";
/**
* https://www.figma.com/file/71Sm1h6VV23lbBbQ3CJJ9t/Aksel-v2?node-id=1861%3A186079&t=ARKgZcA6B7ysmG3V-0
* TODO:
* - Oppdatere url-query basert på query + filter: ?search=abcd&filter=god_praksis
* - Oppdatere søkefelt og filter basert på url.
const GlobalSearch = () => (
<SearchProvider>
<SearchLoggingProvider>
<SearchResultProvider>
<SearchNavigationProvider>
<Search />
</SearchNavigationProvider>
</SearchResultProvider>
</SearchLoggingProvider>
</SearchProvider>
);
*/
export const GlobalSearch = () => {
const [results, setResults] = useState<SearchResultsT>(null);
const [loading, setLoading] = useState(false);
const [open, setOpen] = useState(false);
const [activeTags, setTags] = useState<Array<keyof typeof searchOptions>>([]);
const [os, setOs] = useState<"mac" | "windows">("windows");
const inputRef = useRef(null);
const session = useRef<{ retires: number; queries: string[] }>({
retires: 0,
queries: [],
});
const firstSearch = useRef<boolean>(true);
const prevSearch = useRef<string>("");
const router = useRouter();
const [query, setQuery] = useState("");
const debouncedSearchTerm = useDebounce(query);
useEffect(() => {
ReactModal.setAppElement("#__next");
navigator.userAgent?.indexOf("Mac OS X") !== -1
? setOs("mac")
: setOs("windows");
}, []);
const logSearchAttempt = useCallback(
(hits: number, type: SearchLogT["type"]) => {
const data: SearchLogT = {
type,
searchedFromUrl: router.asPath,
hits: hits ?? 0,
retries: session.current.retires,
retriedQueries: session.current.queries,
query: debouncedSearchTerm,
filter: activeTags,
};
logSearch(data);
},
[router.asPath, debouncedSearchTerm, activeTags]
);
const logSuccessSearchAttempt = useCallback(
(index: number, url: string) => {
const data: SearchLogT = {
type: "suksess",
searchedFromUrl: router.asPath,
hits: results?.totalHits ?? 0,
retries: session.current.retires,
retriedQueries: session.current.queries,
query: debouncedSearchTerm,
filter: activeTags,
index,
url,
accuracy: (100 - index / results?.totalHits).toFixed(0),
topResult: index <= results?.topResults?.length,
};
logSearch(data);
},
[
router.asPath,
results?.totalHits,
results?.topResults,
debouncedSearchTerm,
activeTags,
]
);
useEffect(() => {
if (debouncedSearchTerm && open) {
setLoading(true);
fetch(
`/api/search/v1?q=${encodeURIComponent(debouncedSearchTerm)}${
activeTags.length > 0 ? `&doc=${activeTags.join(",")}` : ""
}`
)
.then((x) => x.json())
.then((res) => {
setResults(res);
logSearchAttempt(res?.hits?.totalHits ?? 0, "standard");
setLoading(false);
})
.catch(() => {
setResults(null);
setLoading(false);
});
window.scrollTo(0, 0);
} else {
setLoading(false);
setResults(null);
}
}, [debouncedSearchTerm, activeTags, open, logSearchAttempt]);
useEffect(() => {
if (debouncedSearchTerm && open && !firstSearch.current) {
session.current = {
queries: [...session.current.queries, prevSearch.current],
retires: session.current.retires + 1,
};
prevSearch.current = debouncedSearchTerm;
} else if (debouncedSearchTerm && open) {
firstSearch.current = false;
prevSearch.current = debouncedSearchTerm;
}
}, [debouncedSearchTerm, activeTags, open]);
useEffect(() => {
const listener = (event: KeyboardEvent) => {
if (
event.key === "b" &&
(event.metaKey || event.ctrlKey) &&
!event.shiftKey
) {
event.preventDefault();
if (open) {
inputRef.current?.focus();
} else {
setOpen(true);
}
}
};
document.addEventListener("keydown", listener);
return () => document.removeEventListener("keydown", listener);
}, [open]);
const handleClose = useCallback(() => {
if (session.current.retires > 0 || prevSearch.current !== "") {
logSearchAttempt(results?.totalHits, "feilet");
}
setOpen(false);
setQuery("");
setTags([]);
session.current = { queries: [], retires: 0 };
firstSearch.current = true;
}, [logSearchAttempt, results?.totalHits]);
useEffect(() => {
router.events.on("beforeHistoryChange", handleClose);
router.events.on("hashChangeComplete", handleClose);
return () => {
router.events.off("beforeHistoryChange", handleClose);
router.events.off("hashChangeComplete", handleClose);
};
}, [handleClose, router.events]);
const handleQueryChange = (v: string) => {
setQuery(v);
v !== debouncedSearchTerm && setLoading(!!v);
};
const noHits = (key: string) => {
return !Object.hasOwn(results?.groupedHits ?? {}, key);
};
const noHitsAndQuery = (key: string) => {
return debouncedSearchTerm.length > 0 && !activeTags.find((x) => x === key);
};
return (
<div className="z-[1050] mr-4 ml-auto flex justify-center lg:mr-0 lg:ml-0">
<Button
variant="primary"
className="hover:bg-deepblue-700 bg-deepblue-600 focus-visible:shadow-focus-gap h-11 focus:shadow-none"
aria-keyshortcuts="Control+b"
icon={
<MagnifyingGlassIcon
className="pointer-events-none -mt-[1px] shrink-0 text-2xl"
aria-label="Åpne meny"
aria-hidden
/>
}
iconPosition="left"
onClick={() => setOpen(true)}
>
Søk
</Button>
<ReactModal
isOpen={open}
onRequestClose={() => handleClose()}
aria={{ modal: true }}
contentLabel="Søk"
className="bg-surface-default absolute inset-0 block w-screen overflow-x-auto px-4 md:px-6"
overlayClassName={styles.modalOverlaySearch}
>
<div className="search-grid-wrapper relative mx-auto max-w-4xl gap-4 gap-x-8 py-24">
<button
className="focus-visible:shadow-focus hover:bg-surface-neutral-subtle-hover absolute top-8 right-4 flex items-center justify-center rounded py-3 px-2 text-lg focus:outline-none"
onClick={() => handleClose()}
>
Lukk søk <KBD>ESC</KBD>
</button>
<div className="search-grid-filter mt-8">
<CheckboxGroup legend="Filter" onChange={setTags}>
{Object.entries(searchOptions)
.filter((x) => !x[1].hidden)
.map(([key, val]) => (
<Checkbox
disabled={
noHitsAndQuery(key) &&
noHits(key) &&
results?.hits[key] === 0
}
key={key}
value={key}
className="whitespace-nowrap"
>
{`${val.display} ${
results?.hits[key] > 0 ? `(${results?.hits[key]})` : ""
}`}
</Checkbox>
))}
</CheckboxGroup>
</div>
<div className="search-grid-input w-full">
<form role="search" onSubmit={(e) => e.preventDefault()}>
<Search
label={
<span className="flex items-center">
Søk i hele Aksel{" "}
{os === "mac" ? <KBD>CMD + B</KBD> : <KBD>CTRL + B</KBD>}
</span>
}
variant="simple"
value={query}
hideLabel={false}
onChange={(v) => handleQueryChange(v)}
onClear={() => setQuery("")}
ref={inputRef}
autoComplete="off"
autoCorrect="off"
autoCapitalize="off"
spellCheck={false}
autoFocus
id="aksel-search-input"
clearButton={false}
/>
</form>
</div>
<div
className="search-grid-results mt-8 w-full max-w-3xl"
aria-busy={loading}
role="status"
>
{loading && (
<div className="flex w-full justify-center p-4">
<Loader size="xlarge" variant="neutral" />
</div>
)}
{results && query && !loading && (
<div id="aksel-search-results" aria-label="Søkeresultater">
<p className="text-xl font-semibold">
{`${results?.totalHits} treff på "${query}"${
activeTags.length > 0
? ` i ${activeTags
.map((x) => searchOptions[x].display.toLowerCase())
.join(", ")}`
: ""
}`}
</p>
<div className="mt-4 pb-16 md:block">
{results?.topResults.length > 0 && (
<GroupComponent
startIndex={1}
heading={
<span className="flex items-center gap-2">
Beste treff
<ChangeLogIconOutline className="shrink-0" />
</span>
}
logSuccess={logSuccessSearchAttempt}
hits={results?.topResults}
query={query}
/>
)}
<Group
startIndex={
results?.topResults.length > 0
? results?.topResults.length + 1
: 1
}
logSuccess={logSuccessSearchAttempt}
groups={results?.groupedHits}
query={debouncedSearchTerm}
/>
</div>
</div>
)}
</div>
</div>
<style jsx>{`
.search-grid-wrapper {
display: grid;
grid-template-columns: 12rem auto;
grid-template-areas:
"input input"
"results results";
}
.search-grid-wrapper .search-grid-filter {
display: none;
}
.search-grid-wrapper .search-grid-input {
grid-area: input;
}
.search-grid-wrapper .search-grid-results {
grid-area: results;
}
@media (min-width: 768px) {
.search-grid-wrapper {
display: grid;
grid-template-columns: 12rem auto;
grid-template-areas:
"empty input"
"filter results";
}
.search-grid-wrapper .search-grid-filter {
display: block;
grid-area: filter;
}
}
`}</style>
</ReactModal>
</div>
);
};
function KBD({ children }: { children: React.ReactNode }) {
return (
<Detail
as="kbd"
className="bg-surface-neutral-subtle-hover ml-2 hidden rounded px-2 font-sans font-semibold uppercase md:inline-block"
>
{children}
</Detail>
);
}
export default GlobalSearch;
@@ -0,0 +1,50 @@
import { logSearch } from "@/utils";
import { createContext, useCallback, useContext } from "react";
import { useSearch } from "../hooks";
import { SearchContext } from "./SearchProvider";
type SearchLoggingContextType = {
logSuccess: (index: number, url: string, tag?: string) => void;
};
export const SearchLoggingContext = createContext<SearchLoggingContextType>({
logSuccess: () => null,
});
export const SearchLoggingProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const context = useSearch();
const { query, tags } = useContext(SearchContext);
const logSuccess = useCallback(
(index: number, url: string, tag?: string) => {
const data = {
type: "suksess",
searchedFromUrl: window.location.pathname,
query,
filter: tags,
tag,
index,
url,
accuracy: (100 - index / context?.results?.totalHits).toFixed(0),
topResult: index <= context?.results?.topResults?.length,
};
logSearch(data);
},
[
query,
tags,
context?.results?.totalHits,
context?.results?.topResults?.length,
]
);
return (
<SearchLoggingContext.Provider value={{ logSuccess }}>
{children}
</SearchLoggingContext.Provider>
);
};
@@ -0,0 +1,52 @@
import { createContext, useCallback, useContext, useEffect } from "react";
import { SearchContext } from "./SearchProvider";
import { SearchResultContext } from "./SearchResultProvider";
import { useRouter } from "next/router";
type SearchNavigationProviderT = { close: () => void };
export const SearchNavigationContext = createContext<SearchNavigationProviderT>(
{
close: () => null,
}
);
export const SearchNavigationProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const router = useRouter();
const { setOpen, setQuery, setTags } = useContext(SearchContext);
const { reset } = useContext(SearchResultContext);
const close = useCallback(() => {
setOpen(false);
reset();
setQuery("");
setTags([]);
}, [reset, setOpen, setQuery, setTags]);
/* Add a small delay to get a precieved smoother navigation */
useEffect(() => {
let timeout: NodeJS.Timeout;
const handler = () => {
timeout && clearTimeout(timeout);
setTimeout(() => close(), 100);
};
router.events.on("beforeHistoryChange", handler);
router.events.on("hashChangeComplete", handler);
return () => {
router.events.off("beforeHistoryChange", handler);
router.events.off("hashChangeComplete", handler);
};
}, [close, router.events]);
return (
<SearchNavigationContext.Provider value={{ close }}>
{children}
</SearchNavigationContext.Provider>
);
};
@@ -0,0 +1,63 @@
import { searchOptions } from "@/types";
import {
Dispatch,
SetStateAction,
createContext,
useEffect,
useRef,
useState,
} from "react";
import { useShortcut } from "../hooks";
type SearchContextType = {
os: "mac" | "windows";
open: boolean;
setOpen: (v) => void;
tags: Array<keyof typeof searchOptions>;
setTags: Dispatch<SetStateAction<Array<keyof typeof searchOptions>>>;
query: string;
setQuery: (v: string) => void;
};
export const SearchContext = createContext<SearchContextType>({
os: "windows",
open: false,
setOpen: () => null,
tags: [],
setTags: () => null,
query: "",
setQuery: () => null,
});
export const SearchProvider = ({ children }: { children: React.ReactNode }) => {
const [open, setOpen] = useState(false);
const [tags, setTags] = useState<SearchContextType["tags"]>([]);
const [os, setOs] = useState<SearchContextType["os"]>("windows");
const inputRef = useRef(null);
const [query, setQuery] = useState("");
useShortcut(open, setOpen, inputRef);
useEffect(() => {
navigator.userAgent?.indexOf("Mac OS X") !== -1
? setOs("mac")
: setOs("windows");
}, []);
return (
<SearchContext.Provider
value={{
os,
open,
setOpen,
tags,
setTags,
query,
setQuery,
}}
>
{children}
</SearchContext.Provider>
);
};
@@ -0,0 +1,53 @@
import { allArticleDocuments } from "@/sanity/config";
import { SearchHitT, SearchResultsT } from "@/types";
import { createContext, useContext, useMemo } from "react";
import { useSearch } from "../hooks";
import { formatRawResults } from "../utils";
import { SearchContext } from "./SearchProvider";
type SearchResultContextType = {
results: SearchResultsT;
update: (value: string, tags: string[]) => void;
error: any;
isValidating: boolean;
reset: () => void;
mostResent: Omit<SearchHitT, "score" | "anchor" | "matches">[];
};
export const SearchResultContext = createContext<SearchResultContextType>({
results: null,
update: () => null,
error: false,
isValidating: false,
reset: () => null,
mostResent: [],
});
export const SearchResultProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const context = useSearch();
const { tags } = useContext(SearchContext);
const mostResent: Omit<SearchHitT, "score" | "anchor">[] = useMemo(() => {
if (!context.rawData) {
return null;
}
return formatRawResults(
context.rawData
.filter((x) =>
(tags.length > 0 ? tags : allArticleDocuments).includes(x._type)
)
.slice(0, 20)
);
}, [context.rawData, tags]);
return (
<SearchResultContext.Provider value={{ ...context, mostResent }}>
{children}
</SearchResultContext.Provider>
);
};
@@ -0,0 +1,4 @@
export * from "./SearchProvider";
export * from "./SearchResultProvider";
export * from "./SearchLoggingProvider";
export * from "./SearchNavigationProvider";
@@ -0,0 +1,36 @@
import { FuseItemT, SearchHitT } from "@/types";
import { omit } from "@navikt/ds-react";
import Fuse from "fuse.js";
export function formatResults(res: Fuse.FuseResult<FuseItemT>[]): SearchHitT[] {
return res.map((x) => {
return {
...x,
item: omit(x.item, ["intro", "ingress"]) as Omit<
FuseItemT,
"ingress" | "intro"
>,
anchor: resolveAnchor(x.matches[0], x.item),
description: x?.item.intro || x.item.ingress || "",
};
});
}
export function formatRawResults(
res: FuseItemT[]
): Omit<SearchHitT, "score" | "anchor">[] {
return res.map((x) => ({
item: omit(x, ["intro", "ingress"]) as Omit<FuseItemT, "ingress" | "intro">,
description: x?.intro || x.ingress || "",
}));
}
function resolveAnchor(match: Fuse.FuseResultMatch, item: FuseItemT) {
if (match.key.includes("lvl")) {
return item[match.key.split(".")[0]][match.refIndex].id;
}
if (match.key === "content.text") {
return item[match.key.split(".")[0]][match.refIndex].id;
}
return null;
}
@@ -0,0 +1,25 @@
import { FuseItemT } from "@/types";
import Fuse from "fuse.js";
export function fuseSearch(results: any[], query: string) {
/* https://fusejs.io/api/options.html */
const fuse = new Fuse<FuseItemT>(results, {
keys: [
{ name: "heading", weight: 100 },
{ name: "lvl2.text", weight: 50 },
{ name: "lvl3.text", weight: 40 },
{ name: "lvl4.text", weight: 30 },
{ name: "ingress", weight: 30 },
{ name: "intro", weight: 30 },
{ name: "tema", weight: 70 },
{ name: "content.text", weight: 20 },
],
includeScore: true,
shouldSort: true,
minMatchCharLength: 3,
includeMatches: true,
threshold: 0.3,
location: 120,
});
return fuse.search(query).filter((x) => x.score < 0.3);
}
@@ -0,0 +1,42 @@
import { FuseItemT, GroupedHitsT, SearchHitT, SearchResultsT } from "@/types";
import Fuse from "fuse.js";
export function createSearchResult(
result: SearchHitT[],
rawResults: Fuse.FuseResult<FuseItemT>[]
) {
const groupedHits: GroupedHitsT = result?.reduce((prev, cur) => {
if (cur.item._type in prev) {
return { ...prev, [cur.item._type]: [...prev[cur.item._type], cur] };
} else {
return { ...prev, [cur.item._type]: [cur] };
}
}, {});
const response: SearchResultsT = {
groupedHits,
topResults:
result?.length > 4 ? result.slice(0, 4).filter((x) => x.score < 0.1) : [],
totalHits: result?.length ?? 0,
hits: {
komponent_artikkel: rawResults.filter(
(x: any) => x.item._type === "komponent_artikkel"
).length,
aksel_artikkel: rawResults.filter(
(x: any) => x.item._type === "aksel_artikkel"
).length,
ds_artikkel: rawResults.filter((x: any) => x.item._type === "ds_artikkel")
.length,
aksel_blogg: rawResults.filter((x: any) => x.item._type === "aksel_blogg")
.length,
aksel_prinsipp: rawResults.filter(
(x: any) => x.item._type === "aksel_prinsipp"
).length,
aksel_standalone: rawResults.filter(
(x: any) => x.item._type === "aksel_prinsipp"
).length,
},
};
return response;
}
@@ -0,0 +1,93 @@
import React from "react";
import cl from "clsx";
import Fuse from "fuse.js";
import { searchOptions } from "@/types";
function splitStr(str: string, query: string) {
const regexStr = query.toLowerCase().split(" ").join("|");
return str.split(new RegExp(`(${regexStr})`, "gi"));
}
export function highlightStr(
str: string,
query: string,
tag: keyof typeof searchOptions
) {
if (!query) {
return str;
}
const getClass = (part: string) => {
if (
!query
.split(" ")
.map((x) => x.toLowerCase())
.includes(part.toLowerCase())
) {
return undefined;
}
return cl(
"text-text-default group-focus-visible:text-text-on-action group-focus-visible:bg-transparent",
{
"bg-gray-200": !tag,
"bg-teal-100": tag === "aksel_artikkel",
"bg-deepblue-100": ["komponent_artikkel", "ds_artikkel"].includes(tag),
"bg-violet-100": ["aksel_prinsipp", "aksel_standalone"].includes(tag),
"bg-pink-100": tag === "aksel_blogg",
}
);
};
return (
<>
{splitStr(str, query)
.filter((x) => !!x)
.map((part, i) => (
<span key={i} className={getClass(part)}>
{part}
</span>
))}
</>
);
}
export function highlightMatches(hitMatch: Fuse.FuseResultMatch) {
const text = hitMatch.value;
const result = [];
const matches = [...hitMatch.indices];
let index = 0;
matches.forEach((match, i) => {
const str = text.substring(match[0], match[1] - 1);
let preText = text.substring(index, match[0]);
if (i === 0) {
const preLength = preText.length;
preText = preText.substring(preText.length - 23);
if (preText.length < preLength) {
preText = `...${preText}`;
}
}
result.push({ text: preText, highlight: false });
result.push({ text: str, highlight: true });
index = match[1] - 1;
if (i === matches.length - 1) {
result.push({ text: text.substring(index), highlight: false });
}
});
return (
<span>
{result.map((part, i) => (
<span
key={i}
className={cl({
"text-text-default font-semibold": part.highlight,
})}
>
{part.text}
</span>
))}
</span>
);
}
@@ -0,0 +1,4 @@
export * from "./format-result";
export * from "./fuse-search";
export * from "./group-results";
export * from "./highlighter";
@@ -1,12 +1,12 @@
import { AkselSidebarT } from "@/types";
import { logNav } from "@/utils";
import { ChevronDownIcon } from "@navikt/aksel-icons";
import { BodyShort, Detail, Tag } from "@navikt/ds-react";
import { Detail, Tag } from "@navikt/ds-react";
import cl from "clsx";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { useSection } from "./use-sections";
import { useSection } from "./useSection";
const NavItem = ({
link,
@@ -17,61 +17,52 @@ const NavItem = ({
}) => {
const isActive = asPath === `/${link.slug}`;
return (
<li
className={cl(
"focus-within:shadow-focus peer relative rounded-sm pl-2 before:absolute before:left-0 before:z-[-1]",
{
"before:border-l-border-action-selected before:top-1/2 before:h-6 before:-translate-y-1/2 before:rounded-full before:border-l-[4px]":
isActive,
"before:h-full before:rounded-full before:border-l before:border-l-gray-200 hover:before:top-1/2 hover:before:h-6 hover:before:-translate-y-1/2 hover:before:border-l-2 hover:before:border-l-gray-400":
!isActive,
}
)}
>
<Link href={`/${link.slug}`} passHref legacyBehavior>
<BodyShort
as="a"
size="small"
onClick={(e) => {
logNav(
"meny",
window.location.pathname,
e.currentTarget.getAttribute("href")
);
}}
className={cl(
"hover:text-deepblue-800 relative flex overflow-hidden px-2 py-[6px] no-underline focus:outline-none",
{
"text-deepblue-800 font-semibold": isActive,
"text-text-subtle": !isActive,
}
)}
>
{link.heading}
<span className="ml-auto capitalize">
{link.tag && ["deprecated", "beta", "new"].includes(link.tag) && (
<Tag
size="xsmall"
variant={
link.tag === "beta"
? "alt1"
: link.tag === "deprecated"
? "neutral"
: "info"
}
className={cl({
"border-violet-300 bg-violet-50": link.tag === "beta",
})}
>
{link.tag === "new"
? "ny"
<li>
<Link
href={`/${link.slug}`}
onClick={(e) => {
logNav(
"meny",
window.location.pathname,
e.currentTarget.getAttribute("href")
);
}}
className={cl(
"hover:text-deepblue-800 focus-visible:shadow-focus text-medium relative flex overflow-hidden rounded-sm py-1 pl-4 pr-2 leading-snug before:rounded-full hover:before:transition-colors focus:outline-none",
{
"before:border-l-border-action-selected before:absolute before:left-0 before:top-1/2 before:h-6 before:-translate-y-1/2 before:border-l-[4px]":
isActive,
"text-deepblue-800 font-semibold": isActive,
"text-text-subtle": !isActive,
"before:absolute before:left-0 before:h-full before:border-l before:border-l-gray-200 hover:before:top-1/2 hover:before:h-6 hover:before:-translate-y-1/2 hover:before:border-l-2 hover:before:border-l-gray-400":
!isActive,
}
)}
>
{link.heading}
<span className="ml-auto capitalize">
{link.tag && ["deprecated", "beta", "new"].includes(link.tag) && (
<Tag
size="xsmall"
variant={
link.tag === "beta"
? "alt1"
: link.tag === "deprecated"
? "Avviklet"
: link.tag}
</Tag>
)}
</span>
</BodyShort>
? "neutral"
: "info"
}
className={cl({
"border-violet-300 bg-violet-50": link.tag === "beta",
})}
>
{link.tag === "new"
? "ny"
: link.tag === "deprecated"
? "Avviklet"
: link.tag}
</Tag>
)}
</span>
</Link>
</li>
);
@@ -88,10 +79,10 @@ const Dropdown = ({
const { asPath } = useRouter();
return (
<li className="sidebar-dropdown w-full" data-open={open}>
<li className="peer w-full peer-data-[open=true]:mt-6" data-open={open}>
<button
onClick={() => setOpen((x) => !x)}
className="min-h-8 text-text-subtle hover:text-deepblue-800 group z-10 flex w-full cursor-pointer items-center justify-between pr-2 focus:outline-none"
className="min-h-8 text-text-subtle hover:text-deepblue-800 group z-10 flex w-full cursor-pointer items-center justify-between focus:outline-none"
aria-expanded={open}
>
<Detail
@@ -109,16 +100,11 @@ const Dropdown = ({
</Detail>
</button>
<ul hidden={!open} className="px-2">
<ul hidden={!open} className="pl-2">
{links.map((z) => (
<NavItem link={z} key={z?.heading} asPath={asPath} />
))}
</ul>
<style jsx>{`
.sidebar-dropdown[data-open="true"] + .sidebar-dropdown {
margin-top: var(--a-spacing-6);
}
`}</style>
</li>
);
};
@@ -135,10 +121,10 @@ export const Sidebar = ({
return (
<div
data-testid="ds-sidebar"
className="toc-ignore w-sidebar bg-surface-default z-[1002] hidden shrink-0 self-start pb-4 md:block"
className="toc-ignore w-sidebar bg-surface-default z-[1002] hidden shrink-0 self-start overflow-x-auto pb-8 pl-6 pr-2 md:block"
>
<nav aria-label={kategori} className={cl("overflow-x-auto")}>
<ul className="pb-4 pl-6">
<nav aria-label={kategori}>
<ul>
{sections.map((kat) => (
<Dropdown links={kat.pages} title={kat.title} key={kat.title} />
))}
@@ -3,37 +3,36 @@ import { withErrorBoundary } from "@/error-boundary";
import { urlFor } from "@/sanity/interface";
import { DoDontT } from "@/types";
import {
CheckmarkCircleFillIcon,
ExclamationmarkTriangleFillIcon,
XMarkOctagonFillIcon,
CheckmarkIcon,
ExclamationmarkIcon,
XMarkIcon,
} from "@navikt/aksel-icons";
import { BodyShort } from "@navikt/ds-react";
import cl from "clsx";
import React from "react";
const GetIcon = (s: string) => {
switch (s) {
case "do":
return (
<CheckmarkCircleFillIcon
<CheckmarkIcon
aria-hidden
fontSize="1.5rem"
fontSize="2rem"
className="flex-shrink-0 text-green-500"
/>
);
case "dont":
return (
<XMarkOctagonFillIcon
<XMarkIcon
aria-hidden
fontSize="1.5rem"
fontSize="2rem"
className="flex-shrink-0 text-red-500"
/>
);
case "warning":
return (
<ExclamationmarkTriangleFillIcon
<ExclamationmarkIcon
aria-hidden
fontSize="1.5rem"
fontSize="2rem"
className="flex-shrink-0 text-orange-500"
/>
);
@@ -42,42 +41,56 @@ const GetIcon = (s: string) => {
}
};
const getText = (s: string) => {
switch (s) {
case "do":
return "Do: ";
case "dont":
return "Don't: ";
case "warning":
return "Fare på ferde: ";
default:
return "";
}
};
const Element = ({ block }: { block: DoDontT["blokker"][number] }) => {
if (!block.picture) return null;
return (
<figure
className={cl("sm:min-w-80 flex min-w-full flex-1 flex-col rounded-t", {
className={cl("sm:min-w-80 flex min-w-full flex-1 flex-col", {
"basis-full": block?.fullwidth,
"max-w-sm": !block?.fullwidth,
})}
>
<img
className="ring-border-subtle rounded-t bg-gray-50 ring-1"
alt={block.alt}
loading="lazy"
decoding="async"
src={urlFor(block.picture).auto("format").url()}
/>
<div
className={cl(
"z-10 -ml-[1px] w-[calc(100%_+_2px)] rounded-b border-t-8",
"relative z-10 -ml-[1px] w-[calc(100%_+_2px)] border-t-4",
{
"border-t-border-success": block.variant === "do",
"border-t-border-danger": block.variant === "dont",
"border-t-surface-warning": block.variant === "warning",
}
)}
aria-hidden
>
<span className="absolute right-[10px] z-10 translate-y-2">
{GetIcon(block.variant)}
</span>
</div>
<img
className="ring-border-subtle bg-gray-50 ring-1"
alt={block.alt}
loading="lazy"
decoding="async"
src={urlFor(block.picture).auto("format").url()}
/>
<figcaption data-variant={block.variant}>
<div className="mt-3">
{block.description && (
<BodyShort
size="small"
as="span"
className="inline-flex items-center gap-2"
>
{GetIcon(block.variant)}
{block.description}
<BodyShort size="small" as="span">
<span className="font-semibold">{getText(block.variant)}</span>
<span>{block.description}</span>
</BodyShort>
)}
</div>
Loaded 100 of 224 files, more files were not shown because too many files have changed in this diff. Show more