mirror of
https://github.com/navikt/aksel.git
synced 2026-10-13 09:12:03 +00:00
Compare commits
82
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
874961ff7e | ||
|
|
df48236fc8 | ||
|
|
65cc6f425d | ||
|
|
2d34691450 | ||
|
|
094f2b1c2b | ||
|
|
fb1606d771 | ||
|
|
ab62211743 | ||
|
|
c3d9735ea7 | ||
|
|
1f16bfc866 | ||
|
|
b218be2908 | ||
|
|
190b9b7dba | ||
|
|
cfd0b37906 | ||
|
|
9ed49b8a20 | ||
|
|
39ff2c1809 | ||
|
|
4fcc83381d | ||
|
|
8c855ee0da | ||
|
|
ade35e9fba | ||
|
|
b9d2a65c3f | ||
|
|
5cd1a6d86a | ||
|
|
1bdebfd134 | ||
|
|
138c4523ee | ||
|
|
363d51e2c4 | ||
|
|
1079e8465a | ||
|
|
8dda59082d | ||
|
|
21d5ddd433 | ||
|
|
1b7bc53294 | ||
|
|
4dc5a90c72 | ||
|
|
c7694f8ddd | ||
|
|
10bbdb730b | ||
|
|
f4458f3bb8 | ||
|
|
9dafbeb4eb | ||
|
|
4b4e190c20 | ||
|
|
b7d03bfc89 | ||
|
|
bc259f5b30 | ||
|
|
16cad26378 | ||
|
|
6ee4aa3e82 | ||
|
|
bb5466178a | ||
|
|
efb92be344 | ||
|
|
e3f75f542f | ||
|
|
96610bb543 | ||
|
|
eee7143cf6 | ||
|
|
a1b8cc0364 | ||
|
|
e5d2f7eb8c | ||
|
|
67b8b6626e | ||
|
|
8783f1821d | ||
|
|
d52085f256 | ||
|
|
67e8565da3 | ||
|
|
bb4ff1afc3 | ||
|
|
16c0c15c67 | ||
|
|
f506dca7e1 | ||
|
|
a44b2e2e7c | ||
|
|
d96d368e7b | ||
|
|
d3c0052c33 | ||
|
|
f848bea71a | ||
|
|
1585106c55 | ||
|
|
5ed06e19fe | ||
|
|
c0f334061b | ||
|
|
79bddd9caa | ||
|
|
4a68409898 | ||
|
|
786bff67ab | ||
|
|
8a17caea6c | ||
|
|
3f43d0bf3b | ||
|
|
26541984ac | ||
|
|
10303602aa | ||
|
|
42254543d8 | ||
|
|
81c84e4817 | ||
|
|
b441175028 | ||
|
|
75362a0dc1 | ||
|
|
d8732ffa0e | ||
|
|
0ea9155085 | ||
|
|
92b9687ed1 | ||
|
|
1c022b14c9 | ||
|
|
abdeb616d2 | ||
|
|
37165e9f38 | ||
|
|
d19920ca78 | ||
|
|
60d3481300 | ||
|
|
f486937439 | ||
|
|
c62058abc1 | ||
|
|
84d9b8260f | ||
|
|
c108063dcb | ||
|
|
2a8533ad56 | ||
|
|
9b990a2178 |
No files matched your search
@@ -20,7 +20,11 @@
|
||||
"access": "public",
|
||||
"baseBranch": "main",
|
||||
"updateInternalDependencies": "patch",
|
||||
"ignore": ["aksel.nav.no", "aksel-icons-figma-plugin"],
|
||||
"ignore": [
|
||||
"aksel.nav.no",
|
||||
"aksel-icons-figma-plugin",
|
||||
"eslint-plugin-aksel-local"
|
||||
],
|
||||
|
||||
"___experimentalUnsafeOptions_WILL_CHANGE_IN_PATCH": {
|
||||
"updateInternalDependents": "always"
|
||||
|
||||
@@ -47,6 +47,7 @@ module.exports = {
|
||||
files: ["**/*.test.*", "**/__tests__/*"],
|
||||
extends: ["plugin:jest/recommended", "plugin:testing-library/react"],
|
||||
},
|
||||
|
||||
{
|
||||
files: ["aksel.nav.no/website/**"],
|
||||
env: {
|
||||
@@ -78,6 +79,16 @@ module.exports = {
|
||||
"jsx-a11y/anchor-is-valid": "off",
|
||||
},
|
||||
},
|
||||
{
|
||||
files: [
|
||||
"aksel.nav.no/website/pages/eksempler/**/*.tsx",
|
||||
"aksel.nav.no/website/pages/templates/**/*.tsx",
|
||||
],
|
||||
plugins: ["aksel-local"],
|
||||
rules: {
|
||||
"aksel-local/comment-check": ["error"],
|
||||
},
|
||||
},
|
||||
],
|
||||
globals: {
|
||||
Locale: "readonly",
|
||||
|
||||
@@ -12,7 +12,7 @@ body:
|
||||
attributes:
|
||||
label: Lenke til side
|
||||
description: Lenke til siden hvor eksemplet er
|
||||
placeholder:
|
||||
placeholder:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
|
||||
@@ -2,8 +2,6 @@ name: Ny komponent
|
||||
description: Send et ønske om en eller flere nye komponenter
|
||||
title: "[Ny komponent]: "
|
||||
labels: ["nytt ✨", "forespørsel 🥰", "komponenter 🧩"]
|
||||
assignees:
|
||||
- KenAJoh
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -2,8 +2,6 @@ name: Nytt ikon
|
||||
description: Send et ønske om et eller flere nye ikoner
|
||||
title: "[Nytt ikon]: "
|
||||
labels: ["nytt ✨", "ikoner 🖼", "forespørsel 🥰"]
|
||||
assignees:
|
||||
- KenAJoh
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -2,8 +2,6 @@ name: Innspill til komponent
|
||||
description: Send inn innspill til komponent
|
||||
title: "[Innspill til komponent]: "
|
||||
labels: ["forespørsel 🥰", "komponenter 🧩", "nytt ✨", "beta 🧪"]
|
||||
assignees:
|
||||
- KenAJoh
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -2,8 +2,6 @@ name: Innspill til ikon
|
||||
description: Send inn innspill til ikon
|
||||
title: "[Tilbakemelding på ikon]: "
|
||||
labels: ["forespørsel 🥰", "ikoner 🖼️"]
|
||||
assignees:
|
||||
- KenAJoh
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
|
||||
@@ -53,6 +53,8 @@ jobs:
|
||||
projectToken: x3xqdfgkujg
|
||||
exitZeroOnChanges: false
|
||||
buildScriptName: "build:storybook"
|
||||
onlyChanged: true
|
||||
externals: \@navikt/core/css/**/*.css
|
||||
env:
|
||||
STORYBOOK_GITHUB_SHA: ${{ github.sha }}
|
||||
|
||||
|
||||
+2
-1
@@ -151,4 +151,5 @@ playwright-report/
|
||||
backup-*.tar.gz
|
||||
sitemap-urls.json
|
||||
*.tar.gz
|
||||
searchindex.json
|
||||
searchindex.json
|
||||
aksel.nav.no/website/public/rss
|
||||
@@ -1,3 +1,6 @@
|
||||
import { mergeConfig } from "vite";
|
||||
import turbosnap from "vite-plugin-turbosnap";
|
||||
|
||||
module.exports = {
|
||||
staticDirs: ["./public"],
|
||||
|
||||
@@ -42,4 +45,17 @@ module.exports = {
|
||||
checkOptions: {},
|
||||
reactDocgen: false,
|
||||
},
|
||||
|
||||
async viteFinal(config, { configType }) {
|
||||
return mergeConfig(config, {
|
||||
plugins:
|
||||
configType === "PRODUCTION"
|
||||
? [
|
||||
turbosnap({
|
||||
rootDir: config.root ?? process.cwd(),
|
||||
}),
|
||||
]
|
||||
: [],
|
||||
});
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,29 @@
|
||||
# @navikt/aksel-icons
|
||||
|
||||
## 5.11.4
|
||||
|
||||
## 5.11.3
|
||||
|
||||
## 5.11.2
|
||||
|
||||
## 5.11.1
|
||||
|
||||
## 5.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Ikoner: Nytt ikon `PushPin` ([#2503](https://github.com/navikt/aksel/pull/2503))
|
||||
|
||||
## 5.10.4
|
||||
|
||||
## 5.10.3
|
||||
|
||||
## 5.10.2
|
||||
|
||||
## 5.10.1
|
||||
|
||||
## 5.10.0
|
||||
|
||||
## 5.9.2
|
||||
|
||||
## 5.9.1
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.3117 3.25101C15.5796 2.51892 14.3754 2.57898 13.7198 3.38029L10.1358 7.76072C10.1008 7.80356 10.0525 7.83356 9.99854 7.84601L5.5894 8.8635C4.25008 9.17258 3.77354 10.8342 4.74547 11.8061L7.93934 15L3.46967 19.4697C3.17678 19.7626 3.17678 20.2374 3.46967 20.5303C3.76256 20.8232 4.23744 20.8232 4.53033 20.5303L9 16.0607L12.1939 19.2545C13.1658 20.2265 14.8274 19.7499 15.1365 18.4106L16.154 14.0015C16.1664 13.9475 16.1964 13.8992 16.2393 13.8642L20.6197 10.2802C21.421 9.62457 21.4811 8.42042 20.749 7.68833L16.3117 3.25101ZM9.53033 14.4697L13.2545 18.1939C13.3934 18.3327 13.6308 18.2646 13.6749 18.0733L14.6924 13.6642C14.7795 13.2866 14.9895 12.9486 15.2894 12.7032L19.6699 9.11925C19.7843 9.02559 19.7929 8.85357 19.6883 8.74899L15.251 4.31167C15.1464 4.20709 14.9744 4.21567 14.8807 4.33014L11.2968 8.71058C11.0514 9.01048 10.7134 9.22046 10.3358 9.3076L5.92669 10.3251C5.73536 10.3692 5.66728 10.6066 5.80613 10.7455L9.53033 14.4697Z" fill="#23262A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,9 @@
|
||||
name: PushPin
|
||||
category: Home
|
||||
sub_category: Hosehold items
|
||||
keywords:
|
||||
- pinn
|
||||
- tegnestift
|
||||
variant: Stroke
|
||||
updated_at: 23.11.2023
|
||||
created_at: 23.11.2023
|
||||
@@ -0,0 +1,3 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.3117 3.25101C15.5796 2.51892 14.3754 2.57898 13.7198 3.38029L10.1358 7.76072C10.1008 7.80356 10.0525 7.83356 9.99856 7.84601L5.58942 8.8635C4.2501 9.17258 3.77356 10.8342 4.74548 11.8061L12.1939 19.2545C13.1658 20.2265 14.8274 19.7499 15.1365 18.4106L16.154 14.0015C16.1665 13.9475 16.1965 13.8992 16.2393 13.8642L20.6197 10.2802C21.421 9.62457 21.4811 8.42042 20.749 7.68833L16.3117 3.25101ZM4.53033 20.5303L8.06066 17L7 15.9393L3.46967 19.4697C3.17678 19.7626 3.17678 20.2374 3.46967 20.5303C3.76256 20.8232 4.23744 20.8232 4.53033 20.5303Z" fill="#23262A"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 718 B |
@@ -0,0 +1,9 @@
|
||||
name: PushPinFill
|
||||
category: Home
|
||||
sub_category: Hosehold items
|
||||
keywords:
|
||||
- pinn
|
||||
- tegnestift
|
||||
variant: Fill
|
||||
updated_at: 23.11.2023
|
||||
created_at: 23.11.2023
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-icons",
|
||||
"version": "5.9.2",
|
||||
"version": "5.11.4",
|
||||
"author": "Aksel | NAV",
|
||||
"homepage": "https://aksel.nav.no/ikoner",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# @navikt/aksel-stylelint
|
||||
|
||||
## 5.11.4
|
||||
|
||||
## 5.11.3
|
||||
|
||||
## 5.11.2
|
||||
|
||||
## 5.11.1
|
||||
|
||||
## 5.11.0
|
||||
|
||||
## 5.10.4
|
||||
|
||||
## 5.10.3
|
||||
|
||||
## 5.10.2
|
||||
|
||||
## 5.10.1
|
||||
|
||||
## 5.10.0
|
||||
|
||||
## 5.9.2
|
||||
|
||||
## 5.9.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-stylelint",
|
||||
"version": "5.9.2",
|
||||
"version": "5.11.4",
|
||||
"author": "Aksel | NAV",
|
||||
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
|
||||
"repository": {
|
||||
@@ -35,8 +35,8 @@
|
||||
"dev": "yarn watch:lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-css": "^5.9.2",
|
||||
"@navikt/ds-tokens": "^5.9.2",
|
||||
"@navikt/ds-css": "^5.11.4",
|
||||
"@navikt/ds-tokens": "^5.11.4",
|
||||
"@types/jest": "^29.0.0",
|
||||
"concurrently": "7.2.1",
|
||||
"copyfiles": "2.4.1",
|
||||
|
||||
@@ -1,5 +1,75 @@
|
||||
# @navikt/aksel
|
||||
|
||||
## 5.11.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`65cc6f425`](https://github.com/navikt/aksel/commit/65cc6f425db5972e5f3145e333292ec7b1776f5a)]:
|
||||
- @navikt/ds-css@5.11.4
|
||||
|
||||
## 5.11.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`8dda59082`](https://github.com/navikt/aksel/commit/8dda59082d9e64a81751805e6be5c30e3fb9cfaa), [`363d51e2c`](https://github.com/navikt/aksel/commit/363d51e2c414f619026084698911a99ac6b5e0bf)]:
|
||||
- @navikt/ds-css@5.11.3
|
||||
|
||||
## 5.11.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-css@5.11.2
|
||||
|
||||
## 5.11.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`10bbdb730`](https://github.com/navikt/aksel/commit/10bbdb730ba79544fa65e1aacebd865be8672a05)]:
|
||||
- @navikt/ds-css@5.11.1
|
||||
|
||||
## 5.11.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`b7d03bfc8`](https://github.com/navikt/aksel/commit/b7d03bfc89dddb679c49a075bd45d675aacc7a71), [`9dafbeb4e`](https://github.com/navikt/aksel/commit/9dafbeb4eb802532cb9c6ad50ab5bf502e1bd5b4), [`16cad2637`](https://github.com/navikt/aksel/commit/16cad263787ad12978c191e2fb45816d2015ef0c)]:
|
||||
- @navikt/ds-css@5.11.0
|
||||
|
||||
## 5.10.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-css@5.10.4
|
||||
|
||||
## 5.10.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`d3c0052c3`](https://github.com/navikt/aksel/commit/d3c0052c3300189798d04f17c2c2892f7c304277)]:
|
||||
- @navikt/ds-css@5.10.3
|
||||
|
||||
## 5.10.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`79bddd9ca`](https://github.com/navikt/aksel/commit/79bddd9caa9467b60e1e0c1c15b16f76dccbc304)]:
|
||||
- @navikt/ds-css@5.10.2
|
||||
|
||||
## 5.10.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-css@5.10.1
|
||||
|
||||
## 5.10.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`60d348130`](https://github.com/navikt/aksel/commit/60d34813009377a1a7d84bed6627c60524849957)]:
|
||||
- @navikt/ds-css@5.10.0
|
||||
|
||||
## 5.9.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel",
|
||||
"version": "5.9.2",
|
||||
"version": "5.11.4",
|
||||
"description": "Aksel command line interface. Handles css-imports, codemods and more",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"license": "MIT",
|
||||
@@ -29,8 +29,8 @@
|
||||
},
|
||||
"homepage": "https://aksel.nav.no/grunnleggende/kode/kommandolinje",
|
||||
"dependencies": {
|
||||
"@navikt/ds-css": "5.9.2",
|
||||
"axios": "1.3.6",
|
||||
"@navikt/ds-css": "5.11.4",
|
||||
"axios": "1.6.0",
|
||||
"chalk": "4.1.0",
|
||||
"clipboardy": "^2.3.0",
|
||||
"commander": "10.0.1",
|
||||
@@ -38,7 +38,7 @@
|
||||
"fast-glob": "3.2.11",
|
||||
"figlet": "1.6.0",
|
||||
"is-git-clean": "1.1.0",
|
||||
"jscodeshift": "0.13.1",
|
||||
"jscodeshift": "^0.15.1",
|
||||
"jscodeshift-add-imports": "1.0.10",
|
||||
"lodash": "4.17.21",
|
||||
"react-scanner": "^1.1.0"
|
||||
|
||||
@@ -1,5 +1,61 @@
|
||||
# @navikt/ds-css
|
||||
|
||||
## 5.11.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- :bug: Modal: Hindre at elementer med sr-only posisjoneres feil ([#2541](https://github.com/navikt/aksel/pull/2541))
|
||||
|
||||
## 5.11.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- :bug: Textarea: Skru av autosize ved manuell resize ([#2518](https://github.com/navikt/aksel/pull/2518))
|
||||
|
||||
- Page: La til `md`-bredde (768px) ([#2510](https://github.com/navikt/aksel/pull/2510))
|
||||
|
||||
## 5.11.2
|
||||
|
||||
## 5.11.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Switch: Byttet success-farger med action npr checked ([#2511](https://github.com/navikt/aksel/pull/2511))
|
||||
|
||||
## 5.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- :sparkles: Textarea: Mulighet for å sette resize-retning ([#2494](https://github.com/navikt/aksel/pull/2494))
|
||||
|
||||
- :sparkles: Textarea: Eksperimentell støtte for automatisk scrollbar ([#2457](https://github.com/navikt/aksel/pull/2457))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- :wheelchair: Textarea: Forsinkelse ved live-opplesning av gjenstående tegn ([#2502](https://github.com/navikt/aksel/pull/2502))
|
||||
|
||||
## 5.10.4
|
||||
|
||||
## 5.10.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- :bug: Textarea: Teller flyttet ut av tekstfeltet for å unngå overlapp og misforståelser ([#2483](https://github.com/navikt/aksel/pull/2483))
|
||||
|
||||
## 5.10.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Primtives: CSS-import for primitives er nå mer spesifikk ([#2469](https://github.com/navikt/aksel/pull/2469))
|
||||
|
||||
## 5.10.1
|
||||
|
||||
## 5.10.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Primitives: Ny komponent `Page` og `Page.Block` ([#2456](https://github.com/navikt/aksel/pull/2456))
|
||||
|
||||
## 5.9.2
|
||||
|
||||
## 5.9.1
|
||||
|
||||
@@ -35,6 +35,10 @@ const StyleMappings = {
|
||||
component: "Box",
|
||||
main: primitivesCss,
|
||||
},
|
||||
{
|
||||
component: "Page",
|
||||
main: primitivesCss,
|
||||
},
|
||||
{
|
||||
component: "HGrid",
|
||||
main: primitivesCss,
|
||||
@@ -183,7 +187,7 @@ const StyleMappings = {
|
||||
{
|
||||
component: "DatePicker",
|
||||
main: "date.css",
|
||||
dependencies: [typoCss, primitivesCss, "button.css", "popover.css"],
|
||||
dependencies: [typoCss, "button.css", "popover.css", primitivesCss],
|
||||
},
|
||||
{
|
||||
component: "MonthPicker",
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
|
||||
.navds-switch__input:hover ~ .navds-switch__label-wrapper,
|
||||
.navds-switch__label-wrapper:hover {
|
||||
color: var(--ac-switch-action, var(--a-surface-action));
|
||||
color: var(--ac-switch-action, var(--a-surface-action-selected));
|
||||
}
|
||||
|
||||
.navds-switch__input:disabled:hover ~ .navds-switch__label-wrapper {
|
||||
@@ -91,7 +91,7 @@
|
||||
}
|
||||
|
||||
.navds-switch__input:checked ~ .navds-switch__track {
|
||||
background-color: var(--ac-switch-checked-bg, var(--a-surface-success));
|
||||
background-color: var(--ac-switch-checked-bg, var(--a-surface-action-selected));
|
||||
}
|
||||
|
||||
.navds-switch__input:hover ~ .navds-switch__track {
|
||||
@@ -99,7 +99,7 @@
|
||||
}
|
||||
|
||||
.navds-switch__input:hover:checked ~ .navds-switch__track {
|
||||
background-color: var(--ac-switch-checked-hover-bg, var(--a-surface-success-hover));
|
||||
background-color: var(--ac-switch-checked-hover-bg, var(--a-surface-action-selected-hover));
|
||||
}
|
||||
|
||||
.navds-switch__input:disabled ~ .navds-switch__track {
|
||||
@@ -107,7 +107,7 @@
|
||||
}
|
||||
|
||||
.navds-switch__input:checked:disabled ~ .navds-switch__track {
|
||||
background-color: var(--ac-switch-checked-bg, var(--a-border-success));
|
||||
background-color: var(--ac-switch-checked-bg, var(--a-border-action-selected));
|
||||
}
|
||||
|
||||
.navds-switch__input:focus-visible ~ .navds-switch__track {
|
||||
@@ -139,7 +139,7 @@
|
||||
|
||||
.navds-switch__input:checked ~ .navds-switch__track > .navds-switch__thumb {
|
||||
transform: translateX(1.25rem);
|
||||
color: var(--ac-switch-thumb-icon-checked, var(--a-icon-success));
|
||||
color: var(--ac-switch-thumb-icon-checked, var(--a-icon-action-selected));
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: fine) {
|
||||
@@ -160,11 +160,6 @@
|
||||
transform: translateX(1.25rem);
|
||||
}
|
||||
|
||||
/* Loader */
|
||||
.navds-switch__input:checked ~ .navds-switch__track > .navds-switch__thumb .navds-loader__foreground {
|
||||
stroke: var(--ac-switch-thumb-icon-checked, var(--a-icon-success));
|
||||
}
|
||||
|
||||
/* Disabled */
|
||||
.navds-switch__input:disabled {
|
||||
appearance: none;
|
||||
|
||||
@@ -21,8 +21,14 @@
|
||||
}
|
||||
|
||||
.navds-textarea__wrapper {
|
||||
--__ac-textarea-height: initial;
|
||||
|
||||
position: relative;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
|
||||
.navds-textarea__input:first-child {
|
||||
height: var(--__ac-textarea-height);
|
||||
}
|
||||
|
||||
.navds-textarea__input {
|
||||
@@ -37,10 +43,6 @@
|
||||
color: var(--ac-textarea-text, var(--__ac-textarea-text, var(--a-text-default)));
|
||||
}
|
||||
|
||||
.navds-textarea--counter {
|
||||
padding-bottom: var(--a-spacing-8);
|
||||
}
|
||||
|
||||
.navds-textarea__input::placeholder {
|
||||
color: var(--ac-textarea-placeholder, var(--__ac-textarea-placeholder, var(--a-text-subtle)));
|
||||
}
|
||||
@@ -65,27 +67,51 @@
|
||||
padding: var(--a-spacing-1-alt);
|
||||
}
|
||||
|
||||
.navds-form-field--small .navds-textarea--counter.navds-textarea__input {
|
||||
padding-bottom: var(--a-spacing-7);
|
||||
}
|
||||
|
||||
.navds-textarea__counter {
|
||||
pointer-events: none;
|
||||
margin-top: var(--a-spacing-05);
|
||||
color: var(--ac-textarea-counter-text, var(--__ac-textarea-counter-text, var(--a-text-subtle)));
|
||||
font-style: italic;
|
||||
position: absolute;
|
||||
text-align: left;
|
||||
left: 0.0625rem;
|
||||
bottom: 0.0625rem;
|
||||
padding: var(--a-spacing-1) var(--a-spacing-2);
|
||||
}
|
||||
|
||||
.navds-textarea__counter--error {
|
||||
color: var(--ac-textarea-counter-error-text, var(--__ac-textarea-counter-error-text, var(--a-text-danger)));
|
||||
}
|
||||
|
||||
.navds-textarea--resize .navds-textarea__input {
|
||||
.navds-textarea__sr-counter {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navds-textarea__wrapper:focus-within .navds-textarea__sr-counter {
|
||||
display: initial;
|
||||
}
|
||||
|
||||
.navds-textarea--resize-both .navds-textarea__input {
|
||||
resize: both;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
.navds-textarea--resize-horizontal .navds-textarea__input {
|
||||
resize: horizontal;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
.navds-textarea--resize-vertical .navds-textarea__input {
|
||||
resize: vertical;
|
||||
border-end-end-radius: 0;
|
||||
}
|
||||
|
||||
.navds-textarea--autoscrollbar,
|
||||
.navds-textarea--autoscrollbar .navds-textarea__wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
/* Makes the box-shadow visible even though overflow is hidden: */
|
||||
padding: 3px;
|
||||
margin: -3px;
|
||||
}
|
||||
|
||||
.navds-textarea--autoscrollbar .navds-textarea__input {
|
||||
scrollbar-gutter: stable; /* Needed to prevent scrollbar from appearing too early */
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
@charset "UTF-8";
|
||||
@import "baseline/index.css";
|
||||
@import "typography.css";
|
||||
@import "primitives/index.css";
|
||||
@import "grid.css";
|
||||
@import "accordion.css";
|
||||
@import "alert.css";
|
||||
@@ -34,3 +33,4 @@
|
||||
@import "table.css";
|
||||
@import "tabs.css";
|
||||
@import "list.css";
|
||||
@import "primitives/index.css";
|
||||
@@ -118,6 +118,7 @@
|
||||
padding: var(--a-spacing-6);
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
position: relative; /* Needed to make sr-only elements position correctly - see Storybook */
|
||||
}
|
||||
|
||||
.navds-modal__header + .navds-modal__body {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "5.9.2",
|
||||
"version": "5.11.4",
|
||||
"description": "CSS for NAV Designsystem",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
@@ -27,7 +27,7 @@
|
||||
"css:get-version": "node config/get-version.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^5.9.2",
|
||||
"@navikt/ds-tokens": "^5.11.4",
|
||||
"cssnano": "6.0.0",
|
||||
"fast-glob": "3.2.11",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
@import "box.css";
|
||||
@import "bleed.css";
|
||||
@import "page.css";
|
||||
@import "hgrid.css";
|
||||
@import "stack.css";
|
||||
@import "responsive.css";
|
||||
@@ -0,0 +1,54 @@
|
||||
.navds-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
min-height: 100lvh;
|
||||
|
||||
/* stylelint-disable-next-line csstools/value-no-unknown-custom-properties */
|
||||
background-color: var(--__ac-page-background);
|
||||
}
|
||||
|
||||
/* Page.Block */
|
||||
.navds-pageblock {
|
||||
margin-inline: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.navds-page__content--grow {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.navds-page__content--fullheight {
|
||||
min-height: 100vh;
|
||||
min-height: 100lvh;
|
||||
}
|
||||
|
||||
.navds-page__content--padding {
|
||||
padding-block-end: var(--a-spacing-8);
|
||||
}
|
||||
|
||||
.navds-pageblock--md {
|
||||
max-width: 768px;
|
||||
}
|
||||
|
||||
.navds-pageblock--lg {
|
||||
max-width: 1024px;
|
||||
}
|
||||
|
||||
.navds-pageblock--xl {
|
||||
max-width: 1280px;
|
||||
}
|
||||
|
||||
.navds-pageblock--2xl {
|
||||
max-width: 1440px;
|
||||
}
|
||||
|
||||
.navds-pageblock--gutters {
|
||||
padding-inline: var(--a-spacing-4);
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.navds-pageblock--gutters {
|
||||
padding-inline: var(--a-spacing-12);
|
||||
}
|
||||
}
|
||||
@@ -401,14 +401,14 @@
|
||||
"--ac-select-error-border": "--a-surface-danger"
|
||||
},
|
||||
"switch": {
|
||||
"--ac-switch-action": "--a-surface-action",
|
||||
"--ac-switch-action": "--a-surface-action-selected",
|
||||
"--ac-switch-bg": "--a-surface-neutral",
|
||||
"--ac-switch-checked-bg": "--a-surface-success",
|
||||
"--ac-switch-checked-bg": "--a-surface-action-selected",
|
||||
"--ac-switch-hover-bg": "--a-surface-neutral-hover",
|
||||
"--ac-switch-checked-hover-bg": "--a-surface-success-hover",
|
||||
"--ac-switch-checked-hover-bg": "--a-surface-action-selected-hover",
|
||||
"--ac-switch-thumb-bg": "--a-surface-default",
|
||||
"--ac-switch-thumb-icon": "--a-icon-subtle",
|
||||
"--ac-switch-thumb-icon-checked": "--a-icon-success"
|
||||
"--ac-switch-thumb-icon-checked": "--a-icon-action-selected"
|
||||
},
|
||||
"textfield": {
|
||||
"--ac-textfield-bg": "--a-surface-default",
|
||||
|
||||
@@ -1,5 +1,121 @@
|
||||
# @navikt/ds-react
|
||||
|
||||
## 5.11.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Fix issue where clicking in Combobox.FilteredOptions after scrolling selected the wrong element ([#2536](https://github.com/navikt/aksel/pull/2536))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.11.4
|
||||
- @navikt/aksel-icons@5.11.4
|
||||
|
||||
## 5.11.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- :bug: Textarea: Skru av autosize ved manuell resize ([#2518](https://github.com/navikt/aksel/pull/2518))
|
||||
|
||||
- :bug: Popover: Ikke lukk ved klikk inni når parent er fokuserbar ([#2521](https://github.com/navikt/aksel/pull/2521))
|
||||
|
||||
- Page: La til `md`-bredde (768px) ([#2510](https://github.com/navikt/aksel/pull/2510))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.11.3
|
||||
- @navikt/aksel-icons@5.11.3
|
||||
|
||||
## 5.11.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ⬆️ upgrade date-fns version & make it minor-version agnostic ([#2514](https://github.com/navikt/aksel/pull/2514))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.11.2
|
||||
- @navikt/aksel-icons@5.11.2
|
||||
|
||||
## 5.11.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Switch: Byttet success-farger med action npr checked ([#2511](https://github.com/navikt/aksel/pull/2511))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.11.1
|
||||
- @navikt/aksel-icons@5.11.1
|
||||
|
||||
## 5.11.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- :sparkles: Textarea: Mulighet for å sette resize-retning ([#2494](https://github.com/navikt/aksel/pull/2494))
|
||||
|
||||
- :sparkles: Textarea: Eksperimentell støtte for automatisk scrollbar ([#2457](https://github.com/navikt/aksel/pull/2457))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- :wheelchair: Textarea: Forsinkelse ved live-opplesning av gjenstående tegn ([#2502](https://github.com/navikt/aksel/pull/2502))
|
||||
|
||||
- Updated dependencies [[`bc259f5b3`](https://github.com/navikt/aksel/commit/bc259f5b306319c0e08397d5300596fc1d43353e)]:
|
||||
- @navikt/aksel-icons@5.11.0
|
||||
- @navikt/ds-tokens@5.11.0
|
||||
|
||||
## 5.10.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Gjør lukknapp i Alert til type button ([#2497](https://github.com/navikt/aksel/pull/2497))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.10.4
|
||||
- @navikt/aksel-icons@5.10.4
|
||||
|
||||
## 5.10.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- :wheelchair: Textarea: Byttet fra `aria-live` til `role=status` på telleren for bedre semantikk ([#2483](https://github.com/navikt/aksel/pull/2483))
|
||||
|
||||
- :bug: Textarea: Teller flyttet ut av tekstfeltet for å unngå overlapp og misforståelser ([#2483](https://github.com/navikt/aksel/pull/2483))
|
||||
|
||||
- ErrorSummary: Gjør det mulig å rendre ErrorSummary.Item conditionally ([#2484](https://github.com/navikt/aksel/pull/2484))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.10.3
|
||||
- @navikt/aksel-icons@5.10.3
|
||||
|
||||
## 5.10.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- OverridableComponent: Fungerer nå bedre ved bruk av typeof <komponent> ([#2474](https://github.com/navikt/aksel/pull/2474))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.10.2
|
||||
- @navikt/aksel-icons@5.10.2
|
||||
|
||||
## 5.10.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Table: Håndterer nå sjekk for interaktive elementer i ExpandableRow bedre ([#2461](https://github.com/navikt/aksel/pull/2461))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.10.1
|
||||
- @navikt/aksel-icons@5.10.1
|
||||
|
||||
## 5.10.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Primitives: Ny komponent `Page` og `Page.Block` ([#2456](https://github.com/navikt/aksel/pull/2456))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.10.0
|
||||
- @navikt/aksel-icons@5.10.0
|
||||
|
||||
## 5.9.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "5.9.2",
|
||||
"version": "5.11.4",
|
||||
"description": "Aksel react-components for NAV designsystem",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"license": "MIT",
|
||||
@@ -38,12 +38,12 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "0.25.4",
|
||||
"@navikt/aksel-icons": "^5.9.2",
|
||||
"@navikt/ds-tokens": "^5.9.2",
|
||||
"@navikt/aksel-icons": "^5.11.4",
|
||||
"@navikt/ds-tokens": "^5.11.4",
|
||||
"@radix-ui/react-tabs": "1.0.0",
|
||||
"@radix-ui/react-toggle-group": "1.0.0",
|
||||
"clsx": "^1.2.1",
|
||||
"date-fns": "2.29.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"react-day-picker": "8.3.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -117,6 +117,7 @@ export const Alert = forwardRef<HTMLDivElement, AlertProps>(
|
||||
size="small"
|
||||
variant="tertiary-neutral"
|
||||
onClick={onClose}
|
||||
type="button"
|
||||
icon={<XMarkIcon title="Lukk Alert" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -3,8 +3,8 @@ import React, { forwardRef, useMemo, useRef, useState } from "react";
|
||||
import { Loader } from "../loader";
|
||||
import { Label } from "../typography";
|
||||
import {
|
||||
mergeRefs,
|
||||
OverridableComponent,
|
||||
mergeRefs,
|
||||
omit,
|
||||
useClientLayoutEffect,
|
||||
} from "../util";
|
||||
|
||||
@@ -168,7 +168,7 @@ export const FollowYear = () => {
|
||||
return (
|
||||
<div className="min-h-96">
|
||||
<MonthPicker {...monthpickerProps} onYearChange={customYearChange}>
|
||||
<MonthPicker.Input {...inputProps} label="Velg månede" />
|
||||
<MonthPicker.Input {...inputProps} label="Velg måned" />
|
||||
</MonthPicker>
|
||||
{selectedMonth && <div className="pt-4">{selectedMonth.getMonth()}</div>}
|
||||
</div>
|
||||
|
||||
@@ -5,11 +5,11 @@ import React, {
|
||||
useEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { FormFieldProps, useFormField } from "./useFormField";
|
||||
import { ReadOnlyIcon } from "./ReadOnlyIcon";
|
||||
import { Loader } from "../loader";
|
||||
import { BodyShort } from "../typography";
|
||||
import { omit } from "../util";
|
||||
import { ReadOnlyIcon } from "./ReadOnlyIcon";
|
||||
import { FormFieldProps, useFormField } from "./useFormField";
|
||||
|
||||
const SelectedIcon = () => (
|
||||
<svg
|
||||
@@ -134,7 +134,11 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
||||
<span className="navds-switch__track">
|
||||
<span className="navds-switch__thumb">
|
||||
{loading ? (
|
||||
<Loader size="xsmall" aria-live="polite" />
|
||||
<Loader
|
||||
size="xsmall"
|
||||
aria-live="polite"
|
||||
variant={checked ? "interaction" : "neutral"}
|
||||
/>
|
||||
) : checked ? (
|
||||
<SelectedIcon />
|
||||
) : null}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef, useState } from "react";
|
||||
import { BodyShort, ErrorMessage, Label } from "../typography";
|
||||
import TextareaAutosize from "../util/TextareaAutoSize";
|
||||
import { FormFieldProps, useFormField } from "./useFormField";
|
||||
import { ReadOnlyIcon } from "./ReadOnlyIcon";
|
||||
import { omit, useId } from "../util";
|
||||
import TextareaAutosize from "../util/TextareaAutoSize";
|
||||
import { ReadOnlyIcon } from "./ReadOnlyIcon";
|
||||
import Counter from "./TextareaCounter";
|
||||
import { FormFieldProps, useFormField } from "./useFormField";
|
||||
|
||||
/**
|
||||
* TODO: Mulighet for lokalisering av sr-only/counter text
|
||||
@@ -27,7 +28,6 @@ export interface TextareaProps
|
||||
defaultValue?: string;
|
||||
/**
|
||||
* Maximum number of character rows to display.
|
||||
* @bug Internal scrolling with `maxLength` scrolls over maxLength-text
|
||||
*/
|
||||
maxRows?: number;
|
||||
/**
|
||||
@@ -45,12 +45,18 @@ export interface TextareaProps
|
||||
/**
|
||||
* Enables resizing of field
|
||||
*/
|
||||
resize?: boolean;
|
||||
resize?: boolean | "vertical" | "horizontal";
|
||||
/**
|
||||
* Textarea will stop growing and get a scrollbar when there's no more room to grow.
|
||||
* Requires `display:flex` on the parent.
|
||||
* Experimental feature that may be removed or get breaking changes in a minor version.
|
||||
*/
|
||||
UNSAFE_autoScrollbar?: boolean;
|
||||
/**
|
||||
* i18n-translations for counter-text
|
||||
*/
|
||||
i18n?: {
|
||||
/** @default Antall tegn igjen */
|
||||
/** @default tegn igjen */
|
||||
counterLeft?: string;
|
||||
/** @default tegn for mye */
|
||||
counterTooMuch?: string;
|
||||
@@ -86,6 +92,7 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
maxLength,
|
||||
hideLabel = false,
|
||||
resize,
|
||||
UNSAFE_autoScrollbar,
|
||||
i18n,
|
||||
readOnly,
|
||||
...rest
|
||||
@@ -121,7 +128,9 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
"navds-form-field--readonly": readOnly,
|
||||
"navds-textarea--readonly": readOnly,
|
||||
"navds-textarea--error": hasError,
|
||||
"navds-textarea--resize": resize,
|
||||
"navds-textarea--autoscrollbar": UNSAFE_autoScrollbar,
|
||||
[`navds-textarea--resize-${resize === true ? "both" : resize}`]:
|
||||
resize,
|
||||
}
|
||||
)}
|
||||
>
|
||||
@@ -157,19 +166,17 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
: setControlledValue(e.target.value)
|
||||
}
|
||||
minRows={getMinRows()}
|
||||
autoScrollbar={UNSAFE_autoScrollbar}
|
||||
ref={ref}
|
||||
readOnly={readOnly}
|
||||
className={cl(
|
||||
"navds-textarea__input",
|
||||
"navds-body-short",
|
||||
`navds-body-short--${size ?? "medium"}`,
|
||||
{
|
||||
"navds-textarea--counter": hasMaxLength,
|
||||
}
|
||||
`navds-body-short--${size ?? "medium"}`
|
||||
)}
|
||||
{...(describedBy ? { "aria-describedby": describedBy } : {})}
|
||||
/>
|
||||
{hasMaxLength && (
|
||||
{hasMaxLength && !readOnly && !inputProps.disabled && (
|
||||
<>
|
||||
<span id={maxLengthId} className="navds-sr-only">
|
||||
{`Tekstområde med plass til ${maxLength} tegn.`}
|
||||
@@ -198,22 +205,4 @@ export const Textarea = forwardRef<HTMLTextAreaElement, TextareaProps>(
|
||||
}
|
||||
);
|
||||
|
||||
export const Counter = ({ maxLength, currentLength, size, i18n }) => {
|
||||
const difference = maxLength - currentLength;
|
||||
|
||||
return (
|
||||
<BodyShort
|
||||
className={cl("navds-textarea__counter", {
|
||||
"navds-textarea__counter--error": difference < 0,
|
||||
})}
|
||||
aria-live={difference < 20 ? "polite" : "off"}
|
||||
size={size}
|
||||
>
|
||||
{difference < 0
|
||||
? `${Math.abs(difference)} ${i18n?.counterTooMuch ?? "tegn for mye"}`
|
||||
: `${difference} ${i18n?.counterLeft ?? "tegn igjen"}`}
|
||||
</BodyShort>
|
||||
);
|
||||
};
|
||||
|
||||
export default Textarea;
|
||||
@@ -0,0 +1,58 @@
|
||||
import cl from "clsx";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { BodyShort } from "../typography";
|
||||
import debounce from "../util/debounce";
|
||||
import type { TextareaProps } from "./Textarea";
|
||||
|
||||
interface Props {
|
||||
maxLength: number;
|
||||
currentLength: number;
|
||||
size: TextareaProps["size"];
|
||||
i18n: TextareaProps["i18n"];
|
||||
}
|
||||
|
||||
const TextareaCounter = ({ maxLength, currentLength, size, i18n }: Props) => {
|
||||
const difference = maxLength - currentLength;
|
||||
|
||||
const [debouncedDiff, setDebouncedDiff] = useState(difference);
|
||||
|
||||
useEffect(() => {
|
||||
const debounceFunc = debounce(() => {
|
||||
setDebouncedDiff(difference);
|
||||
}, 2000);
|
||||
debounceFunc();
|
||||
|
||||
return () => {
|
||||
debounceFunc.clear();
|
||||
};
|
||||
}, [difference]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{difference < 20 && (
|
||||
<span
|
||||
role="status"
|
||||
className="navds-textarea__sr-counter navds-sr-only"
|
||||
>
|
||||
{getCounterText(debouncedDiff, i18n)}
|
||||
</span>
|
||||
)}
|
||||
|
||||
<BodyShort
|
||||
className={cl("navds-textarea__counter", {
|
||||
"navds-textarea__counter--error": difference < 0,
|
||||
})}
|
||||
size={size}
|
||||
>
|
||||
{getCounterText(difference, i18n)}
|
||||
</BodyShort>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const getCounterText = (difference: number, i18n: TextareaProps["i18n"]) =>
|
||||
difference < 0
|
||||
? `${Math.abs(difference)} ${i18n?.counterTooMuch ?? "tegn for mye"}`
|
||||
: `${difference} ${i18n?.counterLeft ?? "tegn igjen"}`;
|
||||
|
||||
export default TextareaCounter;
|
||||
@@ -168,10 +168,6 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
[filteredOptions.length, virtualFocus, onChange, toggleIsListOpen]
|
||||
);
|
||||
|
||||
const onBlur = () => {
|
||||
virtualFocus.moveFocusToTop();
|
||||
};
|
||||
|
||||
return (
|
||||
<input
|
||||
{...rest}
|
||||
@@ -181,7 +177,6 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
onChange={onChangeHandler}
|
||||
type="text"
|
||||
role="combobox"
|
||||
onBlur={onBlur}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-controls={filteredOptionsUtil.getFilteredOptionsId(inputProps.id)}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { forwardRef, HTMLAttributes } from "react";
|
||||
import React, { forwardRef, HTMLAttributes, isValidElement } from "react";
|
||||
import cl from "clsx";
|
||||
import { Heading, BodyShort } from "../../typography";
|
||||
import ErrorSummaryItem, { ErrorSummaryItemType } from "./ErrorSummaryItem";
|
||||
@@ -100,7 +100,10 @@ export const ErrorSummary = forwardRef<HTMLDivElement, ErrorSummaryProps>(
|
||||
</Heading>
|
||||
<BodyShort as="ul" size={size} className="navds-error-summary__list">
|
||||
{React.Children.map(children, (child) => {
|
||||
return <li key={child?.toString()}>{child}</li>;
|
||||
if (!isValidElement(child)) {
|
||||
return null;
|
||||
}
|
||||
return <li key={child.toString()}>{child}</li>;
|
||||
})}
|
||||
</BodyShort>
|
||||
</section>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
export {
|
||||
default as ConfirmationPanel,
|
||||
type ConfirmationPanelProps,
|
||||
} from "./ConfirmationPanel";
|
||||
export { Fieldset, FieldsetContext, type FieldsetProps } from "./Fieldset";
|
||||
export { default as Select, type SelectProps } from "./Select";
|
||||
export { default as Switch, type SwitchProps } from "./Switch";
|
||||
export { default as TextField, type TextFieldProps } from "./TextField";
|
||||
export { default as Textarea, type TextareaProps } from "./Textarea";
|
||||
export {
|
||||
Checkbox,
|
||||
CheckboxGroup,
|
||||
type CheckboxGroupProps,
|
||||
type CheckboxProps,
|
||||
} from "./checkbox";
|
||||
export {
|
||||
type ConfirmationPanelProps,
|
||||
default as ConfirmationPanel,
|
||||
} from "./ConfirmationPanel";
|
||||
export { Combobox as UNSAFE_Combobox, type ComboboxProps } from "./combobox";
|
||||
export { ErrorSummary, type ErrorSummaryProps } from "./error-summary";
|
||||
export { Fieldset, FieldsetContext, type FieldsetProps } from "./Fieldset";
|
||||
export {
|
||||
Radio,
|
||||
RadioGroup,
|
||||
@@ -17,8 +22,3 @@ export {
|
||||
type RadioProps,
|
||||
} from "./radio";
|
||||
export { Search, type SearchClearEvent, type SearchProps } from "./search";
|
||||
export { Combobox as UNSAFE_Combobox, type ComboboxProps } from "./combobox";
|
||||
export { default as Select, type SelectProps } from "./Select";
|
||||
export { default as Switch, type SwitchProps } from "./Switch";
|
||||
export { Counter, default as Textarea, type TextareaProps } from "./Textarea";
|
||||
export { default as TextField, type TextFieldProps } from "./TextField";
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { Meta, StoryFn, StoryObj } from "@storybook/react";
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "../../button";
|
||||
import { Textarea } from "../index";
|
||||
|
||||
const meta: Meta<typeof Textarea> = {
|
||||
@@ -14,12 +15,15 @@ export const Default: StoryObj<typeof Textarea> = {
|
||||
},
|
||||
|
||||
args: {
|
||||
maxLength: 100,
|
||||
maxLength: 0,
|
||||
label: "Ipsum enim quis culpa",
|
||||
resize: false,
|
||||
},
|
||||
|
||||
argTypes: {
|
||||
resize: {
|
||||
control: { type: "radio" },
|
||||
options: [true, "vertical", "horizontal"],
|
||||
},
|
||||
size: {
|
||||
control: { type: "radio" },
|
||||
options: ["medium", "small"],
|
||||
@@ -28,16 +32,17 @@ export const Default: StoryObj<typeof Textarea> = {
|
||||
error: { type: "string" },
|
||||
hideLabel: { type: "boolean" },
|
||||
disabled: { type: "boolean" },
|
||||
readOnly: { type: "boolean" },
|
||||
maxRows: { type: "number" },
|
||||
minRows: { type: "number" },
|
||||
},
|
||||
};
|
||||
|
||||
export const Small = () => {
|
||||
export const Small: StoryFn = () => {
|
||||
return <Textarea size="small" label="Ipsum enim quis culpa" />;
|
||||
};
|
||||
|
||||
export const Description = () => {
|
||||
export const Description: StoryFn = () => {
|
||||
return (
|
||||
<div className="colgap">
|
||||
<Textarea
|
||||
@@ -53,7 +58,7 @@ export const Description = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Error = () => {
|
||||
export const Error: StoryFn = () => {
|
||||
return (
|
||||
<div className="colgap">
|
||||
<Textarea
|
||||
@@ -66,11 +71,25 @@ export const Error = () => {
|
||||
error="Consectetur labore velit eiusmod Lorem ut nostrud mollit labore ullamco laboris laboris in."
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Ipsum enim quis culpa"
|
||||
error="Consectetur labore velit eiusmod Lorem ut nostrud mollit labore ullamco laboris laboris in."
|
||||
maxLength={20}
|
||||
/>
|
||||
|
||||
<Textarea
|
||||
label="Ipsum enim quis culpa"
|
||||
error="Consectetur labore velit eiusmod Lorem ut nostrud mollit labore ullamco laboris laboris in."
|
||||
value="Sed dignissim sollicitudin porta."
|
||||
maxLength={20}
|
||||
size="small"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const Disabled = () => {
|
||||
export const Disabled: StoryFn = () => {
|
||||
return (
|
||||
<div className="colgap">
|
||||
<Textarea label="Ipsum enim quis culpa" disabled />
|
||||
@@ -79,19 +98,19 @@ export const Disabled = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const HideLabel = () => {
|
||||
export const HideLabel: StoryFn = () => {
|
||||
return <Textarea label="Ipsum enim quis culpa" hideLabel />;
|
||||
};
|
||||
|
||||
export const MaxLength = () => {
|
||||
return <Textarea maxLength={200} label="Ipsum enim quis culpa" />;
|
||||
export const MaxLength: StoryFn = () => {
|
||||
return <Textarea maxLength={50} label="Ipsum enim quis culpa" />;
|
||||
};
|
||||
|
||||
export const MinRows = () => {
|
||||
export const MinRows: StoryFn = () => {
|
||||
return <Textarea minRows={5} label="Ipsum enim quis culpa" />;
|
||||
};
|
||||
|
||||
export const MaxRows = () => {
|
||||
export const MaxRows: StoryFn = () => {
|
||||
return (
|
||||
<Textarea
|
||||
maxRows={3}
|
||||
@@ -101,11 +120,23 @@ export const MaxRows = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Resize = () => {
|
||||
export const Resize: StoryFn = () => {
|
||||
return <Textarea resize label="Ipsum enim quis culpa" />;
|
||||
};
|
||||
|
||||
export const Readonly = () => {
|
||||
export const Controlled: StoryFn = () => {
|
||||
const [value, setValue] = useState("");
|
||||
return (
|
||||
<Textarea
|
||||
resize
|
||||
label="Ipsum enim quis culpa"
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.currentTarget.value)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const Readonly: StoryFn = () => {
|
||||
return (
|
||||
<div className="colgap">
|
||||
<Textarea
|
||||
@@ -123,3 +154,35 @@ export const Readonly = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const AutoScrollbar: StoryFn<typeof Textarea> = (props) => (
|
||||
<div
|
||||
style={{
|
||||
border: "1px solid lightGreen",
|
||||
height: "90vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
<div style={{ border: "1px dashed gray" }}>
|
||||
<h1>Header</h1>
|
||||
</div>
|
||||
<Textarea
|
||||
{...props}
|
||||
resize
|
||||
label="Textarea with autoScrollbar"
|
||||
description="Description"
|
||||
maxLength={30}
|
||||
UNSAFE_autoScrollbar
|
||||
/>
|
||||
<div style={{ border: "1px dashed gray" }}>
|
||||
<Button>Send</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
AutoScrollbar.argTypes = {
|
||||
error: { type: "string" },
|
||||
hideLabel: { type: "boolean" },
|
||||
maxRows: { type: "number" },
|
||||
minRows: { type: "number" },
|
||||
};
|
||||
@@ -38,3 +38,4 @@ export * from "./layout/stack";
|
||||
export * from "./layout/grid";
|
||||
export * from "./layout/content-container";
|
||||
export * from "./layout/responsive";
|
||||
export * from "./layout/page";
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from "react";
|
||||
import type { Meta } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { Box, HStack, VStack } from "../..";
|
||||
import { BodyLong } from "../../typography";
|
||||
import { Bleed } from "./Bleed";
|
||||
import { Box, HStack, VStack } from "../..";
|
||||
|
||||
export default {
|
||||
title: "ds-react/Primitives/Bleed",
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
import bgColors from "@navikt/ds-tokens/src/colors-bg.json";
|
||||
import type { Meta, StoryFn } from "@storybook/react";
|
||||
import React, { useEffect } from "react";
|
||||
import { Box } from "../box";
|
||||
import { HGrid } from "../grid";
|
||||
import Page from "./Page";
|
||||
import { widths } from "./parts/PageBlock";
|
||||
|
||||
const meta: Meta = {
|
||||
title: "ds-react/Primitives/Page",
|
||||
component: Page,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const Default: StoryFn = ({
|
||||
belowFold,
|
||||
background,
|
||||
width,
|
||||
gutters,
|
||||
contentBlockPadding,
|
||||
}) => (
|
||||
<Page
|
||||
footer={<Footer width={width} gutters={gutters} />}
|
||||
footerPosition={belowFold ? "belowFold" : undefined}
|
||||
background={background}
|
||||
contentBlockPadding={contentBlockPadding}
|
||||
>
|
||||
<Header width={width} gutters={gutters} />
|
||||
<Content width={width} gutters={gutters} />
|
||||
</Page>
|
||||
);
|
||||
|
||||
Default.argTypes = {
|
||||
width: {
|
||||
control: "radio",
|
||||
options: widths,
|
||||
},
|
||||
background: {
|
||||
control: "radio",
|
||||
options: Object.keys(bgColors.a),
|
||||
},
|
||||
belowFold: {
|
||||
control: "boolean",
|
||||
},
|
||||
gutters: {
|
||||
control: "boolean",
|
||||
},
|
||||
contentBlockPadding: {
|
||||
control: "radio",
|
||||
options: ["end", "none"],
|
||||
},
|
||||
};
|
||||
|
||||
Default.args = {
|
||||
belowFold: false,
|
||||
gutters: false,
|
||||
contentBlockPadding: "end",
|
||||
};
|
||||
|
||||
export const BelowFold: StoryFn = () => (
|
||||
<Page
|
||||
footer={<Footer />}
|
||||
footerPosition="belowFold"
|
||||
contentBlockPadding="end"
|
||||
>
|
||||
<Header />
|
||||
<Content />
|
||||
</Page>
|
||||
);
|
||||
|
||||
export const Background: StoryFn = () => (
|
||||
<HGrid columns={2} gap="4">
|
||||
<Page
|
||||
background="bg-default"
|
||||
footer={<div>footer</div>}
|
||||
contentBlockPadding="end"
|
||||
>
|
||||
<div>header</div>
|
||||
<div>content</div>
|
||||
</Page>
|
||||
<Page
|
||||
background="bg-subtle"
|
||||
footer={<div>footer</div>}
|
||||
contentBlockPadding="end"
|
||||
>
|
||||
<div>header</div>
|
||||
<div>content</div>
|
||||
</Page>
|
||||
</HGrid>
|
||||
);
|
||||
|
||||
export const ContentBlockPadding: StoryFn = () => (
|
||||
<HGrid columns={2} gap="6" align="start">
|
||||
<Page
|
||||
footer={<Footer width="lg" gutters />}
|
||||
contentBlockPadding="end"
|
||||
background="bg-subtle"
|
||||
>
|
||||
<Header width="lg" gutters />
|
||||
<Page.Block width="lg" gutters as="main">
|
||||
<Box background="surface-alt-3-subtle" style={{ height: "80vh" }}>
|
||||
Main
|
||||
</Box>
|
||||
</Page.Block>
|
||||
</Page>
|
||||
<Page
|
||||
footer={<Footer width="lg" gutters />}
|
||||
contentBlockPadding="none"
|
||||
background="bg-subtle"
|
||||
>
|
||||
<Header width="lg" gutters />
|
||||
<Page.Block width="lg" gutters as="main">
|
||||
<Box background="surface-alt-3-subtle" style={{ height: "80vh" }}>
|
||||
Main
|
||||
</Box>
|
||||
</Page.Block>
|
||||
</Page>
|
||||
</HGrid>
|
||||
);
|
||||
|
||||
export const Gutters: StoryFn = () => (
|
||||
<HGrid columns={2} gap="6" align="start">
|
||||
<Page footer={<Footer width="lg" gutters />} background="bg-subtle">
|
||||
<Header width="lg" gutters />
|
||||
<Page.Block width="lg" gutters as="main">
|
||||
<Box background="surface-alt-3-subtle" style={{ height: "80vh" }}>
|
||||
Main
|
||||
</Box>
|
||||
</Page.Block>
|
||||
</Page>
|
||||
<Page footer={<Footer width="lg" />} background="bg-subtle">
|
||||
<Header width="lg" />
|
||||
<Page.Block width="lg" as="main">
|
||||
<Box background="surface-alt-3-subtle" style={{ height: "80vh" }}>
|
||||
Main
|
||||
</Box>
|
||||
</Page.Block>
|
||||
</Page>
|
||||
</HGrid>
|
||||
);
|
||||
|
||||
Gutters.parameters = {
|
||||
chromatic: {
|
||||
modes: {
|
||||
mobile_portrait: {
|
||||
viewport: {
|
||||
width: 400,
|
||||
height: 850,
|
||||
},
|
||||
},
|
||||
desktop: {
|
||||
viewport: {
|
||||
width: 1280,
|
||||
height: 960,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const MILJO_URL = "https://www.nav.no/dekoratoren";
|
||||
|
||||
export const WithDecorator: StoryFn = () => {
|
||||
return (
|
||||
<Page
|
||||
contentBlockPadding="end"
|
||||
footerPosition="belowFold"
|
||||
footer={<div id="decorator-footer"></div>}
|
||||
>
|
||||
<div id="decorator-header"></div>
|
||||
<Content width="lg" gutters />
|
||||
<div
|
||||
id="decorator-env"
|
||||
data-src={`${MILJO_URL}/env?context=privatperson`}
|
||||
></div>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
WithDecorator.decorators = [
|
||||
(Story) => {
|
||||
useEffect(() => {
|
||||
const script = document.createElement("script");
|
||||
script.src = `${MILJO_URL}/client.js`;
|
||||
script.async = true;
|
||||
document.body.appendChild(script);
|
||||
|
||||
const styles = document.createElement("link");
|
||||
styles.href = `${MILJO_URL}/css/client.css`;
|
||||
styles.rel = "stylesheet";
|
||||
document.head.appendChild(styles);
|
||||
|
||||
return () => {
|
||||
document.body.removeChild(script);
|
||||
document.head.removeChild(styles);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return <Story />;
|
||||
},
|
||||
];
|
||||
|
||||
WithDecorator.parameters = {
|
||||
chromatic: {
|
||||
disable: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const OutsideBackground = () => {
|
||||
return (
|
||||
<Page
|
||||
contentBlockPadding="end"
|
||||
footer={
|
||||
<Box
|
||||
background="surface-alt-3-subtle"
|
||||
style={{ height: 100 }}
|
||||
as="footer"
|
||||
>
|
||||
<Page.Block width="lg" gutters>
|
||||
Footer
|
||||
</Page.Block>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Box background="surface-alt-1-subtle" style={{ height: 64 }} as="header">
|
||||
<Page.Block width="lg" gutters>
|
||||
Header
|
||||
</Page.Block>
|
||||
</Box>
|
||||
<Box background="surface-alt-2-subtle" style={{ height: 300 }} as="main">
|
||||
<Page.Block width="lg" gutters>
|
||||
main
|
||||
</Page.Block>
|
||||
</Box>
|
||||
</Page>
|
||||
);
|
||||
};
|
||||
|
||||
function Header({ width = "lg", gutters = false }: any) {
|
||||
return (
|
||||
<Page.Block as="header" width={width} gutters={gutters}>
|
||||
<Box background="surface-alt-3-subtle" style={{ height: 64 }}>
|
||||
Header
|
||||
</Box>
|
||||
</Page.Block>
|
||||
);
|
||||
}
|
||||
|
||||
function Content({ width = "lg", gutters = false }: any) {
|
||||
return (
|
||||
<Page.Block width={width} gutters={gutters} as="main">
|
||||
<Box background="surface-alt-3-subtle" style={{ height: 300 }}>
|
||||
Main
|
||||
</Box>
|
||||
</Page.Block>
|
||||
);
|
||||
}
|
||||
|
||||
function Footer({ width = "lg", gutters = false }: any) {
|
||||
return (
|
||||
<Page.Block width={width} gutters={gutters} as="footer">
|
||||
<Box background="surface-alt-3-subtle" style={{ height: 100 }}>
|
||||
Footer
|
||||
</Box>
|
||||
</Page.Block>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
import bgColors from "@navikt/ds-tokens/src/colors-bg.json";
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import { OverridableComponent } from "../../util";
|
||||
import { PageBlock } from "./parts/PageBlock";
|
||||
|
||||
export interface PageProps extends React.HTMLAttributes<HTMLElement> {
|
||||
/**
|
||||
* Overrides html-tag
|
||||
* @default "div"
|
||||
*/
|
||||
as?: "div" | "body";
|
||||
/**
|
||||
* Background color. Accepts a color token.
|
||||
* @default bg-default
|
||||
*/
|
||||
background?: keyof typeof bgColors.a;
|
||||
/**
|
||||
* Allows better positioning of footer
|
||||
*/
|
||||
footer?: React.ReactNode;
|
||||
/**
|
||||
* Places footer below page-fold
|
||||
*/
|
||||
footerPosition?: "belowFold";
|
||||
/**
|
||||
* Adds a standardised padding of 4rem between content and footer
|
||||
* @default block-end
|
||||
*/
|
||||
contentBlockPadding?: "end" | "none";
|
||||
}
|
||||
|
||||
interface PageComponentType
|
||||
extends OverridableComponent<PageProps, HTMLElement> {
|
||||
Block: typeof PageBlock;
|
||||
}
|
||||
|
||||
export const PageComponent: OverridableComponent<PageProps, HTMLElement> =
|
||||
forwardRef(
|
||||
(
|
||||
{
|
||||
as: Component = "div",
|
||||
className,
|
||||
style: _style,
|
||||
footer,
|
||||
children,
|
||||
footerPosition,
|
||||
background = "bg-default",
|
||||
contentBlockPadding = "end",
|
||||
...rest
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const style: React.CSSProperties = {
|
||||
..._style,
|
||||
"--__ac-page-background": `var(--a-${background})`,
|
||||
};
|
||||
|
||||
const belowFold = footerPosition === "belowFold";
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...rest}
|
||||
className={cl("navds-page", className)}
|
||||
ref={ref}
|
||||
style={style}
|
||||
>
|
||||
<div
|
||||
className={cl({
|
||||
"navds-page__content--fullheight": belowFold,
|
||||
"navds-page__content--grow": !belowFold,
|
||||
"navds-page__content--padding": contentBlockPadding === "end",
|
||||
})}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{footer}
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Page helps with establishing a top-level layout for your page
|
||||
*
|
||||
* @see [📝 Documentation](https://aksel.nav.no/komponenter/primitives/page)
|
||||
* @see 🏷️ {@link PageProps}
|
||||
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <Page
|
||||
* footer={<Page.Block width="xl" gutters />}
|
||||
* >
|
||||
* <Page.Block width="xl" gutters />// Header
|
||||
* <Page.Block width="xl" gutters />// Content
|
||||
* </Page>
|
||||
* ```
|
||||
* @example
|
||||
* Footer placed below page-fold
|
||||
* ```jsx
|
||||
* <Page
|
||||
* footer={<Page.Block width="xl" gutters />}
|
||||
* footerPosition="belowFold"
|
||||
* >
|
||||
* <Page.Block width="xl" gutters />// Header
|
||||
* <Page.Block width="xl" gutters />// Content
|
||||
* </Page>
|
||||
* ```
|
||||
*/
|
||||
const Page = PageComponent as PageComponentType;
|
||||
|
||||
Page.Block = PageBlock;
|
||||
|
||||
export default Page;
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as Page, type PageProps } from "./Page";
|
||||
export { type PageBlockProps } from "./parts/PageBlock";
|
||||
@@ -0,0 +1,73 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import { OverridableComponent } from "../../../util/OverridableComponent";
|
||||
|
||||
export const widths = ["md", "lg", "xl", "2xl"] as const;
|
||||
|
||||
export interface PageBlockProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Predefined max-width
|
||||
* @example
|
||||
* md: 768px
|
||||
* lg: 1024px
|
||||
* xl: 1280px
|
||||
* 2xl: 1440px
|
||||
* @default max-width: 100%;
|
||||
*/
|
||||
width?: (typeof widths)[number];
|
||||
/**
|
||||
* Adds a standardised responsive padding-inline
|
||||
* @example
|
||||
* 3rem on > md
|
||||
* 1rem on < md
|
||||
* @default false
|
||||
*/
|
||||
gutters?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Acts as a top-level container for defining max-width, gutters and horizontal centering
|
||||
*
|
||||
* @see [📝 Documentation](https://aksel.nav.no/komponenter/primitives/page)
|
||||
* @see 🏷️ {@link PageBlockProps}
|
||||
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <Page
|
||||
* footer={<Page.Block width="xl" gutters />}
|
||||
* >
|
||||
* <Page.Block width="xl" gutters />// Header
|
||||
* <Page.Block width="xl" gutters />// Content
|
||||
* </Page>
|
||||
* ```
|
||||
* @example
|
||||
* With background bleed
|
||||
* Wrapping `Page.Block` with `Box` allows the background-color to use full screen-width
|
||||
* ```jsx
|
||||
* <Page
|
||||
* footer={<Box background="..."><Page.Block width="xl" gutters /></Box>}
|
||||
* footerPosition="belowFold"
|
||||
* >
|
||||
* <Box background="..."><Page.Block width="xl" gutters /></Box>//Header
|
||||
* <Box background="..."><Page.Block width="xl" gutters /></Box>//Content
|
||||
* </Page>
|
||||
* ```
|
||||
*/
|
||||
export const PageBlock: OverridableComponent<PageBlockProps, HTMLDivElement> =
|
||||
forwardRef(
|
||||
({ as: Component = "div", gutters, className, width, ...rest }, ref) => {
|
||||
return (
|
||||
<Component
|
||||
{...rest}
|
||||
className={cl(
|
||||
"navds-pageblock",
|
||||
`navds-pageblock--${width}`,
|
||||
className,
|
||||
{ "navds-pageblock--gutters": gutters }
|
||||
)}
|
||||
ref={ref}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -1,13 +1,14 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import { OverridableComponent } from "../util/OverridableComponent";
|
||||
|
||||
export interface LinkProps
|
||||
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
/**
|
||||
* Inverts when the underline appears. If this is false,
|
||||
* the underline does not appear by default, but does appear when the link is hovered.
|
||||
* This makes it more suitable for use when inlined in text.
|
||||
* the underline only appears on hover.
|
||||
*
|
||||
* **NB: Underline can only be removed in menus etc. where it's obvious that it's a link.**
|
||||
* @default true
|
||||
*/
|
||||
underline?: boolean;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { FileIcon } from "@navikt/aksel-icons";
|
||||
import { Meta, StoryFn } from "@storybook/react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { FileIcon } from "@navikt/aksel-icons";
|
||||
import { BodyLong, Button, Heading, Tooltip } from "..";
|
||||
import { Checkbox, CheckboxGroup } from "../form/checkbox";
|
||||
import { VStack } from "../layout/stack";
|
||||
import Modal from "./Modal";
|
||||
|
||||
const meta: Meta<typeof Modal> = {
|
||||
@@ -203,6 +205,51 @@ export const WithTooltip: StoryFn = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const WithSrOnlyElement: StoryFn = () => (
|
||||
<Modal open width={300} header={{ heading: "Simple header" }}>
|
||||
<Modal.Body>
|
||||
<VStack gap="16">
|
||||
<BodyLong>
|
||||
The modal body needs to have position:relative to make sr-only
|
||||
elements position themselves correctly when the modal body is
|
||||
overflowing (i.e. has a scrollbar).
|
||||
</BodyLong>
|
||||
<BodyLong>
|
||||
If the modal body (and other parents of the sr-only element inside the
|
||||
modal body) does not have position:relative (or equivalent), the
|
||||
sr-only element is pushed downwards relative to the height of the
|
||||
overflowing content.
|
||||
</BodyLong>
|
||||
<BodyLong>
|
||||
In the most extreme cases, an additional scrollbar appears bc. the
|
||||
dialog element (parent of modal body) starts overflowing. (We have
|
||||
overflow:auto on the dialog element in case you are on a small screen
|
||||
and header/footer has a lot of content.)
|
||||
</BodyLong>
|
||||
<BodyLong>Example: CheckboxGroup with a hidden legend:</BodyLong>
|
||||
<CheckboxGroup legend="What fruits do you like?" hideLegend>
|
||||
<Checkbox>Banana</Checkbox>
|
||||
<Checkbox>Apple</Checkbox>
|
||||
<Checkbox>Orange</Checkbox>
|
||||
<Checkbox>Pear</Checkbox>
|
||||
</CheckboxGroup>
|
||||
<BodyLong>
|
||||
Try to remove position:relative from the modal body and see what
|
||||
happens. (Use DevTools to find the legend element.) You might need to
|
||||
make the viewport shorter to get the full effect (double scrollbar).
|
||||
</BodyLong>
|
||||
<BodyLong>
|
||||
Note: The large amount of gap has been added on purpose to force
|
||||
overflow.
|
||||
</BodyLong>
|
||||
</VStack>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary">Dummy button</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
export const ChromaticViewportTesting: StoryFn = () => (
|
||||
<div id="modal-story-wrapper" style={{ width: "100vw", height: "100vh" }}>
|
||||
<style>{`#storybook-root { padding: 0 !important }`}</style>
|
||||
|
||||
@@ -184,9 +184,10 @@ export const Popover = forwardRef<HTMLDivElement, PopoverProps>(
|
||||
(e: FocusEvent) => {
|
||||
if (
|
||||
e.target instanceof HTMLElement &&
|
||||
![anchorEl, refs?.floating?.current].some((element) =>
|
||||
![anchorEl, refs.floating.current].some((element) =>
|
||||
element?.contains(e.target as Node)
|
||||
)
|
||||
) &&
|
||||
!e.target.contains(refs.floating.current)
|
||||
) {
|
||||
open && onClose();
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Popover } from "../index";
|
||||
import { Button } from "../button";
|
||||
import { Popover } from "../index";
|
||||
|
||||
const placements = [
|
||||
"top",
|
||||
@@ -47,9 +47,9 @@ export default {
|
||||
|
||||
export const Default = (props: any) => {
|
||||
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>();
|
||||
const [open, setOpen] = useState<boolean>(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<div>
|
||||
<div tabIndex={-1}>
|
||||
<Button ref={(el) => setAnchorEl(el)} onClick={() => setOpen((x) => !x)}>
|
||||
Open
|
||||
</Button>
|
||||
@@ -57,10 +57,12 @@ export const Default = (props: any) => {
|
||||
{...props}
|
||||
open={props.open ?? open}
|
||||
anchorEl={anchorEl}
|
||||
onClose={(e) => setOpen(e)}
|
||||
onClose={() => setOpen(false)}
|
||||
>
|
||||
<Popover.Content>Velit in consequat</Popover.Content>
|
||||
</Popover>
|
||||
|
||||
<Button variant="secondary">Another button</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
+2
-2
@@ -2,7 +2,7 @@ import "react";
|
||||
|
||||
declare module "react" {
|
||||
interface CSSProperties {
|
||||
[key: `--ac-${string}`]: string | undefined;
|
||||
[key: `--__ac-${string}`]: string | undefined;
|
||||
[key: `--ac-${string}`]: string | number | undefined;
|
||||
[key: `--__ac-${string}`]: string | number | undefined;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { forwardRef, useState } from "react";
|
||||
import cl from "clsx";
|
||||
import { BodyLong } from "../typography";
|
||||
import React, { forwardRef, useState } from "react";
|
||||
import { ChevronDownIcon } from "@navikt/aksel-icons";
|
||||
import { BodyLong } from "../typography";
|
||||
|
||||
export interface ReadMoreProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
@@ -15,17 +15,17 @@ export interface ReadMoreProps
|
||||
header: React.ReactNode;
|
||||
/**
|
||||
* Opens component if 'true', closes if 'false'
|
||||
* Using this props removes automatic control of open-state
|
||||
* Using this prop removes automatic control of open-state
|
||||
*/
|
||||
open?: boolean;
|
||||
/**
|
||||
* Defaults the accordion to opened state
|
||||
* Initially open
|
||||
* @default false
|
||||
*/
|
||||
defaultOpen?: boolean;
|
||||
/**
|
||||
* Changes fontsize for content
|
||||
* @default false
|
||||
* @default medium
|
||||
*/
|
||||
size?: "medium" | "small";
|
||||
}
|
||||
|
||||
@@ -81,11 +81,8 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
const onRowClick = (e) => {
|
||||
if (e.target.nodeName === "TD" || e.target.nodeName === "TH") {
|
||||
expansionHandler(e);
|
||||
}
|
||||
};
|
||||
const onRowClick = (e) =>
|
||||
!isInteractiveTarget(e.target) && expansionHandler(e);
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -143,4 +140,19 @@ export const ExpandableRow: ExpandableRowType = forwardRef(
|
||||
}
|
||||
);
|
||||
|
||||
function isInteractiveTarget(elm: HTMLElement) {
|
||||
if (elm.nodeName === "TD" || elm.nodeName === "TH" || !elm.parentElement) {
|
||||
return false;
|
||||
}
|
||||
if (
|
||||
["BUTTON", "DETAILS", "LABEL", "SELECT", "TEXTAREA", "INPUT", "A"].includes(
|
||||
elm.nodeName
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isInteractiveTarget(elm.parentElement);
|
||||
}
|
||||
|
||||
export default ExpandableRow;
|
||||
@@ -57,7 +57,7 @@ export interface TableType
|
||||
* <Table.Header>
|
||||
* <Table.Row>
|
||||
* <Table.HeaderCell scope="col">Navn</Table.HeaderCell>
|
||||
* <Table.HeaderCell scope="col">Fødseslnr.</Table.HeaderCell>
|
||||
* <Table.HeaderCell scope="col">Fødselsnr.</Table.HeaderCell>
|
||||
* <Table.HeaderCell scope="col">Start</Table.HeaderCell>
|
||||
* </Table.Row>
|
||||
* </Table.Header>
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { expect, jest } from "@storybook/jest";
|
||||
import { userEvent, within } from "@storybook/testing-library";
|
||||
import React from "react";
|
||||
import { Table } from "../..";
|
||||
|
||||
export default {
|
||||
title: "ds-react/Table/Tests",
|
||||
component: Table,
|
||||
};
|
||||
|
||||
export const ClickableRowTest = {
|
||||
render: ({ onOpenChange }) => {
|
||||
return (
|
||||
<>
|
||||
<Table zebraStripes>
|
||||
<Table.Header>
|
||||
<Table.Row>
|
||||
<Table.HeaderCell aria-hidden />
|
||||
<Table.HeaderCell aria-hidden />
|
||||
<Table.HeaderCell aria-hidden />
|
||||
<Table.HeaderCell aria-hidden />
|
||||
<Table.HeaderCell aria-hidden />
|
||||
<Table.HeaderCell aria-hidden />
|
||||
</Table.Row>
|
||||
</Table.Header>
|
||||
<Table.Body>
|
||||
<Table.ExpandableRow
|
||||
content={<div>placeholder row 2</div>}
|
||||
togglePlacement="right"
|
||||
data-testid="row1"
|
||||
expandOnRowClick
|
||||
onOpenChange={onOpenChange}
|
||||
>
|
||||
<Table.DataCell>
|
||||
<div data-testid="cell1">Should be clickable</div>
|
||||
</Table.DataCell>
|
||||
<Table.DataCell data-testid="cell2">
|
||||
Should also be clickable
|
||||
</Table.DataCell>
|
||||
|
||||
<Table.DataCell>
|
||||
<button data-testid="cell3">Should not be clickable</button>
|
||||
</Table.DataCell>
|
||||
<Table.DataCell>
|
||||
<div>
|
||||
<div>
|
||||
<button data-testid="cell4">
|
||||
Nested should not be clickable
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Table.DataCell>
|
||||
<Table.DataCell>
|
||||
<div>
|
||||
<div>
|
||||
<button data-testid="cell4">
|
||||
<span>2x nested should not be clickable</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</Table.DataCell>
|
||||
</Table.ExpandableRow>
|
||||
</Table.Body>
|
||||
</Table>
|
||||
</>
|
||||
);
|
||||
},
|
||||
args: {
|
||||
onOpenChange: jest.fn(),
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
args.onOpenChange.mockClear();
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const cell1 = canvas.getByText("Should be clickable");
|
||||
const cell2 = canvas.getByText("Should also be clickable");
|
||||
const cell3 = canvas.getByText("Should not be clickable");
|
||||
const cell4 = canvas.getByText("Nested should not be clickable");
|
||||
const cell5 = canvas.getByText("2x nested should not be clickable");
|
||||
|
||||
await userEvent.click(cell1);
|
||||
expect(args.onOpenChange.mock.calls).toHaveLength(1);
|
||||
await userEvent.click(cell1);
|
||||
expect(args.onOpenChange.mock.calls).toHaveLength(2);
|
||||
|
||||
await userEvent.click(cell2);
|
||||
expect(args.onOpenChange.mock.calls).toHaveLength(3);
|
||||
await userEvent.click(cell2);
|
||||
expect(args.onOpenChange.mock.calls).toHaveLength(4);
|
||||
|
||||
await userEvent.click(cell3);
|
||||
expect(args.onOpenChange.mock.calls).toHaveLength(4);
|
||||
|
||||
await userEvent.click(cell4);
|
||||
expect(args.onOpenChange.mock.calls).toHaveLength(4);
|
||||
|
||||
await userEvent.click(cell5);
|
||||
expect(args.onOpenChange.mock.calls).toHaveLength(4);
|
||||
},
|
||||
};
|
||||
@@ -1,11 +1,28 @@
|
||||
import React from "react";
|
||||
|
||||
/**
|
||||
* This interface represents a component that can be overridden with different props and elements.
|
||||
* @template Component The type of the props for the component.
|
||||
* @template Element The type of the HTML element that the component renders.
|
||||
*/
|
||||
export interface OverridableComponent<Component, Element extends HTMLElement> {
|
||||
/**
|
||||
* This is a function type that takes props and returns a React functional component.
|
||||
* @param props The props for the component, which are a combination of the Component type and React.RefAttributes for the Element type.
|
||||
* @returns A React functional component.
|
||||
*/
|
||||
(props: Component & React.RefAttributes<Element>): ReturnType<React.FC>;
|
||||
|
||||
/**
|
||||
* This is a function type that takes props and returns a React functional component.
|
||||
* It allows for the 'as' prop to override the type of element that the component renders.
|
||||
* @template As The type of the element that the component should render as.
|
||||
* @param props The props for the component, which are a combination of the Component type, the 'as' prop, and any other props that are not part of the Component type or the 'as' prop.
|
||||
* @returns A React functional component.
|
||||
*/
|
||||
<As extends React.ElementType>(
|
||||
props: {
|
||||
as: As;
|
||||
as?: As;
|
||||
} & Component &
|
||||
Omit<React.ComponentPropsWithRef<As>, keyof Component | "as">
|
||||
): ReturnType<React.FC>;
|
||||
|
||||
@@ -1,19 +1,57 @@
|
||||
/* https://github.com/mui/material-ui/blob/master/packages/mui-base/src/TextareaAutosize/TextareaAutosize.js */
|
||||
/* https://github.com/mui/material-ui/blob/master/packages/mui-base/src/TextareaAutosize/TextareaAutosize.tsx */
|
||||
import React, { forwardRef, useEffect, useMemo, useRef, useState } from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import { debounce, mergeRefs, useClientLayoutEffect } from "../util";
|
||||
|
||||
type State = {
|
||||
outerHeightStyle: number;
|
||||
overflow?: boolean | undefined;
|
||||
};
|
||||
|
||||
const updateState = (
|
||||
prevState: State,
|
||||
newState: State,
|
||||
renders: React.MutableRefObject<number>
|
||||
) => {
|
||||
const { outerHeightStyle, overflow } = newState;
|
||||
// Need a large enough difference to update the height.
|
||||
// This prevents infinite rendering loop.
|
||||
if (
|
||||
renders.current < 20 &&
|
||||
((outerHeightStyle > 0 &&
|
||||
Math.abs((prevState.outerHeightStyle || 0) - outerHeightStyle) > 1) ||
|
||||
prevState.overflow !== overflow)
|
||||
) {
|
||||
renders.current += 1;
|
||||
return {
|
||||
overflow,
|
||||
outerHeightStyle,
|
||||
};
|
||||
}
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (renders.current === 20) {
|
||||
console.error(
|
||||
[
|
||||
"Textarea: Too many re-renders. The layout is unstable.",
|
||||
"TextareaAutosize limits the number of renders to prevent an infinite loop.",
|
||||
].join("\n")
|
||||
);
|
||||
}
|
||||
}
|
||||
return prevState;
|
||||
};
|
||||
|
||||
/**
|
||||
* https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/ownerDocument.ts
|
||||
* https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/ownerWindow.ts
|
||||
* https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/ownerDocument/ownerDocument.ts
|
||||
* https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/ownerWindow/ownerWindow.ts
|
||||
*/
|
||||
const ownerWindow = (node: Node | null): Window => {
|
||||
const ownerWindow = (node: Node | undefined): Window => {
|
||||
const doc = (node && node.ownerDocument) || document;
|
||||
return doc.defaultView || window;
|
||||
};
|
||||
|
||||
function getStyleValue(computedStyle, property) {
|
||||
return parseInt(computedStyle[property], 10) || 0;
|
||||
function getStyleValue(value: string) {
|
||||
return parseInt(value, 10) || 0;
|
||||
}
|
||||
|
||||
interface TextareaAutosizeProps
|
||||
@@ -30,34 +68,46 @@ interface TextareaAutosizeProps
|
||||
* @default 1
|
||||
*/
|
||||
minRows?: number;
|
||||
/**
|
||||
* If true, textarea will never get `overflow:hidden`
|
||||
*/
|
||||
autoScrollbar?: boolean;
|
||||
}
|
||||
|
||||
const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
|
||||
(
|
||||
{ className, onChange, maxRows, minRows = 1, style, value, ...other },
|
||||
{
|
||||
className,
|
||||
onChange,
|
||||
maxRows,
|
||||
minRows = 1,
|
||||
autoScrollbar,
|
||||
style,
|
||||
value,
|
||||
...other
|
||||
}: TextareaAutosizeProps,
|
||||
ref
|
||||
) => {
|
||||
const { current: isControlled } = useRef(value != null);
|
||||
const inputRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const inputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const handleRef = useMemo(() => mergeRefs([inputRef, ref]), [ref]);
|
||||
const shadowRef = useRef<HTMLTextAreaElement | null>(null);
|
||||
const shadowRef = useRef<HTMLTextAreaElement>(null);
|
||||
const renders = useRef(0);
|
||||
const [state, setState] = useState<any>({});
|
||||
const [state, setState] = useState<State>({ outerHeightStyle: 0 });
|
||||
|
||||
const getUpdatedState = React.useCallback(() => {
|
||||
if (!inputRef.current || !shadowRef.current) return;
|
||||
const input = inputRef.current;
|
||||
const input = inputRef.current!;
|
||||
const containerWindow = ownerWindow(input);
|
||||
const computedStyle = containerWindow.getComputedStyle(input);
|
||||
|
||||
// If input's width is shrunk and it's not visible, don't sync height.
|
||||
if (computedStyle.width === "0px") {
|
||||
return;
|
||||
return { outerHeightStyle: 0 };
|
||||
}
|
||||
|
||||
const inputShallow = shadowRef.current;
|
||||
const inputShallow = shadowRef.current!;
|
||||
inputShallow.style.width = computedStyle.width;
|
||||
inputShallow.value = input.value || other?.placeholder || "x";
|
||||
inputShallow.value = input.value || other.placeholder || "x";
|
||||
if (inputShallow.value.slice(-1) === "\n") {
|
||||
// Certain fonts which overflow the line height will cause the textarea
|
||||
// to report a different scrollHeight depending on whether the last line
|
||||
@@ -65,13 +115,13 @@ const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
|
||||
inputShallow.value += " ";
|
||||
}
|
||||
|
||||
const boxSizing = computedStyle["box-sizing"];
|
||||
const boxSizing = computedStyle.boxSizing;
|
||||
const padding =
|
||||
getStyleValue(computedStyle, "padding-bottom") +
|
||||
getStyleValue(computedStyle, "padding-top");
|
||||
getStyleValue(computedStyle.paddingBottom) +
|
||||
getStyleValue(computedStyle.paddingTop);
|
||||
const border =
|
||||
getStyleValue(computedStyle, "border-bottom-width") +
|
||||
getStyleValue(computedStyle, "border-top-width");
|
||||
getStyleValue(computedStyle.borderBottomWidth) +
|
||||
getStyleValue(computedStyle.borderTopWidth);
|
||||
|
||||
// The height of the inner content
|
||||
const innerHeight = inputShallow.scrollHeight - padding;
|
||||
@@ -97,7 +147,7 @@ const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
|
||||
const overflow = Math.abs(outerHeight - innerHeight) <= 1;
|
||||
|
||||
return { outerHeightStyle, overflow };
|
||||
}, [maxRows, minRows, other?.placeholder]);
|
||||
}, [maxRows, minRows, other.placeholder]);
|
||||
|
||||
const syncHeight = React.useCallback(() => {
|
||||
const newState = getUpdatedState();
|
||||
@@ -106,85 +156,61 @@ const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
|
||||
return;
|
||||
}
|
||||
|
||||
setState((prevState) => {
|
||||
return updateState(prevState, newState);
|
||||
});
|
||||
setState((prevState) => updateState(prevState, newState, renders));
|
||||
}, [getUpdatedState]);
|
||||
|
||||
const updateState = (prevState, newState) => {
|
||||
const { outerHeightStyle, overflow } = newState;
|
||||
// Need a large enough difference to update the height.
|
||||
// This prevents infinite rendering loop.
|
||||
if (
|
||||
renders.current < 20 &&
|
||||
((outerHeightStyle > 0 &&
|
||||
Math.abs((prevState.outerHeightStyle || 0) - outerHeightStyle) > 1) ||
|
||||
prevState.overflow !== overflow)
|
||||
) {
|
||||
renders.current += 1;
|
||||
return {
|
||||
overflow,
|
||||
outerHeightStyle,
|
||||
};
|
||||
}
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (renders.current === 20) {
|
||||
console.error(
|
||||
[
|
||||
"Textarea: Too many re-renders. The layout is unstable.",
|
||||
"TextareaAutosize limits the number of renders to prevent an infinite loop.",
|
||||
].join("\n")
|
||||
);
|
||||
useClientLayoutEffect(() => {
|
||||
const syncHeightWithFlushSync = () => {
|
||||
const newState = getUpdatedState();
|
||||
|
||||
if (isEmpty(newState)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
return prevState;
|
||||
};
|
||||
|
||||
const withFlushSync = () => {
|
||||
const newState = getUpdatedState();
|
||||
|
||||
if (isEmpty(newState)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// In React 18, state updates in a ResizeObserver's callback are happening after the paint which causes flickering
|
||||
// when doing some visual updates in it. Using flushSync ensures that the dom will be painted after the states updates happen
|
||||
// Related issue - https://github.com/facebook/react/issues/24331
|
||||
ReactDOM.flushSync(() => {
|
||||
setState((prevState) => {
|
||||
return updateState(prevState, newState);
|
||||
// In React 18, state updates in a ResizeObserver's callback are happening after
|
||||
// the paint, this leads to an infinite rendering.
|
||||
//
|
||||
// Using flushSync ensures that the states is updated before the next pain.
|
||||
// Related issue - https://github.com/facebook/react/issues/24331
|
||||
ReactDOM.flushSync(() => {
|
||||
setState((prevState) => updateState(prevState, newState, renders));
|
||||
});
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
const handleResize = debounce(() => {
|
||||
const handleResize = () => {
|
||||
renders.current = 0;
|
||||
|
||||
if (inputRef.current) {
|
||||
withFlushSync();
|
||||
if (inputRef.current?.style.height || inputRef.current?.style.width) {
|
||||
// User has resized manually
|
||||
if (inputRef.current?.style.overflow === "hidden") {
|
||||
setState((oldState) => ({ ...oldState, overflow: false })); // The state update isn't important, we just need to trigger a rerender
|
||||
}
|
||||
return;
|
||||
}
|
||||
});
|
||||
let resizeObserver: ResizeObserver;
|
||||
|
||||
syncHeightWithFlushSync();
|
||||
};
|
||||
|
||||
const debounceHandleResize = debounce(handleResize);
|
||||
const input = inputRef.current!;
|
||||
const containerWindow = ownerWindow(input);
|
||||
|
||||
containerWindow.addEventListener("resize", handleResize);
|
||||
containerWindow.addEventListener("resize", debounceHandleResize);
|
||||
|
||||
let resizeObserver: ResizeObserver;
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
resizeObserver = new ResizeObserver(handleResize);
|
||||
resizeObserver.observe(input);
|
||||
}
|
||||
|
||||
return () => {
|
||||
handleResize.clear();
|
||||
containerWindow.removeEventListener("resize", handleResize);
|
||||
debounceHandleResize.clear();
|
||||
containerWindow.removeEventListener("resize", debounceHandleResize);
|
||||
if (resizeObserver) {
|
||||
resizeObserver.disconnect();
|
||||
}
|
||||
};
|
||||
});
|
||||
}, [getUpdatedState]);
|
||||
|
||||
useClientLayoutEffect(() => {
|
||||
syncHeight();
|
||||
@@ -194,7 +220,7 @@ const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
|
||||
renders.current = 0;
|
||||
}, [value]);
|
||||
|
||||
const handleChange = (event) => {
|
||||
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
renders.current = 0;
|
||||
|
||||
if (!isControlled) {
|
||||
@@ -206,6 +232,20 @@ const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
|
||||
}
|
||||
};
|
||||
|
||||
const mainStyle: React.CSSProperties = {
|
||||
["--__ac-textarea-height"]: state.outerHeightStyle + "px",
|
||||
// Need a large enough difference to allow scrolling.
|
||||
// This prevents infinite rendering loop.
|
||||
overflow:
|
||||
state.overflow &&
|
||||
!autoScrollbar &&
|
||||
!inputRef.current?.style.height &&
|
||||
!inputRef.current?.style.width
|
||||
? "hidden"
|
||||
: undefined,
|
||||
...style,
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<textarea
|
||||
@@ -214,13 +254,7 @@ const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
|
||||
ref={handleRef}
|
||||
// Apply the rows prop to get a "correct" first SSR paint
|
||||
rows={minRows}
|
||||
style={{
|
||||
height: state.outerHeightStyle,
|
||||
// Need a large enough difference to allow scrolling.
|
||||
// This prevents infinite rendering loop.
|
||||
...(state.overflow ? { overflow: "hidden" } : {}),
|
||||
...style,
|
||||
}}
|
||||
style={mainStyle}
|
||||
{...other}
|
||||
className={className}
|
||||
/>
|
||||
@@ -250,12 +284,12 @@ const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
|
||||
}
|
||||
);
|
||||
|
||||
function isEmpty(obj: any) {
|
||||
function isEmpty(obj: State) {
|
||||
return (
|
||||
obj === undefined ||
|
||||
obj === null ||
|
||||
Object.keys(obj).length === 0 ||
|
||||
(obj?.outerHeightStyle === 0 && !obj?.overflow)
|
||||
(obj.outerHeightStyle === 0 && !obj.overflow)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
src/tokens.ts
|
||||
tailwind.config.js
|
||||
tailwind.config.js
|
||||
!tailwind.config.d.ts
|
||||
@@ -1,5 +1,29 @@
|
||||
# @navikt/ds-tailwind
|
||||
|
||||
## 5.11.4
|
||||
|
||||
## 5.11.3
|
||||
|
||||
## 5.11.2
|
||||
|
||||
## 5.11.1
|
||||
|
||||
## 5.11.0
|
||||
|
||||
## 5.10.4
|
||||
|
||||
## 5.10.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- typescript types for tailwind preset ([#2486](https://github.com/navikt/aksel/pull/2486))
|
||||
|
||||
## 5.10.2
|
||||
|
||||
## 5.10.1
|
||||
|
||||
## 5.10.0
|
||||
|
||||
## 5.9.2
|
||||
|
||||
## 5.9.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tailwind",
|
||||
"version": "5.9.2",
|
||||
"version": "5.11.4",
|
||||
"description": "Tailwind config based on @navikt/ds-tokens",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
@@ -8,9 +8,11 @@
|
||||
"design-tokens"
|
||||
],
|
||||
"main": "tailwind.config.js",
|
||||
"types": "tailwind.config.d.ts",
|
||||
"license": "MIT",
|
||||
"files": [
|
||||
"tailwind.config.js"
|
||||
"tailwind.config.js",
|
||||
"tailwind.config.d.ts"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "ts-node ./src",
|
||||
@@ -22,11 +24,12 @@
|
||||
"directory": "@navikt/core/tailwind"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^5.9.2",
|
||||
"@navikt/ds-tokens": "^5.11.4",
|
||||
"@types/jest": "^29.0.0",
|
||||
"color": "4.2.3",
|
||||
"jest": "^29.0.0",
|
||||
"lodash": "^4.17.21",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"ts-jest": "^29.0.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.1.6"
|
||||
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
declare const preset: Partial<Config>;
|
||||
|
||||
export default preset;
|
||||
@@ -1,5 +1,25 @@
|
||||
# @navikt/ds-tokens
|
||||
|
||||
## 5.11.4
|
||||
|
||||
## 5.11.3
|
||||
|
||||
## 5.11.2
|
||||
|
||||
## 5.11.1
|
||||
|
||||
## 5.11.0
|
||||
|
||||
## 5.10.4
|
||||
|
||||
## 5.10.3
|
||||
|
||||
## 5.10.2
|
||||
|
||||
## 5.10.1
|
||||
|
||||
## 5.10.0
|
||||
|
||||
## 5.9.2
|
||||
|
||||
## 5.9.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tokens",
|
||||
"version": "5.9.2",
|
||||
"version": "5.11.4",
|
||||
"description": "Design-tokens for NAV designsystem",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
|
||||
+379
-757
File diff suppressed because it is too large.
Load diff
@@ -35,7 +35,8 @@
|
||||
|
||||
aksel.nav.no/
|
||||
├── website # NEXT.JS app (v13)
|
||||
└── website/sanity # Sanity CMS-app (v3)
|
||||
├── website/sanity # Sanity CMS-app (v3)
|
||||
└── playroom # aksel.nav.no/sandbox
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ ENV NODE_ENV production
|
||||
|
||||
WORKDIR /app
|
||||
COPY aksel.nav.no/website/.next/standalone ./
|
||||
COPY aksel.nav.no/website/next-logger.config.js ./
|
||||
COPY aksel.nav.no/website/config/next-logger.config.js ./
|
||||
|
||||
ENV NODE_OPTIONS '-r next-logger'
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Det publiserte studioet finner man på [aksel.nav.no/admin](https://aksel.nav.no
|
||||
|
||||
### Bidrag
|
||||
|
||||
Branch `main` blir pusher rett til prod -> aksel.nav.no. For å utvikle nye features kan branch `next` brukes, som ved push publiseres til devmiljø https://aksel.ekstern.dev.nav.no/. Kan også publisere andre branches til dev ved bruk av workflow-dispatch.
|
||||
Branch `main` pusher rett til prod -> aksel.nav.no. For å utvikle nye features kan branch `next` brukes, som ved push publiseres til devmiljø https://aksel.ekstern.dev.nav.no/. Kan også publisere andre branches til dev ved bruk av workflow-dispatch.
|
||||
|
||||
## Kode-eksempler
|
||||
|
||||
|
||||
@@ -12,14 +12,15 @@
|
||||
"test": "yarn workspace website test"
|
||||
},
|
||||
"workspaces": [
|
||||
"website"
|
||||
"website",
|
||||
"playroom"
|
||||
],
|
||||
"dependencies": {
|
||||
"@navikt/aksel-icons": "^5.9.2",
|
||||
"@navikt/ds-css": "^5.9.2",
|
||||
"@navikt/ds-react": "^5.9.2",
|
||||
"@navikt/ds-tailwind": "^5.9.2",
|
||||
"@navikt/ds-tokens": "^5.9.2"
|
||||
"@navikt/aksel-icons": "^5.11.4",
|
||||
"@navikt/ds-css": "^5.11.4",
|
||||
"@navikt/ds-react": "^5.11.4",
|
||||
"@navikt/ds-tailwind": "^5.11.4",
|
||||
"@navikt/ds-tokens": "^5.11.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier-plugin-tailwindcss": "^0.2.3"
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# playroom
|
||||
|
||||
https://github.com/seek-oss/playroom
|
||||
|
||||
This is the playroom / code sandbox that is built as a standalone static app, and then it's added to the aksel website through an iframe.
|
||||
@@ -0,0 +1,49 @@
|
||||
/**
|
||||
* @type {import('@babel/core').TransformOptions}
|
||||
*/
|
||||
module.exports = function (api) {
|
||||
const envName = api.env();
|
||||
const development = envName === "development" || envName === "test";
|
||||
return {
|
||||
presets: [
|
||||
[
|
||||
"@babel/preset-env",
|
||||
{ useBuiltIns: "entry", corejs: "3.0", bugfixes: true },
|
||||
],
|
||||
["@babel/preset-typescript"],
|
||||
["@babel/preset-react", { development, useBuiltIns: true }],
|
||||
],
|
||||
assumptions: {
|
||||
setPublicClassFields: true,
|
||||
privateFieldsAsProperties: true,
|
||||
// nothing accesses `document.all`:
|
||||
noDocumentAll: true,
|
||||
// nothing relies on class constructors invoked without `new` throwing:
|
||||
noClassCalls: true,
|
||||
// nothing should be relying on tagged template strings being frozen:
|
||||
mutableTemplateObject: true,
|
||||
// nothing is relying on Function.prototype.length:
|
||||
ignoreFunctionLength: true,
|
||||
// nothing is relying on mutable re-exported bindings:
|
||||
constantReexports: true,
|
||||
// don't bother marking Module records non-enumerable:
|
||||
enumerableModuleMeta: true,
|
||||
// nothing uses [[Symbol.toPrimitive]]:
|
||||
// (see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toPrimitive)
|
||||
ignoreToPrimitiveHint: true,
|
||||
// nothing relies on spread copying Symbol keys: ({...{ [Symbol()]: 1 }})
|
||||
objectRestNoSymbols: true,
|
||||
// nothing relies on `new (() => {})` throwing:
|
||||
noNewArrows: true,
|
||||
// transpile object spread to assignment instead of defineProperty():
|
||||
setSpreadProperties: true,
|
||||
// nothing should be using custom iterator protocol:
|
||||
skipForOfIteratorClosing: true,
|
||||
// nothing inherits from a constructor function with explicit return value:
|
||||
superIsCallableConstructor: true,
|
||||
// nothing relies on CJS-transpiled namespace imports having all properties prior to module execution completing:
|
||||
noIncompleteNsImportDetection: true,
|
||||
},
|
||||
babelrcRoots: ["."],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "playroom",
|
||||
"scripts": {
|
||||
"start": "playroom start",
|
||||
"build:playroom": "playroom build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/preset-env": "^7.23.2",
|
||||
"@babel/preset-react": "^7.22.15",
|
||||
"@babel/preset-typescript": "^7.23.2",
|
||||
"@navikt/ds-css": "^5.9.2",
|
||||
"@navikt/ds-react": "^5.9.2",
|
||||
"babel-loader": "^9.1.3",
|
||||
"babel-plugin-preval": "^5.1.0",
|
||||
"css-loader": "^6.8.1",
|
||||
"playroom": "^0.32.1",
|
||||
"style-loader": "^3.3.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Note: Hot module reloading for dev has been turned off for this environment
|
||||
// Why? We embed playroom in an iframe, and that iframe is loaded from the public asset folder
|
||||
// So we need to build into that public asset folder. Hence no dev mode and HMR
|
||||
// Workaround: restart the next dev server after each change to the playroom config and associated files.
|
||||
|
||||
const path = require("path");
|
||||
|
||||
module.exports = {
|
||||
baseUrl: "./",
|
||||
components: path.resolve("./src/components.ts"),
|
||||
outputPath: path.resolve("./dist"),
|
||||
// Optional:
|
||||
title: "Aksel Sandbox",
|
||||
snippets: path.resolve("./src/snippets.ts"),
|
||||
widths: [320, 480, 768, 1024, 1280, 1440],
|
||||
defaultWidths: [320, 1280], // will be supported if this PR is merged: https://github.com/seek-oss/playroom/pull/302
|
||||
port: 9000,
|
||||
frameComponent: path.resolve("./src/FrameComponent.tsx"),
|
||||
useScope: path.resolve("./src/useScope.tsx"),
|
||||
openBrowser: false,
|
||||
paramType: "search", // default is 'hash'
|
||||
iframeSandbox: "allow-scripts allow-same-origin",
|
||||
exampleCode: `
|
||||
<Heading>This is a sandbox!</Heading>
|
||||
<HStack gap="10">
|
||||
<Box> box 1</Box> <Box> box 2</Box> <Box> box 3</Box>
|
||||
</HStack>
|
||||
<Button>test</Button>
|
||||
<BodyShort>This is a bodyshort</BodyShort>
|
||||
`,
|
||||
webpackConfig: () => ({
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.(js|ts|tsx)$/,
|
||||
include: [path.resolve("./")],
|
||||
use: [
|
||||
{
|
||||
loader: require.resolve("babel-loader"),
|
||||
options: {
|
||||
presets: [
|
||||
[
|
||||
require.resolve("@babel/preset-env"),
|
||||
{ shippedProposals: true },
|
||||
],
|
||||
require.resolve("@babel/preset-react"),
|
||||
require.resolve("@babel/preset-typescript"),
|
||||
],
|
||||
plugins: [require.resolve("babel-plugin-preval")],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.css$/i,
|
||||
include: [require.resolve("@navikt/ds-css")],
|
||||
use: ["style-loader", "css-loader"],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
};
|
||||
@@ -0,0 +1,8 @@
|
||||
import "@navikt/ds-css";
|
||||
import * as React from "react";
|
||||
|
||||
const FrameComponent = ({ children }: { children: React.ReactNode }) => {
|
||||
return <div id="sandbox-wrapper">{children}</div>;
|
||||
};
|
||||
|
||||
export default FrameComponent;
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from "@navikt/aksel-icons";
|
||||
export * from "@navikt/ds-react";
|
||||
@@ -0,0 +1,29 @@
|
||||
const snippets = [
|
||||
{
|
||||
group: "Templates",
|
||||
name: "Single Component",
|
||||
code: `<Button variant="primary">Button</Button>`,
|
||||
},
|
||||
{
|
||||
group: "Templates",
|
||||
name: "Multiple Components",
|
||||
code: ` <Button variant="primary">First</Button>
|
||||
<Button>Second</Button>`,
|
||||
},
|
||||
{
|
||||
group: "Templates",
|
||||
name: "Component with state",
|
||||
code: `{
|
||||
(() => {
|
||||
const [count, setCount] = React.useState(0);
|
||||
|
||||
return (
|
||||
<Button variant="primary" onClick={() => setCount(count + 1)}>Count: {count}</Button>
|
||||
)
|
||||
})()
|
||||
}
|
||||
`,
|
||||
},
|
||||
];
|
||||
|
||||
export default snippets;
|
||||
@@ -0,0 +1,2 @@
|
||||
public/sandbox
|
||||
test-results/
|
||||
@@ -1,7 +1,7 @@
|
||||
import { loadCsf } from "@storybook/csf-tools";
|
||||
import type { StorybookConfig } from "@storybook/nextjs";
|
||||
import { readFileSync } from "fs";
|
||||
import { dirname, join } from "path";
|
||||
import { dirname, join, resolve } from "path";
|
||||
|
||||
/**
|
||||
* This function is used to resolve the absolute path of a package.
|
||||
@@ -65,5 +65,74 @@ const config: StorybookConfig = {
|
||||
docs: {
|
||||
autodocs: "tag",
|
||||
},
|
||||
webpackFinal: async (sbConfig: any) => {
|
||||
if (!sbConfig.resolve.alias) {
|
||||
return sbConfig;
|
||||
}
|
||||
// Add path aliases
|
||||
sbConfig.resolve.alias["@/sb-util"] = resolve(__dirname, "./utils.tsx");
|
||||
sbConfig.resolve.alias["@/types"] = resolve(
|
||||
__dirname,
|
||||
"../components/types/index.ts"
|
||||
);
|
||||
sbConfig.resolve.alias["@/sanity-block"] = resolve(
|
||||
__dirname,
|
||||
"../components/website-modules/SanityBlockContent.tsx"
|
||||
);
|
||||
sbConfig.resolve.alias["@/hooks"] = resolve(
|
||||
__dirname,
|
||||
"../components/hooks/"
|
||||
);
|
||||
sbConfig.resolve.alias["@/logging"] = resolve(
|
||||
__dirname,
|
||||
"../components/logging/index.ts"
|
||||
);
|
||||
sbConfig.resolve.alias["@/utils"] = resolve(
|
||||
__dirname,
|
||||
"../components/utils/index.ts"
|
||||
);
|
||||
sbConfig.resolve.alias["@/utils"] = resolve(
|
||||
__dirname,
|
||||
"../components/utils/index.ts"
|
||||
);
|
||||
sbConfig.resolve.alias["@/layout"] = resolve(
|
||||
__dirname,
|
||||
"../components/layout/"
|
||||
);
|
||||
sbConfig.resolve.alias["@/assets"] = resolve(
|
||||
__dirname,
|
||||
"../components/assets/"
|
||||
);
|
||||
sbConfig.resolve.alias["@/web"] = resolve(
|
||||
__dirname,
|
||||
"../components/website-modules/"
|
||||
);
|
||||
sbConfig.resolve.alias["@/cms"] = resolve(
|
||||
__dirname,
|
||||
"../components/sanity-modules/"
|
||||
);
|
||||
sbConfig.resolve.alias["@/error-boundary"] = resolve(
|
||||
__dirname,
|
||||
"../components/website-modules/ErrorBoundary.tsx"
|
||||
);
|
||||
sbConfig.resolve.alias["@/sanity/config"] = resolve(
|
||||
__dirname,
|
||||
"../sanity/config.ts"
|
||||
);
|
||||
sbConfig.resolve.alias["@/sanity/client.server"] = resolve(
|
||||
__dirname,
|
||||
"../sanity/interface/client.server.js"
|
||||
);
|
||||
sbConfig.resolve.alias["@/sanity/interface"] = resolve(
|
||||
__dirname,
|
||||
"../sanity/interface/interface.ts"
|
||||
);
|
||||
sbConfig.resolve.alias["@/sanity/queries"] = resolve(
|
||||
__dirname,
|
||||
"../sanity/interface/queries.js"
|
||||
);
|
||||
|
||||
return sbConfig;
|
||||
},
|
||||
};
|
||||
export default config;
|
||||
@@ -1,73 +0,0 @@
|
||||
import { getClient } from "@/sanity/client.server";
|
||||
import { useLiveQuery, LiveQueryProvider } from "next-sanity/preview";
|
||||
import { useMemo, ComponentType } from "react";
|
||||
import dynamic from "next/dynamic";
|
||||
import { useCheckAuth } from "components/website-modules/utils/useCheckAuth";
|
||||
|
||||
export const PreviewBanner = dynamic(
|
||||
() => import("components/website-modules/PreviewBanner"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
function PreviewProvider({ children }: { children: React.ReactNode }) {
|
||||
const client = useMemo(() => {
|
||||
return getClient().withConfig({
|
||||
useCdn: false,
|
||||
ignoreBrowserTokenWarning: true,
|
||||
perspective: "previewDrafts",
|
||||
});
|
||||
}, []);
|
||||
return <LiveQueryProvider client={client}>{children}</LiveQueryProvider>;
|
||||
}
|
||||
|
||||
function LiveQuery({
|
||||
comp: Comp,
|
||||
query,
|
||||
params,
|
||||
props,
|
||||
validUser,
|
||||
}: {
|
||||
comp: ComponentType;
|
||||
query: string;
|
||||
props: any;
|
||||
params?: any;
|
||||
validUser: boolean;
|
||||
}) {
|
||||
const [data, loading] = useLiveQuery(props, query, params);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PreviewBanner loading={loading} validUser={validUser} />
|
||||
<Comp {...props} {...data} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const WithPreview = ({
|
||||
comp,
|
||||
query,
|
||||
params,
|
||||
props,
|
||||
}: {
|
||||
comp;
|
||||
query: string;
|
||||
props: any;
|
||||
params?: any;
|
||||
}) => {
|
||||
const validUser = useCheckAuth();
|
||||
return (
|
||||
<PreviewProvider>
|
||||
<LiveQuery
|
||||
props={props}
|
||||
query={query}
|
||||
params={params}
|
||||
comp={comp}
|
||||
validUser={validUser}
|
||||
/>
|
||||
</PreviewProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default WithPreview;
|
||||
@@ -199,3 +199,20 @@ export const StorybookIcon = ({ ...props }) => (
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export const CodeSandboxLogo = ({ title, ...props }) => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
fill="none"
|
||||
viewBox="8 3 24 24"
|
||||
focusable="false"
|
||||
{...props}
|
||||
>
|
||||
<title>{title}</title>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M20.54 25a.681.681 0 00.158-.074l6.174-3.69c.734-.438 1.128-.885 1.128-2.084v-6.444c0-.248-.016-.447-.063-.629l-7.397 4.434zm-8.477-12.921l7.397 4.434V25a.514.514 0 01-.15-.074l-6.182-3.69c-.726-.438-1.128-.885-1.128-2.084v-6.444c0-.248.016-.447.063-.629zM20.004 7c.443 0 .9.143 1.359.406l.068.04 5.567 3.326c.155.088.284.177.394.282l.04.04-7.428 4.426-7.436-4.425c.11-.109.233-.198.378-.286l.063-.037 4.07-2.442h.002l1.488-.883c.434-.262.868-.415 1.296-.443l.071-.003z"
|
||||
></path>
|
||||
</svg>
|
||||
);
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
|
||||
const Logo = (props) => (
|
||||
const AkselLogo = (props) => (
|
||||
<svg
|
||||
width="119"
|
||||
height="26"
|
||||
@@ -43,4 +43,4 @@ const Logo = (props) => (
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default Logo;
|
||||
export default AkselLogo;
|
||||
@@ -1 +0,0 @@
|
||||
export * from "./Icons";
|
||||
File renamed without changes.
+2
-1
@@ -1,5 +1,5 @@
|
||||
import { dateStr } from "components/website-modules/utils/date-string";
|
||||
import { useEffect, useState } from "react";
|
||||
import { dateStr } from "@/utils";
|
||||
|
||||
export const useFormatedDate = (date: string) => {
|
||||
const [_date, setDate] = useState(null);
|
||||
@@ -8,5 +8,6 @@ export const useFormatedDate = (date: string) => {
|
||||
const handleDate = async () => setDate(await dateStr(date));
|
||||
handleDate();
|
||||
}, [date]);
|
||||
|
||||
return _date;
|
||||
};
|
||||
+1
-7
@@ -2,7 +2,7 @@ import { useEffect } from "react";
|
||||
|
||||
// https://stackoverflow.com/questions/38588346/anchor-a-tags-not-working-in-chrome-when-using/38588927#38588927
|
||||
// https://github.com/vercel/next.js/discussions/13134
|
||||
export function useScrollToHashOnPageLoad(): void {
|
||||
export function useHashScroll(): void {
|
||||
useEffect(() => {
|
||||
if (window.location.hash) {
|
||||
const hash = window.location.hash;
|
||||
@@ -13,9 +13,3 @@ export function useScrollToHashOnPageLoad(): void {
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
|
||||
export const removeEmojies = (s: string) =>
|
||||
s.replace(
|
||||
/([\u2700-\u27BF]|[\uE000-\uF8FF]|\uD83C[\uDC00-\uDFFF]|\uD83D[\uDC00-\uDFFF]|[\u2011-\u26FF]|\uD83E[\uDD10-\uDDFF])/g,
|
||||
""
|
||||
);
|
||||
File renamed without changes.
@@ -0,0 +1,13 @@
|
||||
import { createContext, useContext } from "react";
|
||||
|
||||
export const SanityDocIdContext = createContext<{ id?: string }>(null);
|
||||
|
||||
export function useSanityDocId() {
|
||||
const ctx = useContext(SanityDocIdContext);
|
||||
if (!ctx) {
|
||||
console.error(
|
||||
"useSanityDocId has to be used inside SanityDocIdContext.provider"
|
||||
);
|
||||
}
|
||||
return ctx;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from "./assets";
|
||||
export * from "./website-modules/BreadCrumbs";
|
||||
export * from "./website-modules/toc/TOC";
|
||||
export * from "./website-modules/utils";
|
||||
@@ -1,131 +1,92 @@
|
||||
import { amplitudeLogNavigation } from "@/logging";
|
||||
import { BodyShort, Heading, Link } from "@navikt/ds-react";
|
||||
import { FigmaIcon, GithubIcon, SlackIcon } from "components/assets";
|
||||
import Logo from "components/assets/Logo";
|
||||
import { FigmaIcon, GithubIcon, SlackIcon } from "@/assets/Icons";
|
||||
import AkselLogo from "@/assets/Logo";
|
||||
import { Box, HGrid, Page, VStack } from "@navikt/ds-react";
|
||||
import dynamic from "next/dynamic";
|
||||
/* import FooterForm from "./FooterForm"; */
|
||||
import LinkBlock from "./parts/LinkBlock";
|
||||
|
||||
export const EditButton = dynamic(
|
||||
() => import("components/website-modules/EditButton"),
|
||||
{
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
export const EditButton = dynamic(() => import("@/web/EditButton"), {
|
||||
ssr: false,
|
||||
});
|
||||
|
||||
/* FooterForm is disabled to better comply with policy */
|
||||
const Footer = () => {
|
||||
return (
|
||||
<footer
|
||||
id="aksel-footer"
|
||||
data-hj-suppress
|
||||
<Box
|
||||
as="footer"
|
||||
data-theme="dark"
|
||||
className="toc-ignore text-text-on-inverted bg-deepblue-800 relative flex w-full justify-center"
|
||||
id="aksel-footer"
|
||||
className="relative"
|
||||
background="surface-alt-3-strong"
|
||||
paddingBlock="12 16"
|
||||
>
|
||||
<div className="relative z-10 mx-auto grid w-full max-w-screen-2xl gap-12 px-4 pb-16 pt-12 md:grid-cols-2 md:px-6 lg:grid-cols-4 xl:grid-cols-4 xl:gap-6">
|
||||
<LogoBlock />
|
||||
<Snarveier />
|
||||
<SideLenker />
|
||||
<Kontakt />
|
||||
{/* <FooterForm /> */}
|
||||
</div>
|
||||
<EditButton />
|
||||
</footer>
|
||||
<Page.Block width="2xl" gutters>
|
||||
<HGrid columns={{ xs: 1, md: 2, lg: 4 }} gap={{ xs: "12", xl: "6" }}>
|
||||
<VStack gap="4">
|
||||
<AkselLogo className="fill-white" />
|
||||
<div>
|
||||
<p>© {new Date().getFullYear()} NAV</p>
|
||||
<p>Arbeids- og velferdsetaten</p>
|
||||
</div>
|
||||
</VStack>
|
||||
<LinkBlock
|
||||
heading="Snarveier"
|
||||
links={[
|
||||
{
|
||||
url: "/god-praksis/artikler/skriv-for-aksel",
|
||||
text: "Skriv for Aksel",
|
||||
},
|
||||
{
|
||||
url: "/prinsipper/brukeropplevelse",
|
||||
text: "Prinsipper for brukeropplevelse",
|
||||
},
|
||||
{ url: "https://sikkerhet.nav.no/", text: "Security Playbook" },
|
||||
{
|
||||
url: "https://etterlevelse.intern.nav.no/",
|
||||
text: "Etterlevelse",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<LinkBlock
|
||||
heading="Om nettstedet"
|
||||
links={[
|
||||
{
|
||||
url: "/god-praksis/artikler/om-aksel",
|
||||
text: "Hva er Aksel?",
|
||||
},
|
||||
{
|
||||
url: "/side/personvernerklaering",
|
||||
text: "Personvernerklæring",
|
||||
},
|
||||
{
|
||||
url: "/side/tilgjengelighetserklaring-for-aksel",
|
||||
text: "Tilgjengelighetserklæring",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<LinkBlock
|
||||
heading="Finn oss"
|
||||
links={[
|
||||
{
|
||||
url: "https://www.figma.com/@nav_aksel",
|
||||
text: "Figma",
|
||||
icon: <FigmaIcon />,
|
||||
},
|
||||
{
|
||||
url: "https://github.com/navikt/aksel",
|
||||
text: "Github",
|
||||
icon: <GithubIcon />,
|
||||
},
|
||||
{
|
||||
url: "https://nav-it.slack.com/archives/C7NE7A8UF",
|
||||
text: "Slack",
|
||||
icon: <SlackIcon />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</HGrid>
|
||||
</Page.Block>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
function LogoBlock() {
|
||||
return (
|
||||
<div>
|
||||
<Logo className="fill-white" />
|
||||
<p className="mt-4">© {new Date().getFullYear()} NAV</p>
|
||||
<p>Arbeids- og velferdsetaten</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Snarveier() {
|
||||
return (
|
||||
<div>
|
||||
<Heading level="2" size="xsmall">
|
||||
Snarveier
|
||||
</Heading>
|
||||
<BodyShort as="ul" className="mt-3 grid gap-3">
|
||||
<FooterLink href="/god-praksis/artikler/skriv-for-aksel">
|
||||
Skriv for Aksel
|
||||
</FooterLink>
|
||||
<FooterLink href="/prinsipper/brukeropplevelse">
|
||||
Prinsipper for brukeropplevelse
|
||||
</FooterLink>
|
||||
{/* <FooterLink href="https://identitet.nav.no/">Identitet</FooterLink> */}
|
||||
<FooterLink href="https://sikkerhet.nav.no/">
|
||||
Security Playbook
|
||||
</FooterLink>
|
||||
<FooterLink href="https://etterlevelse.intern.nav.no/">
|
||||
Etterlevelse
|
||||
</FooterLink>
|
||||
</BodyShort>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Kontakt() {
|
||||
return (
|
||||
<div>
|
||||
<Heading level="2" size="xsmall">
|
||||
Finn oss
|
||||
</Heading>
|
||||
<BodyShort as="ul" className="mt-3 grid gap-3">
|
||||
<FooterLink href="https://www.figma.com/@nav_aksel">
|
||||
<FigmaIcon />
|
||||
Figma
|
||||
</FooterLink>
|
||||
<FooterLink href="https://github.com/navikt/aksel">
|
||||
<GithubIcon />
|
||||
Github
|
||||
</FooterLink>
|
||||
<FooterLink href="https://nav-it.slack.com/archives/C7NE7A8UF">
|
||||
<SlackIcon />
|
||||
Slack
|
||||
</FooterLink>
|
||||
</BodyShort>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
function SideLenker() {
|
||||
return (
|
||||
<div>
|
||||
<Heading level="2" size="xsmall">
|
||||
Om nettstedet
|
||||
</Heading>
|
||||
<BodyShort as="ul" className="mt-3 grid gap-3">
|
||||
<FooterLink href="/god-praksis/artikler/om-aksel">
|
||||
Hva er Aksel?
|
||||
</FooterLink>
|
||||
<FooterLink href="/side/personvernerklaering">
|
||||
Personvernerklæring
|
||||
</FooterLink>
|
||||
<FooterLink href="/side/tilgjengelighetserklaring-for-aksel">
|
||||
Tilgjengelighetserklæring
|
||||
</FooterLink>
|
||||
</BodyShort>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FooterLink({ children, href }) {
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
className="text-text-on-inverted focus:shadow-focus focus:text-text-default flex w-fit items-center gap-1 underline hover:no-underline focus:bg-blue-200 focus:shadow-blue-200"
|
||||
href={href}
|
||||
onClick={(e) =>
|
||||
amplitudeLogNavigation("footer", e.currentTarget.getAttribute("href"))
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default Footer;
|
||||
@@ -1,104 +0,0 @@
|
||||
import { Button, Label, Textarea } from "@navikt/ds-react";
|
||||
import { IdContext } from "components/website-modules/utils/contexts/id-context";
|
||||
import { FeedbackT } from "@/types";
|
||||
import { useRouter } from "next/router";
|
||||
import { useContext, useEffect, useState } from "react";
|
||||
|
||||
const FooterForm = () => {
|
||||
const idCtx = useContext(IdContext);
|
||||
|
||||
const [contactForm, setContactForm] = useState({
|
||||
content: "",
|
||||
hasWritten: false,
|
||||
});
|
||||
|
||||
const [contentError, setContentError] = useState({ content: "" });
|
||||
const [sent, setSent] = useState({ status: false });
|
||||
|
||||
const { asPath, basePath } = useRouter();
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
let fail = false;
|
||||
if (!contactForm?.content || contactForm?.content.trim().length === 0) {
|
||||
setContentError({
|
||||
...contentError,
|
||||
content: "Melding kan ikke være tom. Fyll inn meldingen.",
|
||||
});
|
||||
fail = true;
|
||||
}
|
||||
|
||||
if (fail) return;
|
||||
setContentError({ content: "" });
|
||||
|
||||
const body: FeedbackT = {
|
||||
message: contactForm.content,
|
||||
url: `${basePath}${asPath}`,
|
||||
type: "footer",
|
||||
docId: idCtx?.id,
|
||||
};
|
||||
|
||||
fetch("/api/feedback", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
setSent({ status: true });
|
||||
setContactForm({ content: "", hasWritten: false });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
setSent({ status: false });
|
||||
setContactForm({ content: "", hasWritten: false });
|
||||
setContentError({ content: "" });
|
||||
}, [asPath]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
className="flex w-full max-w-md flex-col lg:col-span-2 xl:col-span-1"
|
||||
data-theme="dark"
|
||||
>
|
||||
<div aria-live="polite">
|
||||
{sent.status && (
|
||||
<Label spacing as="p">
|
||||
Meldingen er sendt til Aksel, takk for tilbakemeldingen!
|
||||
</Label>
|
||||
)}
|
||||
</div>
|
||||
{!sent.status && (
|
||||
<form onSubmit={(e) => handleSubmit(e)} className="w-full">
|
||||
<div className="mb-3 flex flex-col gap-4">
|
||||
<Textarea
|
||||
className="textarea-override"
|
||||
error={contentError.content}
|
||||
autoComplete="off"
|
||||
label="Gi oss en tilbakemelding"
|
||||
value={contactForm.content}
|
||||
onChange={(e) => {
|
||||
setContactForm({
|
||||
...contactForm,
|
||||
content: e.target.value,
|
||||
hasWritten: true,
|
||||
});
|
||||
e.target.value &&
|
||||
e.target.value.trim().length !== 0 &&
|
||||
setContentError({ ...contentError, content: "" });
|
||||
}}
|
||||
description="Ikke skriv inn personopplysninger"
|
||||
minRows={2}
|
||||
/>
|
||||
</div>
|
||||
<Button size="small">Send melding</Button>
|
||||
<style>{`.textarea-override {
|
||||
--ac-textarea-bg: var(--a-surface-default);
|
||||
--ac-textarea-text: var(--a-text-default);
|
||||
}`}</style>
|
||||
</form>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default FooterForm;
|
||||
@@ -0,0 +1,19 @@
|
||||
import { amplitudeLogNavigation } from "@/logging";
|
||||
import { Link } from "@navikt/ds-react";
|
||||
|
||||
function FooterLink({ children, href }) {
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
className="text-text-on-inverted focus:shadow-focus focus:text-text-default flex w-fit items-center gap-1 underline hover:no-underline focus:bg-blue-200 focus:shadow-blue-200"
|
||||
href={href}
|
||||
onClick={(e) =>
|
||||
amplitudeLogNavigation("footer", e.currentTarget.getAttribute("href"))
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
export default FooterLink;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { BodyShort, Heading } from "@navikt/ds-react";
|
||||
import FooterLink from "./FooterLink";
|
||||
|
||||
type LinkBlockPropsT = {
|
||||
heading: string;
|
||||
links: { url: string; text: string; icon?: React.ReactNode }[];
|
||||
};
|
||||
|
||||
function LinkBlock({ heading, links }: LinkBlockPropsT) {
|
||||
return (
|
||||
<div>
|
||||
<Heading level="2" size="xsmall">
|
||||
{heading}
|
||||
</Heading>
|
||||
<BodyShort as="ul" className="mt-3 grid gap-3">
|
||||
{links.map((link) => (
|
||||
<FooterLink key={link.url} href={link.url}>
|
||||
{link.icon}
|
||||
{link.text}
|
||||
</FooterLink>
|
||||
))}
|
||||
</BodyShort>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default LinkBlock;
|
||||
@@ -1,96 +0,0 @@
|
||||
import { MenuHamburgerIcon } from "@navikt/aksel-icons";
|
||||
|
||||
import { amplitudeLogNavigation } from "@/logging";
|
||||
import { Modal } from "@navikt/ds-react";
|
||||
import cl from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
const LinkElement = ({ name, href, onClick }) => {
|
||||
const { asPath } = useRouter();
|
||||
return (
|
||||
<li className="flex h-11 items-center">
|
||||
<Link
|
||||
href={href}
|
||||
passHref
|
||||
className={cl(
|
||||
"hover:bg-surface-action-subtle-hover focus-visible:shadow-focus-inset relative flex h-full w-full items-center rounded px-2 focus:outline-none",
|
||||
{
|
||||
"before:bg-surface-action-selected pl-4 font-semibold before:absolute before:left-0 before:h-6 before:w-1 before:rounded-full":
|
||||
asPath.startsWith(href),
|
||||
"": !asPath.startsWith(href),
|
||||
}
|
||||
)}
|
||||
onClick={(e) => {
|
||||
amplitudeLogNavigation(
|
||||
"header",
|
||||
e.currentTarget.getAttribute("href")
|
||||
);
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export const Hamburger = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<div className="mr-2 grid h-full place-content-center lg:hidden">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
aria-expanded={open}
|
||||
className="focus-visible:shadow-focus hover:bg-surface-neutral-subtle-hover ml-2 flex aspect-square items-center justify-center rounded px-3 py-3 focus:outline-none"
|
||||
>
|
||||
<MenuHamburgerIcon
|
||||
fontSize="2rem"
|
||||
className="pointer-events-none"
|
||||
aria-label="Åpne meny"
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
header={{ heading: "Meny" }}
|
||||
width="small"
|
||||
>
|
||||
<Modal.Body>
|
||||
<nav aria-label="hovedmeny">
|
||||
<ul>
|
||||
<LinkElement
|
||||
onClick={() => setOpen(false)}
|
||||
name="God praksis"
|
||||
href="/god-praksis"
|
||||
/>
|
||||
<LinkElement
|
||||
onClick={() => setOpen(false)}
|
||||
name="Grunnleggende"
|
||||
href="/grunnleggende"
|
||||
/>
|
||||
<LinkElement
|
||||
onClick={() => setOpen(false)}
|
||||
name="Ikoner"
|
||||
href="/ikoner"
|
||||
/>
|
||||
<LinkElement
|
||||
onClick={() => setOpen(false)}
|
||||
name="Komponenter"
|
||||
href="/komponenter"
|
||||
/>
|
||||
<LinkElement
|
||||
onClick={() => setOpen(false)}
|
||||
name="Blogg"
|
||||
href="/produktbloggen"
|
||||
/>
|
||||
</ul>
|
||||
</nav>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,118 +1,96 @@
|
||||
import AkselLogo from "@/assets/Logo";
|
||||
import { amplitudeLogNavigation } from "@/logging";
|
||||
import { MagnifyingGlassIcon } from "@navikt/aksel-icons";
|
||||
import { Button } from "@navikt/ds-react";
|
||||
import { Box, Button, HStack, Page, Show, Spacer } from "@navikt/ds-react";
|
||||
import cl from "clsx";
|
||||
import Logo from "components/assets/Logo";
|
||||
import { Hamburger } from "components/layout/header/Hamburger";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { Hamburger } from "./parts/Hamburger";
|
||||
import HeaderLink from "./parts/HeaderLink";
|
||||
|
||||
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();
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
href={href}
|
||||
prefetch={prefetch}
|
||||
className={cl(
|
||||
"text-deepblue-800 focus-visible:shadow-focus relative grid h-11 place-items-center rounded px-2 focus:outline-none",
|
||||
{
|
||||
"before:bg-deepblue-600 font-semibold before:absolute before:bottom-[1px] before:z-10 before:h-1 before:w-[calc(100%_-_16px)] before:rounded-full":
|
||||
asPath.startsWith(href),
|
||||
"hover:before:bg-border-subtle-hover before:absolute before:bottom-[1px] before:z-10 before:h-1 before:w-[calc(100%_-_16px)] before:rounded-full":
|
||||
!asPath.startsWith(href),
|
||||
}
|
||||
)}
|
||||
onClick={(e) =>
|
||||
amplitudeLogNavigation("header", e.currentTarget.getAttribute("href"))
|
||||
export const GlobalSearch = dynamic(
|
||||
() => import("../../website-modules/search/Search"),
|
||||
{
|
||||
loading: () => (
|
||||
<Button
|
||||
variant="primary"
|
||||
className="hover:bg-deepblue-700 bg-deepblue-600 h-11"
|
||||
aria-keyshortcuts="Control+b"
|
||||
icon={
|
||||
<MagnifyingGlassIcon
|
||||
className="pointer-events-none -mt-[1px] shrink-0 text-2xl"
|
||||
aria-label="Åpne meny"
|
||||
aria-hidden
|
||||
/>
|
||||
}
|
||||
iconPosition="left"
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
Søk
|
||||
</Button>
|
||||
),
|
||||
ssr: false,
|
||||
}
|
||||
);
|
||||
|
||||
export const Header = ({
|
||||
const Header = ({
|
||||
variant = "default",
|
||||
}: {
|
||||
variant?: "blogg" | "subtle" | "default" | "transparent";
|
||||
}) => {
|
||||
return (
|
||||
<>
|
||||
<header
|
||||
className={cl("z-20", {
|
||||
"bg-amber-50": variant === "blogg",
|
||||
"bg-surface-default": variant === "default",
|
||||
"bg-surface-subtle": variant === "subtle",
|
||||
"bg-surface-transparent": variant === "transparent",
|
||||
})}
|
||||
>
|
||||
<a className="skiplink" href="#hovedinnhold">
|
||||
Hopp til innhold
|
||||
</a>
|
||||
<header
|
||||
className={cl("h-header z-20", {
|
||||
"bg-[#FEFCE9]": variant === "blogg",
|
||||
"bg-surface-default": variant === "default",
|
||||
"bg-surface-subtle": variant === "subtle",
|
||||
"bg-surface-transparent": variant === "transparent",
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={cl(
|
||||
"mx-auto flex h-full max-w-screen-2xl items-center pr-4 lg:pr-6"
|
||||
)}
|
||||
>
|
||||
<div className="flex h-11 items-center pl-4 pr-4 sm:pl-4 sm:pr-6">
|
||||
<Link
|
||||
href="/"
|
||||
passHref
|
||||
onClick={(e) =>
|
||||
amplitudeLogNavigation(
|
||||
"header",
|
||||
e.currentTarget.getAttribute("href")
|
||||
)
|
||||
}
|
||||
className="focus-visible:shadow-focus grid h-full place-items-center rounded px-2 focus:outline-none"
|
||||
>
|
||||
<Logo className="text-deepblue-800" />
|
||||
<span className="sr-only">Aksel</span>
|
||||
</Link>
|
||||
</div>
|
||||
<nav
|
||||
className="ml-auto hidden h-full pr-2 lg:block lg:pr-8"
|
||||
aria-label="Hovedmeny"
|
||||
<Page.Block width="2xl">
|
||||
<div className="h-header flex items-center pr-4 lg:pr-6">
|
||||
<Link
|
||||
href="/"
|
||||
passHref
|
||||
onClick={(e) =>
|
||||
amplitudeLogNavigation(
|
||||
"header",
|
||||
e.currentTarget.getAttribute("href")
|
||||
)
|
||||
}
|
||||
className="focus-visible:shadow-focus mx-4 grid h-11 place-items-center rounded px-2 focus:outline-none sm:mr-6"
|
||||
>
|
||||
<ul className="hidden h-full items-center gap-2 md:flex">
|
||||
<LinkElement name="God praksis" href="/god-praksis" />
|
||||
<LinkElement name="Grunnleggende" href="/grunnleggende" />
|
||||
<LinkElement name="Ikoner" href="/ikoner" prefetch={false} />
|
||||
<LinkElement name="Komponenter" href="/komponenter" />
|
||||
<LinkElement name="Bloggen" href="/produktbloggen" />
|
||||
</ul>
|
||||
</nav>
|
||||
<div className="z-[1050] ml-auto mr-4 flex justify-center lg:ml-0 lg:mr-0">
|
||||
<GlobalSearch />
|
||||
</div>
|
||||
<AkselLogo className="text-deepblue-800" />
|
||||
<span className="sr-only">Aksel</span>
|
||||
</Link>
|
||||
|
||||
<Hamburger />
|
||||
<Spacer />
|
||||
<Show above="lg" asChild>
|
||||
<Box
|
||||
as="nav"
|
||||
paddingInline={{ xs: "0 2", lg: "0 8" }}
|
||||
aria-label="Hovedmeny"
|
||||
>
|
||||
<HStack as="ul" gap="2" align="center">
|
||||
<HeaderLink name="God praksis" href="/god-praksis" />
|
||||
<HeaderLink name="Grunnleggende" href="/grunnleggende" />
|
||||
<HeaderLink name="Ikoner" href="/ikoner" prefetch={false} />
|
||||
<HeaderLink name="Komponenter" href="/komponenter" />
|
||||
<HeaderLink name="Mønster & Maler" href="/monster-maler" />
|
||||
<HeaderLink name="Bloggen" href="/produktbloggen" />
|
||||
</HStack>
|
||||
</Box>
|
||||
</Show>
|
||||
<HStack align="center" gap="2">
|
||||
<GlobalSearch />
|
||||
<Hamburger />
|
||||
</HStack>
|
||||
</div>
|
||||
</header>
|
||||
</>
|
||||
</Page.Block>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
export default Header;
|
||||
@@ -0,0 +1,69 @@
|
||||
import { MenuHamburgerIcon } from "@navikt/aksel-icons";
|
||||
import { Button, Modal, Show } from "@navikt/ds-react";
|
||||
import { useState } from "react";
|
||||
import HamburgerLink from "./HamburgerLink";
|
||||
|
||||
export const Hamburger = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Show below="lg">
|
||||
<Button
|
||||
variant="tertiary-neutral"
|
||||
onClick={() => setOpen(!open)}
|
||||
aria-expanded={open}
|
||||
icon={
|
||||
<MenuHamburgerIcon
|
||||
fontSize="2rem"
|
||||
className="pointer-events-none"
|
||||
title="Åpne meny"
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
header={{ heading: "Meny" }}
|
||||
width="small"
|
||||
closeOnBackdropClick
|
||||
>
|
||||
<Modal.Body>
|
||||
<nav aria-label="hovedmeny">
|
||||
<ul>
|
||||
<HamburgerLink
|
||||
onClick={() => setOpen(false)}
|
||||
name="God praksis"
|
||||
href="/god-praksis"
|
||||
/>
|
||||
<HamburgerLink
|
||||
onClick={() => setOpen(false)}
|
||||
name="Grunnleggende"
|
||||
href="/grunnleggende"
|
||||
/>
|
||||
<HamburgerLink
|
||||
onClick={() => setOpen(false)}
|
||||
name="Ikoner"
|
||||
href="/ikoner"
|
||||
/>
|
||||
<HamburgerLink
|
||||
onClick={() => setOpen(false)}
|
||||
name="Komponenter"
|
||||
href="/komponenter"
|
||||
/>
|
||||
<HamburgerLink
|
||||
onClick={() => setOpen(false)}
|
||||
name="Mønster & Maler"
|
||||
href="/monster-maler"
|
||||
/>
|
||||
<HamburgerLink
|
||||
onClick={() => setOpen(false)}
|
||||
name="Blogg"
|
||||
href="/produktbloggen"
|
||||
/>
|
||||
</ul>
|
||||
</nav>
|
||||
</Modal.Body>
|
||||
</Modal>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,35 @@
|
||||
import { amplitudeLogNavigation } from "@/logging";
|
||||
import cl from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
function HamburgerLink({ name, href, onClick }) {
|
||||
const { asPath } = useRouter();
|
||||
return (
|
||||
<li className="my-2 flex h-11 items-center">
|
||||
<Link
|
||||
href={href}
|
||||
passHref
|
||||
className={cl(
|
||||
"hover:bg-surface-action-subtle-hover focus-visible:shadow-focus relative flex h-full w-full items-center rounded px-2 focus:outline-none",
|
||||
{
|
||||
"before:bg-surface-action-selected pl-4 font-semibold before:absolute before:left-0 before:h-6 before:w-1 before:rounded-full":
|
||||
asPath.startsWith(href),
|
||||
"": !asPath.startsWith(href),
|
||||
}
|
||||
)}
|
||||
onClick={(e) => {
|
||||
amplitudeLogNavigation(
|
||||
"header",
|
||||
e.currentTarget.getAttribute("href")
|
||||
);
|
||||
onClick();
|
||||
}}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default HamburgerLink;
|
||||
@@ -0,0 +1,32 @@
|
||||
import { amplitudeLogNavigation } from "@/logging";
|
||||
import cl from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
function HeaderLink({ name, href, prefetch = undefined }) {
|
||||
const { asPath } = useRouter();
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
href={href}
|
||||
prefetch={prefetch}
|
||||
className={cl(
|
||||
"text-deepblue-800 focus-visible:shadow-focus relative grid h-11 place-items-center rounded px-2 focus:outline-none",
|
||||
{
|
||||
"before:bg-deepblue-600 font-semibold before:absolute before:bottom-[1px] before:z-10 before:h-1 before:w-[calc(100%_-_16px)] before:rounded-full":
|
||||
asPath.startsWith(href),
|
||||
"hover:before:bg-border-subtle-hover before:absolute before:bottom-[1px] before:z-10 before:h-1 before:w-[calc(100%_-_16px)] before:rounded-full":
|
||||
!asPath.startsWith(href),
|
||||
}
|
||||
)}
|
||||
onClick={(e) =>
|
||||
amplitudeLogNavigation("header", e.currentTarget.getAttribute("href"))
|
||||
}
|
||||
>
|
||||
{name}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default HeaderLink;
|
||||
@@ -1 +0,0 @@
|
||||
export { default as Footer } from "./footer/Footer";
|
||||
@@ -1,136 +1,79 @@
|
||||
import { amplitudeLogNavigation } from "@/logging";
|
||||
import { AkselSidebarT } from "@/types";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
SparklesIcon,
|
||||
TestFlaskIcon,
|
||||
TrashIcon,
|
||||
} from "@navikt/aksel-icons";
|
||||
import { Detail } from "@navikt/ds-react";
|
||||
import cl from "clsx";
|
||||
import { SidebarT } from "@/types";
|
||||
import { StatusTag } from "@/web/StatusTag";
|
||||
import { Box, Label, Show } from "@navikt/ds-react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import { useSection } from "./useSection";
|
||||
|
||||
const NavItem = ({
|
||||
link,
|
||||
}: {
|
||||
link: { heading: string; slug: string; kategori: string; tag?: string };
|
||||
}) => {
|
||||
const { asPath } = useRouter();
|
||||
const isActive = asPath.split("#")[0] === `/${link.slug}`;
|
||||
|
||||
return (
|
||||
<li>
|
||||
<Link
|
||||
aria-current={isActive ? "page" : undefined}
|
||||
href={`/${link.slug}`}
|
||||
onClick={(e) => {
|
||||
amplitudeLogNavigation("meny", e.currentTarget.getAttribute("href"));
|
||||
}}
|
||||
className={cl(
|
||||
"hover:text-deepblue-800 focus-visible:shadow-focus text-medium pr-05 relative flex overflow-hidden py-1 pl-4 leading-snug before:rounded-full hover:before:transition-colors focus:outline-none focus-visible:z-10 focus-visible:rounded-sm",
|
||||
{
|
||||
"before:border-l-border-action-selected before:absolute before:left-0 before:top-1/2 before:h-6 before:-translate-y-1/2 before:border-l-[4px]":
|
||||
isActive,
|
||||
"text-deepblue-800 font-semibold": isActive,
|
||||
"text-text-subtle": !isActive,
|
||||
"before:absolute before:left-0 before:top-0 before:h-full before:border-l before:border-l-gray-200 hover:before:top-1/2 hover:before:h-6 hover:before:min-h-[70%] hover:before:-translate-y-1/2 hover:before:border-l-2 hover:before:border-l-gray-400":
|
||||
!isActive,
|
||||
}
|
||||
)}
|
||||
>
|
||||
{link.heading}
|
||||
<span className="ml-auto capitalize">
|
||||
{link?.tag === "beta" && (
|
||||
<TestFlaskIcon
|
||||
aria-hidden
|
||||
className="text-violet-800"
|
||||
fontSize="1.25rem"
|
||||
/>
|
||||
)}
|
||||
{link?.tag === "new" && (
|
||||
<SparklesIcon
|
||||
aria-hidden
|
||||
className="text-lightblue-700"
|
||||
fontSize="1.25rem"
|
||||
/>
|
||||
)}
|
||||
{link?.tag === "deprecated" && (
|
||||
<TrashIcon
|
||||
aria-hidden
|
||||
className="text-text-default"
|
||||
fontSize="1.25rem"
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
const Dropdown = ({
|
||||
title,
|
||||
links,
|
||||
}: {
|
||||
title: string;
|
||||
links: AkselSidebarT;
|
||||
}) => {
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<li className="peer w-full peer-data-[open=true]:mt-6" data-open={open}>
|
||||
<button
|
||||
onClick={() => setOpen((x) => !x)}
|
||||
className="min-h-8 text-text-subtle hover:text-deepblue-800 group z-10 flex w-full cursor-pointer items-center justify-between focus:outline-none"
|
||||
aria-expanded={open}
|
||||
>
|
||||
<Detail
|
||||
as="span"
|
||||
className="group-focus-visible:shadow-focus group-hover:bg-surface-neutral-subtle group-active:bg-surface-neutral-subtle-hover mt-6 flex w-full items-center justify-between rounded-sm pl-2 font-semibold transition-colors first:mt-0"
|
||||
>
|
||||
{title}
|
||||
<span className="flex h-6 w-6 items-center justify-center rounded">
|
||||
<ChevronDownIcon
|
||||
className={cl("text-base", { "rotate-180": open })}
|
||||
aria-hidden
|
||||
title={!open ? `åpne ${title}` : `lukk ${title}`}
|
||||
/>
|
||||
</span>
|
||||
</Detail>
|
||||
</button>
|
||||
|
||||
<ul hidden={!open} className="pl-2">
|
||||
{links.map((z) => (
|
||||
<NavItem link={z} key={z?.heading} />
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
};
|
||||
|
||||
export const Sidebar = ({
|
||||
const Sidebar = ({
|
||||
kategori,
|
||||
links,
|
||||
}: {
|
||||
kategori: "komponenter" | "grunnleggende" | "templates";
|
||||
links: AkselSidebarT;
|
||||
links: SidebarT;
|
||||
}) => {
|
||||
const sections = useSection({ kategori, links });
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="ds-sidebar"
|
||||
className="toc-ignore w-sidebar bg-surface-default z-[1002] hidden shrink-0 self-start overflow-x-auto pb-8 pl-6 pr-2 md:block"
|
||||
>
|
||||
<nav aria-label={kategori}>
|
||||
<ul>
|
||||
{sections.map((kat) => (
|
||||
<Dropdown links={kat.pages} title={kat.title} key={kat.title} />
|
||||
<Show asChild above="md">
|
||||
<Box
|
||||
paddingBlock="0 8"
|
||||
paddingInline="2"
|
||||
aria-label={kategori}
|
||||
as="nav"
|
||||
className="w-sidebar shrink-0 self-start"
|
||||
>
|
||||
<ul className="space-y-6">
|
||||
{links.map((section) => (
|
||||
<Section key={section.value} section={section} />
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</Box>
|
||||
</Show>
|
||||
);
|
||||
};
|
||||
|
||||
function Section({ section }: { section: SidebarT[number] }) {
|
||||
const { asPath } = useRouter();
|
||||
|
||||
const isActive = (slug: string) => {
|
||||
return asPath.split("#")[0] === `/${slug}`;
|
||||
};
|
||||
return (
|
||||
<li>
|
||||
<Label
|
||||
as="div"
|
||||
size="small"
|
||||
textColor="subtle"
|
||||
className="min-h-8 flex items-center px-4"
|
||||
>
|
||||
{section.title}
|
||||
</Label>
|
||||
<ul className="space-y-px pl-3">
|
||||
{section.pages.map((page) => (
|
||||
<li
|
||||
aria-current={isActive(page.slug) ? "page" : undefined}
|
||||
key={page.slug}
|
||||
data-active={isActive(page.slug)}
|
||||
className="rounded-medium hover:bg-surface-hover min-h-8 data-active:bg-surface-selected data-active:text-deepblue-800 data-active:shadow-xsmall data-active:font-semibold relative mx-2 flex items-center transition-[background-color] ease-out"
|
||||
>
|
||||
<Link
|
||||
prefetch={false}
|
||||
href={`/${page.slug}`}
|
||||
className="text-medium min-h-8 rounded-medium focus-visible:shadow-focus flex w-full items-center justify-between gap-2 break-words px-2 focus:outline-none focus-visible:z-10"
|
||||
onClick={(e) => {
|
||||
amplitudeLogNavigation(
|
||||
"meny",
|
||||
e.currentTarget.getAttribute("href")
|
||||
);
|
||||
}}
|
||||
>
|
||||
{page.heading}
|
||||
<StatusTag size="xsmall" status={page.tag} />
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
export default Sidebar;
|
||||
Loaded 100 of 681 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user