mirror of
https://github.com/navikt/aksel.git
synced 2026-10-13 09:12:03 +00:00
Compare commits
123
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 | ||
|
|
5fec1f24f4 | ||
|
|
c27c1cfe26 | ||
|
|
5a649d2282 | ||
|
|
baac9cc251 | ||
|
|
d924d523e4 | ||
|
|
fc5a77081d | ||
|
|
1f3477a3e9 | ||
|
|
925450cb73 | ||
|
|
534a6a3b40 | ||
|
|
51eee7f011 | ||
|
|
909ae6aed1 | ||
|
|
cd3859a91e | ||
|
|
338f14e7ac | ||
|
|
aed1d082c3 | ||
|
|
31e9226b55 | ||
|
|
fe15b4d275 | ||
|
|
28e095d5a2 | ||
|
|
43377841ba | ||
|
|
2976ee9626 | ||
|
|
23c135b437 | ||
|
|
7f0498f250 | ||
|
|
5075c8d0a6 | ||
|
|
792eeaba42 | ||
|
|
0c7fc04935 | ||
|
|
0bb339e54a | ||
|
|
f5eb3adaea | ||
|
|
77cf71f814 | ||
|
|
f8c12b0757 | ||
|
|
be8265e13e | ||
|
|
0c7d6fb461 | ||
|
|
b97ac03c5a | ||
|
|
0ddb589138 | ||
|
|
1440c99b96 | ||
|
|
f8cf39c906 | ||
|
|
158c032436 | ||
|
|
6d6e6443dd | ||
|
|
e12ea8694e | ||
|
|
2d4cb67643 | ||
|
|
f5533766c8 | ||
|
|
3d1b0ac204 | ||
|
|
a1678d3b19 | ||
|
|
7007d3c58e | ||
|
|
8524346044 | ||
|
|
d5d8898659 | ||
|
|
1d635e1dbd | ||
|
|
6b3932b172 | ||
|
|
4d6ae1ae42 | ||
|
|
6c29fa83dd | ||
|
|
a6b6733861 | ||
|
|
3e54bd4f43 | ||
|
|
ec205fbc5f | ||
|
|
d8a4097c82 | ||
|
|
3a3a53f823 | ||
|
|
3d3e907608 | ||
|
|
986d0e9740 | ||
|
|
50bb7d4ff8 |
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"
|
||||
|
||||
@@ -39,6 +39,7 @@ module.exports = {
|
||||
files: ["**/*.ts?(x)"],
|
||||
extends: ["plugin:@typescript-eslint/recommended"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-shadow": ["error", { hoist: "all" }], // TODO: Consider { builtinGlobals: true }
|
||||
"@typescript-eslint/no-explicit-any": "off", // Temporary
|
||||
},
|
||||
},
|
||||
@@ -46,6 +47,7 @@ module.exports = {
|
||||
files: ["**/*.test.*", "**/__tests__/*"],
|
||||
extends: ["plugin:jest/recommended", "plugin:testing-library/react"],
|
||||
},
|
||||
|
||||
{
|
||||
files: ["aksel.nav.no/website/**"],
|
||||
env: {
|
||||
@@ -77,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",
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
name: Eksempel på bruk
|
||||
description: Send et eksempel på ting fra Aksel som er brukt i det fri (tokens, komponenter, maler, etc)
|
||||
title: "[Eksempel på bruk]: "
|
||||
labels: ["dokumentasjon 📖"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Takk for at du bidrar til Aksel!
|
||||
- type: input
|
||||
id: title
|
||||
attributes:
|
||||
label: Lenke til side
|
||||
description: Lenke til siden hvor eksemplet er
|
||||
placeholder:
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: examples
|
||||
attributes:
|
||||
label: Forklaring
|
||||
description: Forklar om det er noe man må gjøre for å se eksemplet, eller hvor på siden det ligger
|
||||
@@ -2,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:
|
||||
|
||||
+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,5 +1,43 @@
|
||||
# @navikt/aksel-icons
|
||||
|
||||
## 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
|
||||
|
||||
## 5.9.0
|
||||
|
||||
## 5.8.0
|
||||
|
||||
## 5.7.6
|
||||
|
||||
## 5.7.5
|
||||
|
||||
## 5.7.4
|
||||
|
||||
## 5.7.3
|
||||
|
||||
## 5.7.2
|
||||
|
||||
## 5.7.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.7.2",
|
||||
"version": "5.11.3",
|
||||
"author": "Aksel | NAV",
|
||||
"homepage": "https://aksel.nav.no/ikoner",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
# @navikt/aksel-stylelint
|
||||
|
||||
## 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
|
||||
|
||||
## 5.9.0
|
||||
|
||||
## 5.8.0
|
||||
|
||||
## 5.7.6
|
||||
|
||||
## 5.7.5
|
||||
|
||||
## 5.7.4
|
||||
|
||||
## 5.7.3
|
||||
|
||||
## 5.7.2
|
||||
|
||||
## 5.7.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-stylelint",
|
||||
"version": "5.7.2",
|
||||
"version": "5.11.3",
|
||||
"author": "Aksel | NAV",
|
||||
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
|
||||
"repository": {
|
||||
@@ -35,8 +35,8 @@
|
||||
"dev": "yarn watch:lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-css": "^5.7.2",
|
||||
"@navikt/ds-tokens": "^5.7.2",
|
||||
"@navikt/ds-css": "^5.11.3",
|
||||
"@navikt/ds-tokens": "^5.11.3",
|
||||
"@types/jest": "^29.0.0",
|
||||
"concurrently": "7.2.1",
|
||||
"copyfiles": "2.4.1",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import stylelint from "stylelint";
|
||||
import valueParser from "postcss-value-parser";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { Node as PostCSSNode } from "postcss";
|
||||
import valueParser from "postcss-value-parser";
|
||||
import stylelint from "stylelint";
|
||||
|
||||
import { isCustomProperty, tokenExists } from "../../utils";
|
||||
|
||||
@@ -34,7 +34,6 @@ export const messages = stylelint.utils.ruleMessages(ruleName, {
|
||||
});
|
||||
|
||||
const checkDeclValue = (
|
||||
controlledPrefixes: string[],
|
||||
value: string,
|
||||
postcssResult: stylelint.PostcssResult,
|
||||
rootNode: PostCSSNode
|
||||
@@ -58,7 +57,6 @@ const checkDeclValue = (
|
||||
};
|
||||
|
||||
const checkDeclProp = (
|
||||
controlledPrefixes: string[],
|
||||
prop: string,
|
||||
postcssResult: stylelint.PostcssResult,
|
||||
rootNode: PostCSSNode
|
||||
@@ -81,8 +79,8 @@ const checkDeclProp = (
|
||||
const ruleFunction: stylelint.Rule = () => {
|
||||
return (postcssRoot, postcssResult) => {
|
||||
postcssRoot.walkDecls((node) => {
|
||||
checkDeclValue(controlledPrefixes, node.value, postcssResult, node);
|
||||
checkDeclProp(controlledPrefixes, node.prop, postcssResult, node);
|
||||
checkDeclValue(node.value, postcssResult, node);
|
||||
checkDeclProp(node.prop, postcssResult, node);
|
||||
});
|
||||
};
|
||||
};
|
||||
|
||||
@@ -92,16 +92,6 @@ const tokenJsonFile = "./tokens.json";
|
||||
|
||||
const allowedTokenNames = [];
|
||||
|
||||
export const addTokens = (
|
||||
tokenJSONFile: string,
|
||||
allowedTokenNames: string[]
|
||||
) => {
|
||||
const jsonFileBuffer = readFileSync(`${__dirname}/${tokenJSONFile}`);
|
||||
const fileString = jsonFileBuffer.toString();
|
||||
const flattened = flattenObject(JSON.parse(fileString));
|
||||
flattened.forEach((token) => allowedTokenNames.push(token));
|
||||
};
|
||||
|
||||
export const tokenExists = (
|
||||
controlledPrefixes: string[],
|
||||
inputToken: string
|
||||
@@ -110,7 +100,6 @@ export const tokenExists = (
|
||||
if (!allowedTokenNames.length) {
|
||||
const cssFileBuffer = readFileSync(`${__dirname}/${tokenCSSFile}`);
|
||||
const cssFileString = cssFileBuffer.toString();
|
||||
|
||||
valueParser(cssFileString).walk((node) => {
|
||||
if (
|
||||
node.type === "word" &&
|
||||
@@ -121,7 +110,9 @@ export const tokenExists = (
|
||||
}
|
||||
});
|
||||
|
||||
addTokens(tokenJsonFile, allowedTokenNames);
|
||||
const jsonFileBuffer = readFileSync(`${__dirname}/${tokenJsonFile}`);
|
||||
const flattened = flattenObject(JSON.parse(jsonFileBuffer.toString()));
|
||||
flattened.forEach((token) => allowedTokenNames.push(token));
|
||||
}
|
||||
|
||||
return allowedTokenNames.includes(inputToken);
|
||||
|
||||
@@ -1,5 +1,124 @@
|
||||
# @navikt/aksel
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-css@5.9.2
|
||||
|
||||
## 5.9.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`925450cb7`](https://github.com/navikt/aksel/commit/925450cb73cd792cd3b6ad3ebf9f38ba022ee4df)]:
|
||||
- @navikt/ds-css@5.9.1
|
||||
|
||||
## 5.9.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`51eee7f01`](https://github.com/navikt/aksel/commit/51eee7f011aa016973bb952313cf65a1c55aa382), [`51eee7f01`](https://github.com/navikt/aksel/commit/51eee7f011aa016973bb952313cf65a1c55aa382), [`51eee7f01`](https://github.com/navikt/aksel/commit/51eee7f011aa016973bb952313cf65a1c55aa382), [`51eee7f01`](https://github.com/navikt/aksel/commit/51eee7f011aa016973bb952313cf65a1c55aa382)]:
|
||||
- @navikt/ds-css@5.9.0
|
||||
|
||||
## 5.8.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`0c7fc0493`](https://github.com/navikt/aksel/commit/0c7fc04935b96946b8ed1178c700077b6a1fcefd)]:
|
||||
- @navikt/ds-css@5.8.0
|
||||
|
||||
## 5.7.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`6d6e6443d`](https://github.com/navikt/aksel/commit/6d6e6443dd00464183b03fad6f822e5f3efab100)]:
|
||||
- @navikt/ds-css@5.7.6
|
||||
|
||||
## 5.7.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`3d1b0ac20`](https://github.com/navikt/aksel/commit/3d1b0ac2041bbc289536fa5a0dc65b2170dfc6fa)]:
|
||||
- @navikt/ds-css@5.7.5
|
||||
|
||||
## 5.7.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-css@5.7.4
|
||||
|
||||
## 5.7.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`3d3e90760`](https://github.com/navikt/aksel/commit/3d3e9076088c5e72ed89fb97109b75dcf6e09d96), [`d8a4097c8`](https://github.com/navikt/aksel/commit/d8a4097c82ceee95bd216b36e1dc8eb99e75d7ee), [`a6b673386`](https://github.com/navikt/aksel/commit/a6b67338612b7944dc4e6b3d541244f521e94500)]:
|
||||
- @navikt/ds-css@5.7.3
|
||||
|
||||
## 5.7.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel",
|
||||
"version": "5.7.2",
|
||||
"version": "5.11.3",
|
||||
"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.7.2",
|
||||
"axios": "1.3.6",
|
||||
"@navikt/ds-css": "5.11.3",
|
||||
"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"
|
||||
|
||||
@@ -111,9 +111,9 @@ export function getMigrationNames() {
|
||||
export function getMigrationString() {
|
||||
let str = "";
|
||||
|
||||
Object.entries(migrations).forEach(([version, migrations]) => {
|
||||
Object.entries(migrations).forEach(([version, vMigrations]) => {
|
||||
str += `\n${chalk.underline(version)}\n`;
|
||||
migrations.forEach((migration) => {
|
||||
vMigrations.forEach((migration) => {
|
||||
str += `${chalk.blue(migration.value)}: ${migration.description}\n`;
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,10 +13,10 @@ export default function transformer(file, api) {
|
||||
function addMigrationTag(node) {
|
||||
const attributes = node.openingElement.attributes;
|
||||
const isMigrated = attributes.find(
|
||||
(node) =>
|
||||
node.type === "JSXAttribute" &&
|
||||
node.name.name === "data-version" &&
|
||||
node.value.value === "v1"
|
||||
(attr) =>
|
||||
attr.type === "JSXAttribute" &&
|
||||
attr.name.name === "data-version" &&
|
||||
attr.value.value === "v1"
|
||||
);
|
||||
|
||||
if (!isMigrated) {
|
||||
@@ -30,7 +30,7 @@ export default function transformer(file, api) {
|
||||
function addExplicitStandardProp(node) {
|
||||
const attributes = node.openingElement.attributes;
|
||||
const variant = attributes.find(
|
||||
(node) => node.type === "JSXAttribute" && node.name.name === "size"
|
||||
(attr) => attr.type === "JSXAttribute" && attr.name.name === "size"
|
||||
);
|
||||
|
||||
if (!variant) {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import type { JSCodeshift } from "jscodeshift";
|
||||
import { tokens } from "../../../tokens-map.mjs";
|
||||
import {
|
||||
getImportSpecifier,
|
||||
getImportSpecifierName,
|
||||
renameImportSpecifier,
|
||||
} from "../../../utils/imports";
|
||||
import { translateToken } from "../../../utils/translate-token";
|
||||
import { tokens } from "../../../tokens-map.mjs";
|
||||
import { getLineTerminator } from "../../../utils/lineterminator";
|
||||
import { translateToken } from "../../../utils/translate-token";
|
||||
|
||||
/**
|
||||
* @param {import('jscodeshift').FileInfo} file
|
||||
@@ -29,9 +29,9 @@ export default function transformer(file: JSCodeshift, api) {
|
||||
return src;
|
||||
}
|
||||
|
||||
tokens.forEach((x) => {
|
||||
const name = translateToken(x[0], "js");
|
||||
const out = translateToken(x[1], "js");
|
||||
tokens.forEach((token) => {
|
||||
const name = translateToken(token[0], "js");
|
||||
const out = translateToken(token[1], "js");
|
||||
|
||||
let foundName: string = "";
|
||||
|
||||
|
||||
@@ -1,5 +1,105 @@
|
||||
# @navikt/ds-css
|
||||
|
||||
## 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
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- :lipstick: Modal: Bedre håndtering av mobiler i landskapsmodus ([#2444](https://github.com/navikt/aksel/pull/2444))
|
||||
|
||||
## 5.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Datepicker/Monthpicker: Hvis man bruker komponentene i Modal vil Popover bli erstattet med Modal uansett om man er på desktop eller mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
|
||||
|
||||
- Datepicker/Monthpicker/Popover: Fjernet `bubbleEscape`-prop. ([#2419](https://github.com/navikt/aksel/pull/2419))
|
||||
|
||||
- useDatepicker/useMonthPicker/useRangedpicker: Fjernet `openOnFocus`-prop, kan nå bare åpnes ved klikk på date-knapp i input. ([#2419](https://github.com/navikt/aksel/pull/2419))
|
||||
|
||||
- Datepicker/Monthpicker: Bytter nå automatisk til Modalvisning på mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
|
||||
|
||||
## 5.8.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- :bug: Modal: Bedre støtte for Tooltip i Modal ([#2429](https://github.com/navikt/aksel/pull/2429))
|
||||
|
||||
## 5.7.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- :lipstick: Chips.Toggle: Ikon i uvalgt tilstand ([#2395](https://github.com/navikt/aksel/pull/2395))
|
||||
|
||||
## 5.7.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Checkbox, Radio: Transparent-border fungerer nå bedre på mørkere bakgrunner ([#2388](https://github.com/navikt/aksel/pull/2388))
|
||||
|
||||
## 5.7.4
|
||||
|
||||
## 5.7.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Ny spacing token på 0.375rem ([#2370](https://github.com/navikt/aksel/pull/2370))
|
||||
|
||||
- Select: Tokenstøtte for å endre chevronfarge sammen med tekst ([#2383](https://github.com/navikt/aksel/pull/2383))
|
||||
|
||||
- HelpText: Knapp er visuelt mindre. Klikkflate er fortsatt 24px ([#2376](https://github.com/navikt/aksel/pull/2376))
|
||||
|
||||
## 5.7.2
|
||||
|
||||
## 5.7.1
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
cursor: pointer;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--a-spacing-1);
|
||||
gap: var(--a-spacing-05);
|
||||
margin: 0;
|
||||
padding: 0 var(--a-spacing-3);
|
||||
text-decoration: none;
|
||||
@@ -97,6 +97,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.navds-chips--medium .navds-chips__toggle--with-checkmark {
|
||||
padding-left: var(--a-spacing-1-alt);
|
||||
}
|
||||
|
||||
.navds-chips--small .navds-chips__toggle--with-checkmark {
|
||||
padding-left: var(--a-spacing-1);
|
||||
}
|
||||
|
||||
.navds-chips--small .navds-chips__toggle-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.navds-chips__removable {
|
||||
gap: 0;
|
||||
}
|
||||
@@ -160,11 +173,11 @@
|
||||
}
|
||||
|
||||
.navds-chips--icon-left {
|
||||
padding-left: 0.375rem;
|
||||
padding-left: var(--a-spacing-1-alt);
|
||||
}
|
||||
|
||||
.navds-chips--icon-right {
|
||||
padding-right: 0.375rem;
|
||||
padding-right: var(--a-spacing-1-alt);
|
||||
}
|
||||
|
||||
.navds-chips--small .navds-chips--icon-right {
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -175,7 +175,7 @@
|
||||
}
|
||||
|
||||
.navds-copybutton--small > .navds-copybutton__content {
|
||||
gap: 0.375rem;
|
||||
gap: var(--a-spacing-1-alt);
|
||||
}
|
||||
|
||||
.navds-copybutton--xsmall > .navds-copybutton__content {
|
||||
|
||||
+26
-18
@@ -149,18 +149,6 @@
|
||||
width: fit-content;
|
||||
}
|
||||
|
||||
/* Focus layering */
|
||||
.navds-date__field-input:focus-visible,
|
||||
.navds-date__field-button {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-date__field-input:focus {
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.navds-date .rdp-day_selected,
|
||||
.navds-monthpicker__month--selected {
|
||||
color: var(--ac-date-selected-text, var(--a-text-on-action));
|
||||
@@ -201,11 +189,11 @@
|
||||
}
|
||||
|
||||
.navds-date__field-input {
|
||||
padding-right: var(--a-spacing-14);
|
||||
padding-right: var(--a-spacing-12);
|
||||
}
|
||||
|
||||
.navds-form-field--small .navds-date__field-input {
|
||||
padding-right: var(--a-spacing-10);
|
||||
padding-right: var(--a-spacing-8);
|
||||
}
|
||||
|
||||
/* Error-handling */
|
||||
@@ -282,10 +270,6 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.navds-date__popover:where(.navds-popover) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
/* Readonly */
|
||||
.navds-date__field--readonly .navds-date__field-button {
|
||||
cursor: default;
|
||||
@@ -310,11 +294,35 @@
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.navds-date__modal.navds-date {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navds-date__modal-body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
padding: var(--a-spacing-4);
|
||||
gap: var(--a-spacing-2);
|
||||
}
|
||||
|
||||
.navds-date__modal-body > .navds-date {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.navds-date__popover:where(.navds-popover) {
|
||||
border: none;
|
||||
}
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.navds-date {
|
||||
padding: var(--a-spacing-5) var(--a-spacing-4);
|
||||
}
|
||||
|
||||
.navds-date__modal-body {
|
||||
padding: var(--a-spacing-6);
|
||||
}
|
||||
|
||||
.navds-date__caption {
|
||||
gap: var(--a-spacing-2);
|
||||
}
|
||||
|
||||
@@ -31,11 +31,13 @@
|
||||
.navds-radio__label::before {
|
||||
content: "";
|
||||
background-color: var(--ac-radio-checkbox-bg, var(--a-surface-default));
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default));
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
border-radius: var(--a-border-radius-medium);
|
||||
border-radius: var(--a-border-radius-small);
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default));
|
||||
width: calc(1.5rem - 0.25rem);
|
||||
height: calc(1.5rem - 0.25rem);
|
||||
margin-block: 2px;
|
||||
margin-inline: 2px;
|
||||
}
|
||||
|
||||
.navds-radio__label::before {
|
||||
@@ -59,41 +61,42 @@
|
||||
|
||||
.navds-checkbox--small > .navds-checkbox__label,
|
||||
.navds-radio--small > .navds-radio__label {
|
||||
padding: 0.375rem 0;
|
||||
padding: var(--a-spacing-1-alt) 0;
|
||||
}
|
||||
|
||||
.navds-checkbox--small > .navds-checkbox__label::before,
|
||||
.navds-radio--small > .navds-radio__label::before {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
width: calc(1.25rem - 0.25rem);
|
||||
height: calc(1.25rem - 0.25rem);
|
||||
}
|
||||
|
||||
.navds-checkbox__input:focus-visible + .navds-checkbox__label::before,
|
||||
.navds-radio__input:focus-visible + .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default)), var(--a-shadow-focus);
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default)),
|
||||
0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)), 0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-checkbox__input:focus + .navds-checkbox__label::before,
|
||||
.navds-radio__input:focus + .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default)), var(--a-shadow-focus);
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default)), 0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-checkbox__input:hover:focus-visible + .navds-checkbox__label::before,
|
||||
.navds-radio__input:hover:focus-visible + .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)), var(--a-shadow-focus);
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)), 0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-checkbox__input:hover:focus + .navds-checkbox__label::before,
|
||||
.navds-radio__input:hover:focus + .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)), var(--a-shadow-focus);
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)), 0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-checkbox__input:indeterminate + .navds-checkbox__label::before {
|
||||
box-shadow: none;
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action));
|
||||
background-color: var(--ac-radio-checkbox-action, var(--a-surface-action));
|
||||
}
|
||||
|
||||
@@ -101,7 +104,7 @@
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
transform: translate(0.375rem, -50%);
|
||||
transform: translate(var(--a-spacing-1-alt), -50%);
|
||||
background-color: var(--ac-radio-checkbox-bg, var(--a-surface-default));
|
||||
width: 0.75rem;
|
||||
height: 0.25rem;
|
||||
@@ -114,7 +117,7 @@
|
||||
}
|
||||
|
||||
.navds-checkbox__input:checked + .navds-checkbox__label::before {
|
||||
box-shadow: none;
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action));
|
||||
background-color: var(--ac-radio-checkbox-action, var(--a-surface-action));
|
||||
}
|
||||
|
||||
@@ -126,7 +129,7 @@
|
||||
color: var(--ac-radio-checkbox-bg, var(--a-surface-default));
|
||||
position: absolute;
|
||||
height: 1.5rem;
|
||||
transform: translate(0.375rem);
|
||||
transform: translate(var(--a-spacing-1-alt));
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
@@ -146,31 +149,33 @@
|
||||
|
||||
.navds-checkbox__input:indeterminate:focus-visible + .navds-checkbox__label::before,
|
||||
.navds-checkbox__input:checked:focus-visible + .navds-checkbox__label::before {
|
||||
box-shadow: inset 0 0 0 1px var(--ac-radio-checkbox-bg, var(--a-surface-default)), var(--a-shadow-focus);
|
||||
box-shadow: 0 0 0 1px var(--ac-radio-checkbox-action, var(--a-surface-action)),
|
||||
0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)), 0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-checkbox__input:indeterminate:focus + .navds-checkbox__label::before,
|
||||
.navds-checkbox__input:checked:focus + .navds-checkbox__label::before {
|
||||
box-shadow: inset 0 0 0 1px var(--ac-radio-checkbox-bg, var(--a-surface-default)), var(--a-shadow-focus);
|
||||
box-shadow: 0 0 0 1px var(--ac-radio-checkbox-action, var(--a-surface-action)),
|
||||
0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)), 0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-radio__input:checked + .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
|
||||
inset 0 0 0 4px var(--ac-radio-checkbox-bg, var(--a-surface-default));
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
|
||||
inset 0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default));
|
||||
background-color: var(--ac-radio-checkbox-action, var(--a-surface-action));
|
||||
}
|
||||
|
||||
.navds-radio__input:checked:focus-visible + .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
|
||||
inset 0 0 0 4px var(--ac-radio-checkbox-bg, var(--a-surface-default)), var(--a-shadow-focus);
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
|
||||
inset 0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)), 0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-radio__input:checked:focus + .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
|
||||
inset 0 0 0 4px var(--ac-radio-checkbox-bg, var(--a-surface-default)), var(--a-shadow-focus);
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
|
||||
inset 0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)), 0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -182,13 +187,13 @@
|
||||
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus-visible)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio__input:hover:not(:disabled):not(:checked):not(:focus-visible) + .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action));
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action));
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus) + .navds-checkbox__label::before,
|
||||
.navds-radio__input:hover:not(:disabled):not(:checked):not(:focus) + .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action));
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,14 +206,14 @@
|
||||
> .navds-checkbox__input:not(:hover):not(:disabled):not(:checked):not(:indeterminate)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--error > .navds-radio__input:not(:hover):not(:disabled):not(:checked) + .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
|
||||
}
|
||||
|
||||
.navds-checkbox--error
|
||||
> .navds-checkbox__input:focus-visible:not(:hover):not(:disabled):not(:checked):not(:indeterminate)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--error > .navds-radio__input:focus-visible:not(:hover):not(:disabled):not(:checked) + .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), var(--a-shadow-focus);
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), 0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
@@ -216,7 +221,7 @@
|
||||
> .navds-checkbox__input:focus:not(:hover):not(:disabled):not(:checked):not(:indeterminate)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--error > .navds-radio__input:focus:not(:hover):not(:disabled):not(:checked) + .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), var(--a-shadow-focus);
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), 0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,7 +230,7 @@
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--error > .navds-radio__input:hover:not(:disabled):not(:checked):not(:focus-visible) + .navds-radio__label::before {
|
||||
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
|
||||
}
|
||||
|
||||
.navds-checkbox--error
|
||||
@@ -233,7 +238,7 @@
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--error > .navds-radio__input:focus-visible:hover:not(:disabled):not(:checked) + .navds-radio__label::before {
|
||||
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), var(--a-shadow-focus);
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), 0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
@@ -242,7 +247,7 @@
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--error > .navds-radio__input:hover:not(:disabled):not(:checked):not(:focus) + .navds-radio__label::before {
|
||||
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
|
||||
}
|
||||
|
||||
.navds-checkbox--error
|
||||
@@ -250,7 +255,7 @@
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--error > .navds-radio__input:focus:hover:not(:disabled):not(:checked) + .navds-radio__label::before {
|
||||
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
|
||||
box-shadow: inset 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), var(--a-shadow-focus);
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)), 0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +295,7 @@
|
||||
.navds-radio--readonly > .navds-radio__input:not(:disabled):not(:checked) + .navds-radio__label::before,
|
||||
.navds-radio--readonly > .navds-radio__input:hover:not(:checked):not(:focus-visible) + .navds-radio__label::before {
|
||||
background-color: var(--__ac-radio-checkbox-readonly-bg);
|
||||
box-shadow: inset 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
|
||||
box-shadow: 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
|
||||
}
|
||||
|
||||
.navds-checkbox--readonly > .navds-checkbox__input:focus-visible + .navds-checkbox__label::before,
|
||||
@@ -299,8 +304,8 @@
|
||||
}
|
||||
|
||||
.navds-checkbox--readonly > .navds-checkbox__input:checked + .navds-checkbox__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
|
||||
background-color: var(--__ac-radio-checkbox-readonly-bg);
|
||||
box-shadow: 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
|
||||
}
|
||||
|
||||
.navds-checkbox--readonly > .navds-checkbox__input:checked + .navds-checkbox__label > .navds-checkbox__icon {
|
||||
@@ -308,12 +313,12 @@
|
||||
}
|
||||
|
||||
.navds-radio--readonly > .navds-radio__input:checked + .navds-radio__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--__ac-radio-checkbox-readonly-border), inset 0 0 0 4px var(--__ac-radio-checkbox-readonly-bg);
|
||||
background-color: var(--a-icon-subtle);
|
||||
box-shadow: 0 0 0 2px var(--__ac-radio-checkbox-readonly-border), inset 0 0 0 2px var(--__ac-radio-checkbox-readonly-bg);
|
||||
}
|
||||
|
||||
.navds-checkbox--readonly > .navds-checkbox__input:indeterminate + .navds-checkbox__label::before {
|
||||
box-shadow: inset 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
|
||||
box-shadow: 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
|
||||
background-color: var(--__ac-radio-checkbox-readonly-bg);
|
||||
}
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@
|
||||
right: var(--a-spacing-2);
|
||||
pointer-events: none;
|
||||
align-self: center;
|
||||
color: var(--ac-select-text, var(--a-text-default));
|
||||
}
|
||||
|
||||
.navds-form-field--small .navds-select__input {
|
||||
@@ -51,7 +52,7 @@
|
||||
}
|
||||
|
||||
.navds-form-field--small .navds-select__chevron {
|
||||
right: 0.375rem;
|
||||
right: var(--a-spacing-1-alt);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
@@ -62,30 +64,54 @@
|
||||
}
|
||||
|
||||
.navds-form-field--small .navds-textarea__input {
|
||||
padding: 0.375rem;
|
||||
}
|
||||
|
||||
.navds-form-field--small .navds-textarea--counter.navds-textarea__input {
|
||||
padding-bottom: var(--a-spacing-7);
|
||||
padding: var(--a-spacing-1-alt);
|
||||
}
|
||||
|
||||
.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,6 +1,5 @@
|
||||
.navds-help-text__button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
background-color: transparent;
|
||||
@@ -9,17 +8,19 @@
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--ac-help-text-button-color, var(--a-surface-action));
|
||||
font-size: 1.5rem;
|
||||
font-size: 1.25rem;
|
||||
padding: calc(var(--a-spacing-1) / 2);
|
||||
}
|
||||
|
||||
.navds-help-text__button:focus-visible {
|
||||
box-shadow: 0 0 0 1px var(--a-surface-default), 0 0 0 4px var(--a-border-focus);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 1px var(--a-border-focus), inset 0 0 0 1px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-help-text__button:focus {
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 1px var(--a-surface-default), 0 0 0 4px var(--a-border-focus);
|
||||
box-shadow: 0 0 0 1px var(--a-border-focus), inset 0 0 0 1px var(--a-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
@@ -23,7 +23,6 @@
|
||||
.navds-modal--polyfilled {
|
||||
top: 50%;
|
||||
transform: translate(0, -50%);
|
||||
overflow: auto;
|
||||
|
||||
/* From polyfill (dialog-polyfill/dist/dialog-polyfill.css): */
|
||||
left: 0;
|
||||
@@ -33,6 +32,10 @@
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.navds-modal--polyfilled .navds-modal--polyfilled {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.navds-modal--polyfilled:not([open]) {
|
||||
display: none; /* from polyfill */
|
||||
}
|
||||
@@ -56,12 +59,17 @@
|
||||
|
||||
@media (min-width: 480px) {
|
||||
.navds-modal {
|
||||
max-width: calc((100% - 6px) - 2em);
|
||||
max-height: calc((100% - 6px) - 2em);
|
||||
max-width: calc(100% - 6px - 2em);
|
||||
}
|
||||
|
||||
.navds-modal--autowidth {
|
||||
max-width: min(700px, calc((100% - 6px) - 2em));
|
||||
max-width: min(700px, calc(100% - 6px - 2em));
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-height: 480px) {
|
||||
.navds-modal {
|
||||
max-height: calc(100% - 6px - 2em);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +135,14 @@
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* When Datepicker is used nested inside a Modal */
|
||||
.navds-modal--polyfilled .navds-modal--polyfilled.navds-date__nested-modal {
|
||||
min-width: fit-content;
|
||||
max-width: 100vw;
|
||||
max-height: 100vh;
|
||||
animation: none;
|
||||
}
|
||||
|
||||
@keyframes akselModalFadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "5.7.2",
|
||||
"version": "5.11.3",
|
||||
"description": "CSS for NAV Designsystem",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
@@ -27,7 +27,7 @@
|
||||
"css:get-version": "node config/get-version.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^5.7.2",
|
||||
"@navikt/ds-tokens": "^5.11.3",
|
||||
"cssnano": "6.0.0",
|
||||
"fast-glob": "3.2.11",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -52,8 +52,7 @@
|
||||
}
|
||||
|
||||
.navds-pagination--small .navds-pagination__prev-next:where(.navds-pagination--prev-next--with-text) {
|
||||
padding-left: 0.375rem;
|
||||
padding-right: 0.375rem;
|
||||
padding-inline: var(--a-spacing-1-alt);
|
||||
}
|
||||
|
||||
.navds-pagination--xsmall .navds-pagination__prev-next:where(.navds-pagination--prev-next--with-text) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -37,7 +37,7 @@
|
||||
}
|
||||
|
||||
.navds-tabs--small .navds-tabs__scroll-button {
|
||||
padding: 0.375rem var(--a-spacing-4);
|
||||
padding: var(--a-spacing-1-alt) var(--a-spacing-4);
|
||||
width: 2rem;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
|
||||
.navds-tabs__tab--small {
|
||||
min-height: 2rem;
|
||||
padding: 0.375rem var(--a-spacing-4);
|
||||
padding: var(--a-spacing-1-alt) var(--a-spacing-4);
|
||||
}
|
||||
|
||||
.navds-tabs__tab-icon--top,
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
.navds-tag--small {
|
||||
min-height: 1.5rem;
|
||||
padding: 0 0.375rem;
|
||||
padding: 0 var(--a-spacing-1-alt);
|
||||
}
|
||||
|
||||
.navds-tag--xsmall {
|
||||
|
||||
@@ -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,209 @@
|
||||
# @navikt/ds-react
|
||||
|
||||
## 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
|
||||
|
||||
- :bug: Datepicker lukker ikke lenger modalen den er i ([#2451](https://github.com/navikt/aksel/pull/2451))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.9.2
|
||||
- @navikt/aksel-icons@5.9.2
|
||||
|
||||
## 5.9.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.9.1
|
||||
- @navikt/aksel-icons@5.9.1
|
||||
|
||||
## 5.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Datepicker/Monthpicker: Hvis man bruker komponentene i Modal vil Popover bli erstattet med Modal uansett om man er på desktop eller mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
|
||||
|
||||
- Datepicker/Monthpicker/Popover: Fjernet `bubbleEscape`-prop. ([#2419](https://github.com/navikt/aksel/pull/2419))
|
||||
|
||||
- useDatepicker/useMonthPicker/useRangedpicker: Fjernet `openOnFocus`-prop, kan nå bare åpnes ved klikk på date-knapp i input. ([#2419](https://github.com/navikt/aksel/pull/2419))
|
||||
|
||||
- Datepicker/Monthpicker: Bytter nå automatisk til Modalvisning på mobil. ([#2419](https://github.com/navikt/aksel/pull/2419))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Added useVirtualFocus hook - used in Combobox for now ([#2394](https://github.com/navikt/aksel/pull/2394))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.9.0
|
||||
- @navikt/aksel-icons@5.9.0
|
||||
|
||||
## 5.8.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- :sparkles: Modal: Støtte for å lukke ved klikk utenfor ([#2386](https://github.com/navikt/aksel/pull/2386))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Autocomplete in combobox will not change formatting of the letters while being typed, but will use the casing of the autocompleted word when selecting the option. ([#2207](https://github.com/navikt/aksel/pull/2207))
|
||||
|
||||
- Accordion: Fikset dom-validering ved bruk av `div` i `button`. ([#2426](https://github.com/navikt/aksel/pull/2426))
|
||||
|
||||
- :bug: Modal: Bedre støtte for Tooltip i Modal ([#2429](https://github.com/navikt/aksel/pull/2429))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.8.0
|
||||
- @navikt/aksel-icons@5.8.0
|
||||
|
||||
## 5.7.6
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- :lipstick: Chips.Toggle: Ikon i uvalgt tilstand ([#2395](https://github.com/navikt/aksel/pull/2395))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.7.6
|
||||
- @navikt/aksel-icons@5.7.6
|
||||
|
||||
## 5.7.5
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Loader: Har nå riktig `ref`-type. ([#2391](https://github.com/navikt/aksel/pull/2391))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.7.5
|
||||
- @navikt/aksel-icons@5.7.5
|
||||
|
||||
## 5.7.4
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Datepicker: Fungerer nå bedre i Modal ([#2400](https://github.com/navikt/aksel/pull/2400))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@5.7.4
|
||||
- @navikt/aksel-icons@5.7.4
|
||||
|
||||
## 5.7.3
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Forms: Fikset bug der size='small' ikke oppdaterte typografi. ([#2372](https://github.com/navikt/aksel/pull/2372))
|
||||
|
||||
- HelpText: Knapp er visuelt mindre. Klikkflate er fortsatt 24px ([#2376](https://github.com/navikt/aksel/pull/2376))
|
||||
|
||||
- Updated dependencies [[`3d3e90760`](https://github.com/navikt/aksel/commit/3d3e9076088c5e72ed89fb97109b75dcf6e09d96)]:
|
||||
- @navikt/ds-tokens@5.7.3
|
||||
- @navikt/aksel-icons@5.7.3
|
||||
|
||||
## 5.7.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "5.7.2",
|
||||
"version": "5.11.3",
|
||||
"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.7.2",
|
||||
"@navikt/ds-tokens": "^5.7.2",
|
||||
"@navikt/aksel-icons": "^5.11.3",
|
||||
"@navikt/ds-tokens": "^5.11.3",
|
||||
"@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": {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { ChevronDownIcon } from "@navikt/aksel-icons";
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef, useContext } from "react";
|
||||
import { Heading } from "../typography";
|
||||
import { AccordionContext } from "./AccordionContext";
|
||||
import { AccordionItemContext } from "./AccordionItem";
|
||||
import { ChevronDownIcon } from "@navikt/aksel-icons";
|
||||
|
||||
export interface AccordionHeaderProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
@@ -37,13 +37,13 @@ const AccordionHeader = forwardRef<HTMLButtonElement, AccordionHeaderProps>(
|
||||
aria-expanded={itemContext.open}
|
||||
type="button"
|
||||
>
|
||||
<div className="navds-accordion__icon-wrapper">
|
||||
<span className="navds-accordion__icon-wrapper">
|
||||
<ChevronDownIcon
|
||||
className="navds-accordion__header-chevron"
|
||||
title="Vis mer"
|
||||
aria-hidden
|
||||
/>
|
||||
</div>
|
||||
</span>
|
||||
<Heading
|
||||
size={accordionContext?.headingSize ?? "small"}
|
||||
as="span"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface ToggleChipsProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: string;
|
||||
/**
|
||||
* Toggles aria-pressed and visual-changes
|
||||
* Toggles aria-pressed and visual changes
|
||||
*/
|
||||
selected?: boolean;
|
||||
/**
|
||||
@@ -44,28 +44,38 @@ export const ToggleChips: OverridableComponent<
|
||||
className={cl(
|
||||
"navds-chips__chip navds-chips__toggle",
|
||||
className,
|
||||
`navds-chips__toggle--${variant}`
|
||||
`navds-chips__toggle--${variant}`,
|
||||
{ "navds-chips__toggle--with-checkmark": checkmark }
|
||||
)}
|
||||
aria-pressed={selected}
|
||||
>
|
||||
{selected && checkmark && (
|
||||
{checkmark && (
|
||||
<svg
|
||||
aria-hidden
|
||||
className="navds-chips__toggle-icon"
|
||||
width="10"
|
||||
height="8"
|
||||
viewBox="0 0 10 8"
|
||||
width="1.25em"
|
||||
height="1.25em"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
focusable={false}
|
||||
role="img"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M9.51894 0.45851C9.81799 0.745107 9.8281 1.21987 9.5415 1.51893L3.7915 7.51893C3.51247 7.81009 3.05315 7.82848 2.75174 7.56056L0.501735 5.56056C0.192148 5.28537 0.164263 4.81132 0.439451 4.50173C0.71464 4.19214 1.18869 4.16426 1.49828 4.43944L3.20835 5.95951L8.45852 0.481072C8.74511 0.182015 9.21988 0.171913 9.51894 0.45851Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
{selected ? (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 18.125C14.4873 18.125 18.125 14.4873 18.125 10C18.125 5.51269 14.4873 1.875 10 1.875C5.51269 1.875 1.875 5.51269 1.875 10C1.875 14.4873 5.51269 18.125 10 18.125ZM14.128 7.72904C14.3695 7.44357 14.3339 7.01635 14.0485 6.7748C13.763 6.53326 13.3358 6.56886 13.0942 6.85432L8.60428 12.1606L6.41627 9.97263C6.15185 9.70822 5.72315 9.70822 5.45873 9.97263C5.19431 10.2371 5.19431 10.6658 5.45873 10.9302L8.16706 13.6385C8.30095 13.7724 8.48479 13.8441 8.67397 13.8362C8.86316 13.8284 9.0404 13.7416 9.16271 13.5971L14.128 7.72904Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
) : (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 3.125C6.20304 3.125 3.125 6.20304 3.125 10C3.125 13.797 6.20304 16.875 10 16.875C13.797 16.875 16.875 13.797 16.875 10C16.875 6.20304 13.797 3.125 10 3.125ZM1.875 10C1.875 5.51269 5.51269 1.875 10 1.875C14.4873 1.875 18.125 5.51269 18.125 10C18.125 14.4873 14.4873 18.125 10 18.125C5.51269 18.125 1.875 14.4873 1.875 10Z"
|
||||
fill="var(--a-border-default)"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
)}
|
||||
<span className="navds-chips__chip-text">{children}</span>
|
||||
|
||||
@@ -1,24 +1,12 @@
|
||||
import { Meta } from "@storybook/react";
|
||||
import React, { useState } from "react";
|
||||
import { Chips } from ".";
|
||||
|
||||
export default {
|
||||
const meta: Meta<typeof Chips> = {
|
||||
title: "ds-react/Chips",
|
||||
component: Chips,
|
||||
argTypes: {
|
||||
type: {
|
||||
control: {
|
||||
type: "radio",
|
||||
options: ["toggle", "removable"],
|
||||
},
|
||||
},
|
||||
size: {
|
||||
control: {
|
||||
type: "radio",
|
||||
options: ["medium", "small"],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
export default meta;
|
||||
|
||||
const options = ["Norsk", "Dansk", "Svensk", "Tysk", "Spansk"];
|
||||
|
||||
@@ -61,12 +49,22 @@ export const Default = (props) => {
|
||||
</Chips>
|
||||
);
|
||||
};
|
||||
Default.argTypes = {
|
||||
type: {
|
||||
control: { type: "radio" },
|
||||
options: ["toggle", "removable"],
|
||||
},
|
||||
size: {
|
||||
control: { type: "radio" },
|
||||
options: ["medium", "small"],
|
||||
},
|
||||
};
|
||||
|
||||
export const Toggle = () => {
|
||||
export const Toggle = ({ size }) => {
|
||||
const [selected, setSelected] = useState<number[]>([2, 4]);
|
||||
return (
|
||||
<div className="colgap">
|
||||
<Chips>
|
||||
<Chips size={size}>
|
||||
{options.map((c, y) => (
|
||||
<Chips.Toggle
|
||||
selected={selected.includes(y)}
|
||||
@@ -83,7 +81,7 @@ export const Toggle = () => {
|
||||
</Chips.Toggle>
|
||||
))}
|
||||
</Chips>
|
||||
<Chips>
|
||||
<Chips size={size}>
|
||||
{options.map((c, y) => (
|
||||
<Chips.Toggle
|
||||
variant="neutral"
|
||||
@@ -104,6 +102,12 @@ export const Toggle = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Toggle.argTypes = {
|
||||
size: {
|
||||
control: { type: "radio" },
|
||||
options: ["medium", "small"],
|
||||
},
|
||||
};
|
||||
|
||||
export const ToggleNoCheckmark = () => {
|
||||
const [selected, setSelected] = useState<number>(2);
|
||||
|
||||
@@ -13,13 +13,13 @@ interface DateContextContextProps {
|
||||
* Aria-connected ID
|
||||
*/
|
||||
ariaId?: string;
|
||||
/**
|
||||
* Flag for enabled-check
|
||||
*/
|
||||
defined: boolean;
|
||||
}
|
||||
|
||||
export const DateContext = createContext<DateContextContextProps>({
|
||||
open: false,
|
||||
onOpen: () => null,
|
||||
ariaId: undefined,
|
||||
});
|
||||
export const DateContext = createContext<DateContextContextProps | null>(null);
|
||||
|
||||
export const useDateInputContext = () => {
|
||||
const context = useContext(DateContext);
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import cl from "clsx";
|
||||
import isWeekend from "date-fns/isWeekend";
|
||||
import React, { forwardRef, useRef, useState } from "react";
|
||||
import React, { forwardRef, useMemo, useRef, useState } from "react";
|
||||
import { DateRange, DayPicker, isMatch } from "react-day-picker";
|
||||
import { Popover } from "../../popover";
|
||||
import { omit, useId } from "../../util";
|
||||
import { DatePickerInput } from "../DateInput";
|
||||
import { mergeRefs, omit, useId } from "../../util";
|
||||
import { DateContext } from "../context";
|
||||
import { DatePickerInput } from "../parts/DateInput";
|
||||
import { DateWrapper } from "../parts/DateWrapper";
|
||||
import { getLocaleFromString, labels } from "../utils";
|
||||
import DatePickerStandalone from "./DatePickerStandalone";
|
||||
import Caption from "./parts/Caption";
|
||||
@@ -79,7 +79,6 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
|
||||
onClose,
|
||||
onOpenToggle,
|
||||
strategy,
|
||||
bubbleEscape = false,
|
||||
onWeekNumberClick,
|
||||
...rest
|
||||
},
|
||||
@@ -89,6 +88,7 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
|
||||
const [open, setOpen] = useState(_open ?? false);
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const mergedRef = useMemo(() => mergeRefs([wrapperRef, ref]), [ref]);
|
||||
|
||||
const [selectedDates, setSelectedDates] = React.useState<
|
||||
Date | Date[] | DateRange | undefined
|
||||
@@ -97,19 +97,57 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
|
||||
const mode = rest.mode ?? ("single" as any);
|
||||
|
||||
/**
|
||||
* @param selected Date | Date[] | DateRange | undefined
|
||||
* @param newSelected Date | Date[] | DateRange | undefined
|
||||
*/
|
||||
const handleSelect = (selected) => {
|
||||
setSelectedDates(selected);
|
||||
const handleSelect = (newSelected) => {
|
||||
setSelectedDates(newSelected);
|
||||
|
||||
if (rest.mode === "single") {
|
||||
selected && (onClose?.() ?? setOpen(false));
|
||||
newSelected && (onClose?.() ?? setOpen(false));
|
||||
} else if (rest.mode === "range") {
|
||||
selected?.from && selected?.to && (onClose?.() ?? setOpen(false));
|
||||
newSelected?.from && newSelected?.to && (onClose?.() ?? setOpen(false));
|
||||
}
|
||||
rest?.onSelect?.(selected);
|
||||
rest?.onSelect?.(newSelected);
|
||||
};
|
||||
|
||||
const DatePickerComponent = (
|
||||
<DayPicker
|
||||
locale={getLocaleFromString(locale)}
|
||||
mode={mode}
|
||||
onSelect={handleSelect}
|
||||
selected={selected ?? selectedDates}
|
||||
components={{
|
||||
Caption: dropdownCaption ? DropdownCaption : Caption,
|
||||
Head: TableHead,
|
||||
HeadRow,
|
||||
WeekNumber,
|
||||
Row,
|
||||
Day: DayButton,
|
||||
}}
|
||||
className={cl("navds-date", className)}
|
||||
classNames={{
|
||||
vhidden: "navds-sr-only",
|
||||
}}
|
||||
disabled={(day) => {
|
||||
return (disableWeekends && isWeekend(day)) || isMatch(day, disabled);
|
||||
}}
|
||||
weekStartsOn={1}
|
||||
initialFocus={false}
|
||||
labels={labels as any}
|
||||
modifiers={{
|
||||
weekend: (day) => disableWeekends && isWeekend(day),
|
||||
}}
|
||||
modifiersClassNames={{
|
||||
weekend: "rdp-day__weekend",
|
||||
}}
|
||||
showWeekNumber={showWeekNumber}
|
||||
onWeekNumberClick={mode === "multiple" ? onWeekNumberClick : undefined}
|
||||
fixedWeeks
|
||||
showOutsideDays
|
||||
{...omit(rest, ["onSelect"])}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<DateContext.Provider
|
||||
value={{
|
||||
@@ -119,72 +157,27 @@ export const DatePicker = forwardRef<HTMLDivElement, DatePickerProps>(
|
||||
onOpenToggle?.();
|
||||
},
|
||||
ariaId,
|
||||
defined: true,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
ref={mergedRef}
|
||||
className={cl("navds-date__wrapper", wrapperClassName)}
|
||||
>
|
||||
{children}
|
||||
{(_open ?? open) && (
|
||||
<Popover
|
||||
arrow={false}
|
||||
anchorEl={wrapperRef.current}
|
||||
open={_open ?? open}
|
||||
onClose={() => {
|
||||
onClose?.() ?? setOpen(false);
|
||||
}}
|
||||
placement="bottom-start"
|
||||
id={ariaId}
|
||||
role="dialog"
|
||||
ref={ref}
|
||||
strategy={strategy}
|
||||
className="navds-date__popover"
|
||||
bubbleEscape={bubbleEscape}
|
||||
flip={false}
|
||||
>
|
||||
<DayPicker
|
||||
locale={getLocaleFromString(locale)}
|
||||
mode={mode}
|
||||
onSelect={handleSelect}
|
||||
selected={selected ?? selectedDates}
|
||||
components={{
|
||||
Caption: dropdownCaption ? DropdownCaption : Caption,
|
||||
Head: TableHead,
|
||||
HeadRow,
|
||||
WeekNumber,
|
||||
Row,
|
||||
Day: DayButton,
|
||||
}}
|
||||
className={cl("navds-date", className)}
|
||||
classNames={{
|
||||
vhidden: "navds-sr-only",
|
||||
}}
|
||||
disabled={(day) => {
|
||||
return (
|
||||
(disableWeekends && isWeekend(day)) ||
|
||||
isMatch(day, disabled)
|
||||
);
|
||||
}}
|
||||
weekStartsOn={1}
|
||||
initialFocus={false}
|
||||
labels={labels as any}
|
||||
modifiers={{
|
||||
weekend: (day) => disableWeekends && isWeekend(day),
|
||||
}}
|
||||
modifiersClassNames={{
|
||||
weekend: "rdp-day__weekend",
|
||||
}}
|
||||
showWeekNumber={showWeekNumber}
|
||||
onWeekNumberClick={
|
||||
mode === "multiple" ? onWeekNumberClick : undefined
|
||||
}
|
||||
fixedWeeks
|
||||
showOutsideDays
|
||||
{...omit(rest, ["onSelect"])}
|
||||
/>
|
||||
</Popover>
|
||||
)}
|
||||
<DateWrapper
|
||||
open={_open ?? open}
|
||||
anchor={wrapperRef.current}
|
||||
onClose={() => onClose?.() ?? setOpen(false)}
|
||||
locale={locale}
|
||||
variant={mode}
|
||||
popoverProps={{
|
||||
id: ariaId,
|
||||
strategy,
|
||||
}}
|
||||
>
|
||||
{DatePickerComponent}
|
||||
</DateWrapper>
|
||||
</div>
|
||||
</DateContext.Provider>
|
||||
);
|
||||
|
||||
@@ -71,11 +71,11 @@ export const DatePickerStandalone: DatePickerStandaloneType = forwardRef<
|
||||
const mode = rest.mode ?? ("single" as any);
|
||||
|
||||
/**
|
||||
* @param selected Date | Date[] | DateRange | undefined
|
||||
* @param newSelected Date | Date[] | DateRange | undefined
|
||||
*/
|
||||
const handleSelect = (selected) => {
|
||||
setSelectedDates(selected);
|
||||
onSelect?.(selected);
|
||||
const handleSelect = (newSelected) => {
|
||||
setSelectedDates(newSelected);
|
||||
onSelect?.(newSelected);
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
/* eslint-disable react-hooks/rules-of-hooks */
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import isSameDay from "date-fns/isSameDay";
|
||||
import React, { useId, useState } from "react";
|
||||
import { useDatepicker, useRangeDatepicker } from "..";
|
||||
import { Button, HGrid, VStack } from "../..";
|
||||
import { BodyLong, Button, HGrid, Modal, VStack } from "../..";
|
||||
import DatePicker, { DatePickerProps } from "./DatePicker";
|
||||
import isSameDay from "date-fns/isSameDay";
|
||||
|
||||
const disabledDays = [
|
||||
new Date("Oct 10 2022"),
|
||||
@@ -18,7 +18,6 @@ export default {
|
||||
|
||||
type DefaultStoryProps = DatePickerProps & {
|
||||
size: "medium" | "small";
|
||||
openOnFocus: boolean;
|
||||
inputfield: boolean;
|
||||
standalone: boolean;
|
||||
};
|
||||
@@ -30,13 +29,11 @@ export const Default: StoryObj<DefaultStoryProps> = {
|
||||
const rangeCtx = useRangeDatepicker({
|
||||
fromDate: new Date("Aug 23 2020"),
|
||||
toDate: new Date("Aug 23 2023"),
|
||||
openOnFocus: props.openOnFocus,
|
||||
});
|
||||
|
||||
const singleCtx = useDatepicker({
|
||||
fromDate: new Date("Aug 23 2020"),
|
||||
toDate: new Date("Aug 23 2023"),
|
||||
openOnFocus: props.openOnFocus,
|
||||
});
|
||||
|
||||
const newProps = {
|
||||
@@ -111,7 +108,6 @@ export const Default: StoryObj<DefaultStoryProps> = {
|
||||
disableWeekends: false,
|
||||
showWeekNumber: false,
|
||||
mode: "single",
|
||||
openOnFocus: true,
|
||||
inputfield: true,
|
||||
standalone: false,
|
||||
},
|
||||
@@ -186,21 +182,6 @@ export const UseRangedDatepicker = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const OpenOnFocus = () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
onDateChange: console.log,
|
||||
openOnFocus: false,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "1rem" }}>
|
||||
<DatePicker {...datepickerProps}>
|
||||
<DatePicker.Input {...inputProps} label="Velg dato" />
|
||||
</DatePicker>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const NB = () => (
|
||||
<DatePicker.Standalone locale="nb" today={new Date("2006-07-01")} />
|
||||
);
|
||||
@@ -333,15 +314,32 @@ export const Size = () => {
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
toDate: new Date("Feb 23 2024"),
|
||||
onDateChange: console.log,
|
||||
defaultSelected: new Date("Feb 23 2023"),
|
||||
});
|
||||
const { datepickerProps: d2, inputProps: i2 } = useDatepicker({
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
toDate: new Date("Feb 23 2024"),
|
||||
onDateChange: console.log,
|
||||
defaultSelected: new Date("Feb 23 2023"),
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", gap: "1rem" }}>
|
||||
<DatePicker {...datepickerProps} dropdownCaption>
|
||||
<DatePicker.Input size="medium" {...inputProps} label="Velg dato" />
|
||||
<DatePicker.Input
|
||||
placeholder="10/10/2023"
|
||||
size="medium"
|
||||
{...inputProps}
|
||||
label="Velg dato"
|
||||
/>
|
||||
</DatePicker>
|
||||
<DatePicker {...datepickerProps} dropdownCaption>
|
||||
<DatePicker.Input size="small" {...inputProps} label="Velg dato" />
|
||||
<DatePicker {...d2} dropdownCaption>
|
||||
<DatePicker.Input
|
||||
placeholder="10/10/2023"
|
||||
size="small"
|
||||
{...i2}
|
||||
label="Velg dato"
|
||||
/>
|
||||
</DatePicker>
|
||||
</div>
|
||||
);
|
||||
@@ -432,3 +430,43 @@ export const WeekDayClick = () => {
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalDemo = () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
toDate: new Date("Feb 23 2024"),
|
||||
onDateChange: console.log,
|
||||
});
|
||||
const [open, setOpen] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setOpen(!open)}>Open modal</Button>
|
||||
<Modal
|
||||
open={open}
|
||||
onClose={() => setOpen(false)}
|
||||
header={{ heading: "Modal-demo" }}
|
||||
>
|
||||
<Modal.Body style={{ position: "relative" }}>
|
||||
<BodyLong spacing>
|
||||
Lorem ipsum dolor sit, amet consectetur adipisicing elit.
|
||||
</BodyLong>
|
||||
|
||||
<DatePicker {...datepickerProps} dropdownCaption>
|
||||
<DatePicker.Input
|
||||
{...inputProps}
|
||||
label="Velg dato"
|
||||
description="Format: dd.mm.yyyy"
|
||||
/>
|
||||
</DatePicker>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button>Neste</Button>
|
||||
<Button variant="secondary">Tilbake</Button>
|
||||
<Button variant="tertiary">Avbryt</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
ModalDemo.parameters = { chromatic: { pauseAnimationAtEnd: true } };
|
||||
@@ -101,9 +101,4 @@ export interface DatePickerDefaultProps
|
||||
* @default See Popover
|
||||
*/
|
||||
strategy?: "absolute" | "fixed";
|
||||
/**
|
||||
* Bubbles Escape keydown-event up trough DOM-tree. This is set to false by default to prevent closing components like Modal on Escape
|
||||
* @default false
|
||||
*/
|
||||
bubbleEscape?: boolean;
|
||||
}
|
||||
@@ -1,17 +1,15 @@
|
||||
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
|
||||
import isWeekend from "date-fns/isWeekend";
|
||||
import React, { useCallback, useRef, useState } from "react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { DayClickEventHandler, isMatch } from "react-day-picker";
|
||||
import { DateInputProps } from "../DateInput";
|
||||
import { DatePickerProps } from "../datepicker/DatePicker";
|
||||
import { DateInputProps } from "../parts/DateInput";
|
||||
import {
|
||||
formatDateForInput,
|
||||
getLocaleFromString,
|
||||
isValidDate,
|
||||
parseDate,
|
||||
} from "../utils";
|
||||
import { useEscape } from "./useEscape";
|
||||
import { useOutsideClickHandler } from "./useOutsideClickHandler";
|
||||
|
||||
export interface UseDatepickerOptions
|
||||
extends Pick<
|
||||
@@ -59,10 +57,10 @@ export interface UseDatepickerOptions
|
||||
*/
|
||||
allowTwoDigitYear?: boolean;
|
||||
/**
|
||||
* Opens datepicker on input-focus
|
||||
* @default true
|
||||
* Will be removed in a future major-version
|
||||
* @deprecated
|
||||
*/
|
||||
openOnFocus?: boolean;
|
||||
openOnFocus?: false;
|
||||
}
|
||||
|
||||
interface UseDatepickerValue {
|
||||
@@ -76,7 +74,14 @@ interface UseDatepickerValue {
|
||||
inputProps: Pick<
|
||||
DateInputProps,
|
||||
"onChange" | "onFocus" | "onBlur" | "value"
|
||||
> & { ref: React.RefObject<HTMLInputElement> };
|
||||
> & {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setAnchorRef: React.Dispatch<
|
||||
React.SetStateAction<HTMLButtonElement | null>
|
||||
>;
|
||||
};
|
||||
/**
|
||||
* Resets all states (callback)
|
||||
*/
|
||||
@@ -143,14 +148,11 @@ export const useDatepicker = (
|
||||
onValidate,
|
||||
defaultMonth,
|
||||
allowTwoDigitYear = true,
|
||||
openOnFocus = true,
|
||||
} = opt;
|
||||
|
||||
const [anchorRef, setAnchorRef] = useState<HTMLButtonElement | null>(null);
|
||||
const locale = getLocaleFromString(_locale);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [daypickerRef, setDaypickerRef] = useState<HTMLDivElement>();
|
||||
|
||||
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
|
||||
|
||||
// Initialize states
|
||||
@@ -164,21 +166,14 @@ export const useDatepicker = (
|
||||
const [inputValue, setInputValue] = useState(defaultInputValue);
|
||||
|
||||
const handleOpen = useCallback(
|
||||
(open: boolean) => {
|
||||
setOpen(open);
|
||||
open && setMonth(selectedDay ?? defaultSelected ?? defaultMonth ?? today);
|
||||
(newOpen: boolean) => {
|
||||
setOpen(newOpen);
|
||||
newOpen &&
|
||||
setMonth(selectedDay ?? defaultSelected ?? defaultMonth ?? today);
|
||||
},
|
||||
[defaultMonth, defaultSelected, selectedDay, today]
|
||||
);
|
||||
|
||||
useOutsideClickHandler(open, handleOpen, [
|
||||
daypickerRef,
|
||||
inputRef.current,
|
||||
inputRef.current?.nextSibling,
|
||||
]);
|
||||
|
||||
useEscape(open, handleOpen, inputRef);
|
||||
|
||||
const updateDate = (date?: Date) => {
|
||||
onDateChange?.(date);
|
||||
setSelectedDay(date);
|
||||
@@ -206,7 +201,6 @@ export const useDatepicker = (
|
||||
if (e.target.readOnly) {
|
||||
return;
|
||||
}
|
||||
!open && openOnFocus && handleOpen(true);
|
||||
const day = parseDate(
|
||||
e.target.value,
|
||||
today,
|
||||
@@ -242,7 +236,7 @@ export const useDatepicker = (
|
||||
const handleDayClick: DayClickEventHandler = (day, { selected }) => {
|
||||
if (day && !selected) {
|
||||
handleOpen(false);
|
||||
inputRef.current && inputRef.current.focus();
|
||||
anchorRef?.focus();
|
||||
}
|
||||
|
||||
if (!required && selected) {
|
||||
@@ -310,7 +304,7 @@ export const useDatepicker = (
|
||||
|
||||
const datepickerProps = {
|
||||
month,
|
||||
onMonthChange: (month) => setMonth(month),
|
||||
onMonthChange: setMonth,
|
||||
onDayClick: handleDayClick,
|
||||
selected: selectedDay ?? new Date("Invalid date"),
|
||||
locale: _locale,
|
||||
@@ -318,11 +312,13 @@ export const useDatepicker = (
|
||||
toDate,
|
||||
today,
|
||||
open,
|
||||
onClose: () => {
|
||||
handleOpen(false);
|
||||
anchorRef?.focus();
|
||||
},
|
||||
onOpenToggle: () => handleOpen(!open),
|
||||
disabled,
|
||||
disableWeekends,
|
||||
bubbleEscape: true,
|
||||
ref: setDaypickerRef,
|
||||
};
|
||||
|
||||
const inputProps = {
|
||||
@@ -330,7 +326,7 @@ export const useDatepicker = (
|
||||
onFocus: handleFocus,
|
||||
onBlur: handleBlur,
|
||||
value: inputValue,
|
||||
ref: inputRef,
|
||||
setAnchorRef,
|
||||
};
|
||||
|
||||
return { datepickerProps, inputProps, reset, selectedDay, setSelected };
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
import { useCallback, useEffect, RefObject } from "react";
|
||||
|
||||
export const useEscape = (
|
||||
open: boolean,
|
||||
setOpen: (openState: boolean) => void,
|
||||
focusRef: RefObject<HTMLElement>
|
||||
) => {
|
||||
const handleClose = useCallback(() => {
|
||||
setOpen(false);
|
||||
focusRef?.current && focusRef.current.focus();
|
||||
}, [focusRef, setOpen]);
|
||||
|
||||
const escape = useCallback(
|
||||
(event: KeyboardEvent) => {
|
||||
if (open && event.key === "Escape") {
|
||||
event.preventDefault(); // This prevents modal from closing when using datepicker inside modal
|
||||
handleClose();
|
||||
}
|
||||
},
|
||||
[handleClose, open]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", escape, false);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", escape, false);
|
||||
};
|
||||
}, [escape]);
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import React, { useCallback, useMemo, useRef, useState } from "react";
|
||||
import { DateInputProps } from "../DateInput";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { MonthPickerProps } from "../monthpicker/types";
|
||||
import { DateInputProps } from "../parts/DateInput";
|
||||
import {
|
||||
formatDateForInput,
|
||||
getLocaleFromString,
|
||||
@@ -8,8 +8,6 @@ import {
|
||||
isValidDate,
|
||||
parseDate,
|
||||
} from "../utils";
|
||||
import { useEscape } from "./useEscape";
|
||||
import { useOutsideClickHandler } from "./useOutsideClickHandler";
|
||||
|
||||
export interface UseMonthPickerOptions
|
||||
extends Pick<
|
||||
@@ -45,8 +43,8 @@ export interface UseMonthPickerOptions
|
||||
*/
|
||||
allowTwoDigitYear?: boolean;
|
||||
/**
|
||||
* Opens datepicker on input-focus
|
||||
* @default true
|
||||
* Will be removed in a future major-version
|
||||
* @deprecated
|
||||
*/
|
||||
openOnFocus?: boolean;
|
||||
}
|
||||
@@ -60,7 +58,12 @@ interface UseMonthPickerValue {
|
||||
* Use: <MonthPicker.Input {...inputProps} />
|
||||
*/
|
||||
inputProps: Pick<DateInputProps, "onChange" | "onFocus" | "value"> & {
|
||||
ref: React.RefObject<HTMLInputElement>;
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setAnchorRef: React.Dispatch<
|
||||
React.SetStateAction<HTMLButtonElement | null>
|
||||
>;
|
||||
};
|
||||
/**
|
||||
* Currently selected Date
|
||||
@@ -138,17 +141,14 @@ export const useMonthpicker = (
|
||||
onValidate,
|
||||
defaultYear,
|
||||
allowTwoDigitYear = true,
|
||||
openOnFocus = true,
|
||||
} = opt;
|
||||
|
||||
const [anchorRef, setAnchorRef] = useState<HTMLButtonElement | null>(null);
|
||||
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
|
||||
|
||||
const today = useMemo(() => new Date(), []);
|
||||
const locale = getLocaleFromString(_locale);
|
||||
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [monthpickerRef, setMonthpickerRef] = useState<HTMLDivElement>();
|
||||
|
||||
// Initialize states
|
||||
const [year, setYear] = useState(defaultSelected ?? defaultYear ?? today);
|
||||
const [selectedMonth, setSelectedMonth] = useState(defaultSelected);
|
||||
@@ -161,21 +161,14 @@ export const useMonthpicker = (
|
||||
const [inputValue, setInputValue] = useState(defaultInputValue);
|
||||
|
||||
const handleOpen = useCallback(
|
||||
(open: boolean) => {
|
||||
setOpen(open);
|
||||
open && setYear(selectedMonth ?? defaultSelected ?? defaultYear ?? today);
|
||||
(newOpen: boolean) => {
|
||||
setOpen(newOpen);
|
||||
newOpen &&
|
||||
setYear(selectedMonth ?? defaultSelected ?? defaultYear ?? today);
|
||||
},
|
||||
[defaultSelected, defaultYear, selectedMonth, today]
|
||||
);
|
||||
|
||||
useOutsideClickHandler(open, handleOpen, [
|
||||
monthpickerRef,
|
||||
inputRef.current,
|
||||
inputRef.current?.nextSibling,
|
||||
]);
|
||||
|
||||
useEscape(open, handleOpen, inputRef);
|
||||
|
||||
const updateMonth = (date?: Date) => {
|
||||
onMonthChange?.(date);
|
||||
setSelectedMonth(date);
|
||||
@@ -203,7 +196,7 @@ export const useMonthpicker = (
|
||||
if (e.target.readOnly) {
|
||||
return;
|
||||
}
|
||||
!open && openOnFocus && handleOpen(true);
|
||||
|
||||
const day = parseDate(
|
||||
e.target.value,
|
||||
today,
|
||||
@@ -238,8 +231,8 @@ export const useMonthpicker = (
|
||||
const handleMonthClick = (month?: Date) => {
|
||||
if (month) {
|
||||
handleOpen(false);
|
||||
inputRef.current && inputRef.current.focus();
|
||||
setYear(month);
|
||||
anchorRef?.focus();
|
||||
}
|
||||
|
||||
if (!required && !month) {
|
||||
@@ -313,9 +306,11 @@ export const useMonthpicker = (
|
||||
toDate,
|
||||
open,
|
||||
onOpenToggle: () => handleOpen(!open),
|
||||
onClose: () => {
|
||||
handleOpen(false);
|
||||
anchorRef?.focus();
|
||||
},
|
||||
disabled,
|
||||
bubbleEscape: true,
|
||||
ref: setMonthpickerRef,
|
||||
};
|
||||
|
||||
const inputProps = {
|
||||
@@ -323,7 +318,7 @@ export const useMonthpicker = (
|
||||
onFocus: handleFocus,
|
||||
onBlur: handleBlur,
|
||||
value: inputValue,
|
||||
ref: inputRef,
|
||||
setAnchorRef,
|
||||
};
|
||||
|
||||
return { monthpickerProps, inputProps, reset, selectedMonth, setSelected };
|
||||
|
||||
@@ -1,34 +0,0 @@
|
||||
import { useCallback, useEffect } from "react";
|
||||
|
||||
export const useOutsideClickHandler = (
|
||||
open: boolean,
|
||||
setOpen: (openState: boolean) => void,
|
||||
refs: Array<any>
|
||||
) => {
|
||||
const handleFocusIn = useCallback(
|
||||
(e) => {
|
||||
const composed = e.composedPath?.()?.[0];
|
||||
if (!e?.target || !e?.target?.nodeType || !composed) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!refs.some(
|
||||
(element) =>
|
||||
element?.contains(e.target) || element?.contains(composed)
|
||||
)
|
||||
) {
|
||||
open && setOpen(false);
|
||||
}
|
||||
},
|
||||
[open, refs, setOpen]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("focusin", handleFocusIn);
|
||||
window.addEventListener("pointerdown", handleFocusIn);
|
||||
return () => {
|
||||
window?.removeEventListener?.("focusin", handleFocusIn);
|
||||
window?.removeEventListener?.("pointerdown", handleFocusIn);
|
||||
};
|
||||
}, [handleFocusIn]);
|
||||
};
|
||||
@@ -1,10 +1,10 @@
|
||||
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
|
||||
import isBefore from "date-fns/isBefore";
|
||||
import checkIsBefore from "date-fns/isBefore";
|
||||
import isWeekend from "date-fns/isWeekend";
|
||||
import React, { useRef, useState } from "react";
|
||||
import React, { useState } from "react";
|
||||
import { DateRange, isMatch } from "react-day-picker";
|
||||
import { DateInputProps } from "../DateInput";
|
||||
import { DatePickerProps } from "../datepicker/DatePicker";
|
||||
import { DateInputProps } from "../parts/DateInput";
|
||||
import {
|
||||
formatDateForInput,
|
||||
getLocaleFromString,
|
||||
@@ -12,8 +12,6 @@ import {
|
||||
parseDate,
|
||||
} from "../utils";
|
||||
import { DateValidationT, UseDatepickerOptions } from "./useDatepicker";
|
||||
import { useEscape } from "./useEscape";
|
||||
import { useOutsideClickHandler } from "./useOutsideClickHandler";
|
||||
|
||||
export type RangeValidationT = {
|
||||
from: DateValidationT;
|
||||
@@ -74,14 +72,28 @@ interface UseRangeDatepickerValue {
|
||||
fromInputProps: Pick<
|
||||
DateInputProps,
|
||||
"onChange" | "onFocus" | "onBlur" | "value"
|
||||
> & { ref: React.RefObject<HTMLInputElement> };
|
||||
> & {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setAnchorRef: React.Dispatch<
|
||||
React.SetStateAction<HTMLButtonElement | null>
|
||||
>;
|
||||
};
|
||||
/**
|
||||
* Use: <DatePicker.Input label="to" {...toInputProps}/>
|
||||
*/
|
||||
toInputProps: Pick<
|
||||
DateInputProps,
|
||||
"onChange" | "onFocus" | "onBlur" | "value"
|
||||
> & { ref: React.RefObject<HTMLInputElement> };
|
||||
> & {
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setAnchorRef?: React.Dispatch<
|
||||
React.SetStateAction<HTMLButtonElement | null>
|
||||
>;
|
||||
};
|
||||
/**
|
||||
* Resets all states (callback)
|
||||
*/
|
||||
@@ -218,14 +230,11 @@ export const useRangeDatepicker = (
|
||||
onValidate,
|
||||
defaultMonth,
|
||||
allowTwoDigitYear = true,
|
||||
openOnFocus = true,
|
||||
} = opt;
|
||||
|
||||
const locale = getLocaleFromString(_locale);
|
||||
const [anchorRef, setAnchorRef] = useState<HTMLButtonElement | null>(null);
|
||||
|
||||
const inputRefTo = useRef<HTMLInputElement>(null);
|
||||
const inputRefFrom = useRef<HTMLInputElement>(null);
|
||||
const [daypickerRef, setDaypickerRef] = useState<HTMLDivElement>();
|
||||
const locale = getLocaleFromString(_locale);
|
||||
|
||||
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
|
||||
|
||||
@@ -255,20 +264,6 @@ export const useRangeDatepicker = (
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
useOutsideClickHandler(open, setOpen, [
|
||||
daypickerRef,
|
||||
inputRefTo.current,
|
||||
inputRefFrom.current,
|
||||
inputRefTo.current?.nextSibling,
|
||||
inputRefFrom.current?.nextSibling,
|
||||
]);
|
||||
|
||||
useEscape(
|
||||
open,
|
||||
setOpen,
|
||||
selectedRange?.from && !selectedRange?.to ? inputRefTo : inputRefFrom
|
||||
);
|
||||
|
||||
const updateRange = (range?: DateRange) => {
|
||||
onRangeChange?.(range);
|
||||
setSelectedRange(range);
|
||||
@@ -324,7 +319,6 @@ export const useRangeDatepicker = (
|
||||
if (e.target.readOnly) {
|
||||
return;
|
||||
}
|
||||
!open && openOnFocus && setOpen(true);
|
||||
const day = parseDate(
|
||||
e.target.value,
|
||||
today,
|
||||
@@ -378,11 +372,12 @@ export const useRangeDatepicker = (
|
||||
const handleSelect = (range) => {
|
||||
if (range?.from && range?.to) {
|
||||
setOpen(false);
|
||||
anchorRef?.focus();
|
||||
}
|
||||
const prevToRange =
|
||||
!selectedRange?.from && selectedRange?.to ? selectedRange?.to : range?.to;
|
||||
|
||||
const resetTo = isBefore(prevToRange, range?.from);
|
||||
const resetTo = checkIsBefore(prevToRange, range?.from);
|
||||
|
||||
range?.from
|
||||
? setFromInputValue(
|
||||
@@ -535,7 +530,7 @@ export const useRangeDatepicker = (
|
||||
|
||||
const datepickerProps = {
|
||||
month,
|
||||
onMonthChange: (month) => setMonth(month),
|
||||
onMonthChange: setMonth,
|
||||
onSelect: handleSelect,
|
||||
selected: selectedRange,
|
||||
locale: _locale,
|
||||
@@ -545,10 +540,12 @@ export const useRangeDatepicker = (
|
||||
mode: "range" as const,
|
||||
open,
|
||||
onOpenToggle: () => setOpen((x) => !x),
|
||||
onClose: () => {
|
||||
setOpen(false);
|
||||
anchorRef?.focus();
|
||||
},
|
||||
disabled,
|
||||
disableWeekends,
|
||||
bubbleEscape: true,
|
||||
ref: setDaypickerRef,
|
||||
};
|
||||
|
||||
const fromInputProps = {
|
||||
@@ -556,7 +553,7 @@ export const useRangeDatepicker = (
|
||||
onFocus: (e) => handleFocus(e, RANGE.FROM),
|
||||
onBlur: (e) => handleBlur(e, RANGE.FROM),
|
||||
value: fromInputValue,
|
||||
ref: inputRefFrom,
|
||||
setAnchorRef,
|
||||
};
|
||||
|
||||
const toInputProps = {
|
||||
@@ -564,7 +561,7 @@ export const useRangeDatepicker = (
|
||||
onFocus: (e) => handleFocus(e, RANGE.TO),
|
||||
onBlur: (e) => handleBlur(e, RANGE.TO),
|
||||
value: toInputValue,
|
||||
ref: inputRefTo,
|
||||
setAnchorRef,
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
export { type DateInputProps } from "./DateInput";
|
||||
export {
|
||||
default as DatePicker,
|
||||
type DatePickerProps,
|
||||
@@ -15,3 +14,4 @@ export {
|
||||
export { default as MonthPicker } from "./monthpicker/MonthPicker";
|
||||
export { type MonthPickerStandaloneProps } from "./monthpicker/MonthPickerStandalone";
|
||||
export { type MonthPickerProps } from "./monthpicker/types";
|
||||
export { type DateInputProps } from "./parts/DateInput";
|
||||
@@ -1,14 +1,14 @@
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons";
|
||||
import { isAfter, isBefore } from "date-fns";
|
||||
import setYear from "date-fns/setYear";
|
||||
import startOfMonth from "date-fns/startOfMonth";
|
||||
import startOfYear from "date-fns/startOfYear";
|
||||
import React from "react";
|
||||
import { useDayPicker } from "react-day-picker";
|
||||
import { useSharedMonthContext } from "../context";
|
||||
import { labelNextYear, labelPrevYear } from "../utils";
|
||||
import { isAfter, isBefore } from "date-fns";
|
||||
import { Button } from "../../button";
|
||||
import { Select } from "../../form/Select";
|
||||
import { useSharedMonthContext } from "../context";
|
||||
import { labelNextYear, labelPrevYear } from "../utils";
|
||||
|
||||
export const MonthCaption = () => {
|
||||
const {
|
||||
@@ -24,9 +24,9 @@ export const MonthCaption = () => {
|
||||
|
||||
if (hasDropdown && fromDate && toDate) {
|
||||
const fromYear = fromDate.getFullYear();
|
||||
const toYear = toDate.getFullYear();
|
||||
for (let year = fromYear; year <= toYear; year++) {
|
||||
years.push(setYear(startOfYear(new Date()), year));
|
||||
const toDateYear = toDate.getFullYear();
|
||||
for (let currYear = fromYear; currYear <= toDateYear; currYear++) {
|
||||
years.push(setYear(startOfYear(new Date()), currYear));
|
||||
}
|
||||
|
||||
if (!years.map((x) => x.getFullYear()).includes(year.getFullYear())) {
|
||||
@@ -75,9 +75,9 @@ export const MonthCaption = () => {
|
||||
onChange={handleYearChange}
|
||||
className="navds-date__caption__year"
|
||||
>
|
||||
{years.map((year) => (
|
||||
<option key={year.getFullYear()} value={year.getFullYear()}>
|
||||
{formatYearCaption(year, { locale })}
|
||||
{years.map((yearOpt) => (
|
||||
<option key={yearOpt.getFullYear()} value={yearOpt.getFullYear()}>
|
||||
{formatYearCaption(yearOpt, { locale })}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef, useRef, useState } from "react";
|
||||
import React, { forwardRef, useMemo, useRef, useState } from "react";
|
||||
import { RootProvider } from "react-day-picker";
|
||||
import { Popover } from "../../popover";
|
||||
import { useId } from "../../util";
|
||||
import { MonthPickerInput } from "../DateInput";
|
||||
import { mergeRefs, useId } from "../../util";
|
||||
import { DateContext, SharedMonthProvider } from "../context";
|
||||
import { MonthPickerInput } from "../parts/DateInput";
|
||||
import { DateWrapper } from "../parts/DateWrapper";
|
||||
import { getLocaleFromString } from "../utils";
|
||||
import MonthCaption from "./MonthCaption";
|
||||
import MonthPickerStandalone from "./MonthPickerStandalone";
|
||||
@@ -74,7 +74,6 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
|
||||
year,
|
||||
onYearChange,
|
||||
strategy = "absolute",
|
||||
bubbleEscape = false,
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
@@ -82,6 +81,7 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
|
||||
const [open, setOpen] = useState(_open ?? false);
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const mergedRef = useMemo(() => mergeRefs([wrapperRef, ref]), [ref]);
|
||||
|
||||
const [selectedMonth, setSelectedMonth] = useState<Date | undefined>(
|
||||
defaultSelected
|
||||
@@ -107,51 +107,47 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
|
||||
onOpenToggle?.();
|
||||
},
|
||||
ariaId,
|
||||
defined: true,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
ref={mergedRef}
|
||||
className={cl("navds-date__wrapper", wrapperClassName)}
|
||||
>
|
||||
{children}
|
||||
{(_open ?? open) && (
|
||||
<Popover
|
||||
arrow={false}
|
||||
anchorEl={wrapperRef.current}
|
||||
open={_open ?? open}
|
||||
onClose={() => onClose?.() ?? setOpen(false)}
|
||||
placement="bottom-start"
|
||||
role="dialog"
|
||||
ref={ref}
|
||||
id={ariaId}
|
||||
className="navds-date navds-date__popover"
|
||||
strategy={strategy}
|
||||
bubbleEscape={bubbleEscape}
|
||||
flip={false}
|
||||
<DateWrapper
|
||||
open={_open ?? open}
|
||||
anchor={wrapperRef.current}
|
||||
onClose={() => onClose?.() ?? setOpen(false)}
|
||||
locale={locale}
|
||||
variant="month"
|
||||
popoverProps={{
|
||||
id: ariaId,
|
||||
strategy,
|
||||
}}
|
||||
>
|
||||
<RootProvider
|
||||
locale={getLocaleFromString(locale)}
|
||||
selected={selected ?? selectedMonth}
|
||||
toDate={toDate}
|
||||
fromDate={fromDate}
|
||||
month={selected ?? selectedMonth}
|
||||
>
|
||||
<RootProvider
|
||||
locale={getLocaleFromString(locale)}
|
||||
selected={selected ?? selectedMonth}
|
||||
toDate={toDate}
|
||||
fromDate={fromDate}
|
||||
month={selected ?? selectedMonth}
|
||||
>
|
||||
<div className={cl("rdp-month", className)}>
|
||||
<SharedMonthProvider
|
||||
dropdownCaption={dropdownCaption}
|
||||
disabled={disabled}
|
||||
selected={selected ?? selectedMonth}
|
||||
onSelect={handleSelect}
|
||||
year={year}
|
||||
onYearChange={onYearChange}
|
||||
>
|
||||
<MonthCaption />
|
||||
<MonthSelector />
|
||||
</SharedMonthProvider>
|
||||
</div>
|
||||
</RootProvider>
|
||||
</Popover>
|
||||
)}
|
||||
<div className={cl("rdp-month", className)}>
|
||||
<SharedMonthProvider
|
||||
dropdownCaption={dropdownCaption}
|
||||
disabled={disabled}
|
||||
selected={selected ?? selectedMonth}
|
||||
onSelect={handleSelect}
|
||||
year={year}
|
||||
onYearChange={onYearChange}
|
||||
>
|
||||
<MonthCaption />
|
||||
<MonthSelector />
|
||||
</SharedMonthProvider>
|
||||
</div>
|
||||
</RootProvider>
|
||||
</DateWrapper>
|
||||
</div>
|
||||
</DateContext.Provider>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Meta, StoryFn } from "@storybook/react";
|
||||
import setYear from "date-fns/setYear";
|
||||
import React, { useId, useState } from "react";
|
||||
import { Button, DateInputProps } from "../..";
|
||||
import { useMonthpicker } from "../hooks";
|
||||
import MonthPicker, { MonthPickerProps } from "./MonthPicker";
|
||||
import MonthPicker from "./MonthPicker";
|
||||
import { MonthPickerProps } from "./types";
|
||||
|
||||
export default {
|
||||
title: "ds-react/Monthpicker",
|
||||
@@ -147,3 +149,28 @@ export const UserControlled = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const FollowYear = () => {
|
||||
const { monthpickerProps, inputProps, selectedMonth, setSelected } =
|
||||
useMonthpicker({
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
toDate: new Date("Aug 23 2025"),
|
||||
onMonthChange: console.log,
|
||||
});
|
||||
|
||||
const customYearChange = (yearDate?: Date) => {
|
||||
monthpickerProps.onYearChange?.(yearDate);
|
||||
if (selectedMonth && yearDate) {
|
||||
setSelected(setYear(selectedMonth, yearDate.getFullYear()));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-96">
|
||||
<MonthPicker {...monthpickerProps} onYearChange={customYearChange}>
|
||||
<MonthPicker.Input {...inputProps} label="Velg månede" />
|
||||
</MonthPicker>
|
||||
{selectedMonth && <div className="pt-4">{selectedMonth.getMonth()}</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -75,9 +75,4 @@ export interface MonthPickerProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
* @default "absolute"
|
||||
*/
|
||||
strategy?: "absolute" | "fixed";
|
||||
/**
|
||||
* Bubbles Escape keydown-event up trough DOM-tree. This is set to false by default to prevent closing components like Modal on Escape
|
||||
* @default false
|
||||
*/
|
||||
bubbleEscape?: boolean;
|
||||
}
|
||||
+23
-12
@@ -1,11 +1,11 @@
|
||||
import { CalendarIcon } from "@navikt/aksel-icons";
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef, InputHTMLAttributes } from "react";
|
||||
import { FormFieldProps, useFormField } from "../form/useFormField";
|
||||
import { useDateInputContext } from "./context";
|
||||
import { ReadOnlyIcon } from "../form/ReadOnlyIcon";
|
||||
import { BodyShort, ErrorMessage, Label } from "../typography";
|
||||
import { omit } from "../util";
|
||||
import React, { forwardRef, InputHTMLAttributes, useRef } from "react";
|
||||
import { ReadOnlyIcon } from "../../form/ReadOnlyIcon";
|
||||
import { FormFieldProps, useFormField } from "../../form/useFormField";
|
||||
import { BodyShort, ErrorMessage, Label } from "../../typography";
|
||||
import { omit } from "../../util";
|
||||
import { useDateInputContext } from "../context";
|
||||
|
||||
export interface DateInputProps
|
||||
extends FormFieldProps,
|
||||
@@ -28,6 +28,10 @@ export interface DateInputProps
|
||||
* @private
|
||||
*/
|
||||
variant?: "datepicker" | "monthpicker";
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
setAnchorRef?: React.Dispatch<React.SetStateAction<HTMLButtonElement | null>>;
|
||||
}
|
||||
|
||||
const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
|
||||
@@ -37,9 +41,12 @@ const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
|
||||
label,
|
||||
description,
|
||||
variant = "datepicker",
|
||||
setAnchorRef,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const buttonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const isDatepickerVariant = variant === "datepicker";
|
||||
|
||||
const conditionalVariables = {
|
||||
@@ -50,7 +57,7 @@ const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
|
||||
},
|
||||
};
|
||||
|
||||
const { onOpen, ariaId, open } = useDateInputContext();
|
||||
const context = useDateInputContext();
|
||||
|
||||
const {
|
||||
inputProps,
|
||||
@@ -108,7 +115,7 @@ const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
|
||||
{...omit(rest, ["error", "errorId", "size"])}
|
||||
{...inputProps}
|
||||
autoComplete="off"
|
||||
aria-controls={open ? ariaId : undefined}
|
||||
aria-controls={context?.open ? context.ariaId : undefined}
|
||||
readOnly={readOnly}
|
||||
className={cl(
|
||||
"navds-date__field-input",
|
||||
@@ -116,19 +123,23 @@ const DateInput = forwardRef<HTMLInputElement, DateInputProps>((props, ref) => {
|
||||
"navds-body-short",
|
||||
`navds-body-short--${size}`
|
||||
)}
|
||||
size={isDatepickerVariant ? 10 : 14}
|
||||
size={isDatepickerVariant ? 11 : 14}
|
||||
/>
|
||||
<button
|
||||
disabled={inputProps.disabled || readOnly}
|
||||
tabIndex={readOnly ? -1 : open ? -1 : 0}
|
||||
onClick={() => onOpen()}
|
||||
tabIndex={readOnly ? -1 : context?.open ? -1 : 0}
|
||||
onClick={() => {
|
||||
context?.onOpen();
|
||||
setAnchorRef?.(buttonRef.current);
|
||||
}}
|
||||
type="button"
|
||||
className="navds-date__field-button"
|
||||
ref={buttonRef}
|
||||
>
|
||||
<CalendarIcon
|
||||
pointerEvents="none"
|
||||
title={
|
||||
open
|
||||
context?.open
|
||||
? conditionalVariables.iconTitle.close
|
||||
: conditionalVariables.iconTitle.open
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import cl from "clsx";
|
||||
import React, { useContext, useRef } from "react";
|
||||
import { Button } from "../../button";
|
||||
import { Modal } from "../../modal";
|
||||
import { ModalContext } from "../../modal/ModalContext";
|
||||
import { Popover } from "../../popover";
|
||||
import { useMedia } from "../../util/useMedia";
|
||||
import { modalCloseButtonLabel, modalLabel } from "../utils/labels";
|
||||
|
||||
type DateWrapperProps = {
|
||||
open: boolean;
|
||||
children: React.ReactNode;
|
||||
onClose: () => void;
|
||||
anchor: HTMLDivElement | null;
|
||||
locale: "nb" | "nn" | "en";
|
||||
variant: "single" | "multiple" | "range" | "month";
|
||||
popoverProps: {
|
||||
id?: string;
|
||||
strategy?: "absolute" | "fixed";
|
||||
};
|
||||
};
|
||||
|
||||
export const DateWrapper = ({
|
||||
open,
|
||||
children,
|
||||
onClose,
|
||||
anchor,
|
||||
locale,
|
||||
variant,
|
||||
popoverProps,
|
||||
}: DateWrapperProps) => {
|
||||
const modalRef = useRef<HTMLDialogElement>(null);
|
||||
const isInModal = useContext(ModalContext) !== null;
|
||||
const hideModal =
|
||||
useMedia("screen and (min-width: 768px)", true) && !isInModal;
|
||||
|
||||
if (hideModal) {
|
||||
return (
|
||||
<Popover
|
||||
arrow={false}
|
||||
anchorEl={anchor}
|
||||
open={open}
|
||||
onClose={onClose}
|
||||
placement="bottom-start"
|
||||
role="dialog"
|
||||
className={cl("navds-date__popover", {
|
||||
"navds-date": variant === "month",
|
||||
})}
|
||||
flip={false}
|
||||
{...popoverProps}
|
||||
>
|
||||
{children}
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Modal
|
||||
ref={modalRef}
|
||||
open={open}
|
||||
onClose={(event) => {
|
||||
event.stopPropagation();
|
||||
onClose();
|
||||
}}
|
||||
aria-label={modalLabel(locale, variant)}
|
||||
className={cl("navds-date__modal", {
|
||||
"navds-date__nested-modal": isInModal,
|
||||
"navds-date": variant === "month",
|
||||
})}
|
||||
closeOnBackdropClick
|
||||
>
|
||||
<div className="navds-date__modal-body">
|
||||
{children}
|
||||
<Button
|
||||
variant="tertiary"
|
||||
onClick={() => modalRef?.current?.close()}
|
||||
size="small"
|
||||
>
|
||||
{modalCloseButtonLabel(locale)}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,13 +1,12 @@
|
||||
/* https://github.com/gpbl/react-day-picker/blob/main/src/components/Table/utils/getMonthWeeks.test.ts */
|
||||
import { nb, enGB } from "date-fns/locale";
|
||||
import { enGB, nb } from "date-fns/locale";
|
||||
|
||||
import { getMonthWeeks } from "../get-month-weeks";
|
||||
|
||||
describe('when using the "nB" locale', () => {
|
||||
const locale = nb;
|
||||
describe("when getting the weeks for January 2022", () => {
|
||||
const date = new Date(2022, 0);
|
||||
const weeks = getMonthWeeks(date, { locale });
|
||||
const weeks = getMonthWeeks(new Date(2022, 0), { locale });
|
||||
test("the first week should be the last of the previous year", () => {
|
||||
const weekNumbers = weeks.map((week) => week.weekNumber);
|
||||
expect(weekNumbers[0]).toEqual(52);
|
||||
@@ -29,8 +28,10 @@ describe('when using the "enGB" locale', () => {
|
||||
describe("when using fixed weeks", () => {
|
||||
const useFixedWeeks = true;
|
||||
describe("when getting the weeks for December 2022", () => {
|
||||
const date = new Date(2022, 11);
|
||||
const weeks = getMonthWeeks(date, { useFixedWeeks, locale });
|
||||
const weeks = getMonthWeeks(new Date(2022, 11), {
|
||||
useFixedWeeks,
|
||||
locale,
|
||||
});
|
||||
test("should return 48 - 1 week numbers", () => {
|
||||
const weekNumbers = weeks.map((week) => week.weekNumber);
|
||||
const expectedResult = [48, 49, 50, 51, 52, 1];
|
||||
@@ -67,8 +68,10 @@ describe('when using the "nb" locale', () => {
|
||||
describe("when using fixed weeks", () => {
|
||||
const useFixedWeeks = true;
|
||||
describe("when getting the weeks for December 2022", () => {
|
||||
const date = new Date(2022, 11);
|
||||
const weeks = getMonthWeeks(date, { useFixedWeeks, locale });
|
||||
const weeks = getMonthWeeks(new Date(2022, 11), {
|
||||
useFixedWeeks,
|
||||
locale,
|
||||
});
|
||||
test("should return 48 - 1 week numbers", () => {
|
||||
const weekNumbers = weeks.map((week) => week.weekNumber);
|
||||
const expectedResult = [48, 49, 50, 51, 52, 1];
|
||||
|
||||
@@ -133,3 +133,86 @@ export const labelWeek = (localeCode?: string): string => {
|
||||
return `Uke:`;
|
||||
}
|
||||
};
|
||||
|
||||
const modalLabelSingle = (localeCode?: string): string => {
|
||||
switch (localeCode) {
|
||||
case "nb":
|
||||
return `Velg dato`;
|
||||
case "nn":
|
||||
return `Vel dato`;
|
||||
case "en-GB":
|
||||
return `Pick a date`;
|
||||
default:
|
||||
return `Velg dato`;
|
||||
}
|
||||
};
|
||||
|
||||
const modalLabelMultiple = (localeCode?: string): string => {
|
||||
switch (localeCode) {
|
||||
case "nb":
|
||||
return `Velg datoer`;
|
||||
case "nn":
|
||||
return `Vel datoar`;
|
||||
case "en-GB":
|
||||
return `Pick dates`;
|
||||
default:
|
||||
return `Velg datoer`;
|
||||
}
|
||||
};
|
||||
|
||||
const modalLabelRanged = (localeCode?: string): string => {
|
||||
switch (localeCode) {
|
||||
case "nb":
|
||||
return `Velg start- og sluttdato`;
|
||||
case "nn":
|
||||
return `Vel start- og sluttdato`;
|
||||
case "en-GB":
|
||||
return `Pick a start and end date`;
|
||||
default:
|
||||
return `Velg start- og sluttdato`;
|
||||
}
|
||||
};
|
||||
|
||||
const modalLabelMonth = (localeCode?: string): string => {
|
||||
switch (localeCode) {
|
||||
case "nb":
|
||||
return `Velg måned`;
|
||||
case "nn":
|
||||
return `Vel månad`;
|
||||
case "en-GB":
|
||||
return `Pick a month`;
|
||||
default:
|
||||
return `Velg måned`;
|
||||
}
|
||||
};
|
||||
|
||||
export const modalLabel = (
|
||||
localeCode: string,
|
||||
variant: "single" | "multiple" | "range" | "month"
|
||||
) => {
|
||||
switch (variant) {
|
||||
case "single":
|
||||
return modalLabelSingle(localeCode);
|
||||
case "multiple":
|
||||
return modalLabelMultiple(localeCode);
|
||||
case "range":
|
||||
return modalLabelRanged(localeCode);
|
||||
case "month":
|
||||
return modalLabelMonth(localeCode);
|
||||
default:
|
||||
return modalLabelSingle(localeCode);
|
||||
}
|
||||
};
|
||||
|
||||
export const modalCloseButtonLabel = (localeCode?: string): string => {
|
||||
switch (localeCode) {
|
||||
case "nb":
|
||||
return `Lukk`;
|
||||
case "nn":
|
||||
return `Lukk`;
|
||||
case "en-GB":
|
||||
return `Close`;
|
||||
default:
|
||||
return `Lukk`;
|
||||
}
|
||||
};
|
||||
@@ -15,27 +15,27 @@ export const nextEnabled = (
|
||||
const currentIndex = currentMonth.getMonth();
|
||||
|
||||
if (key === "Home") {
|
||||
const nextEnabled = nextOnRow(
|
||||
const nextDate = nextOnRow(
|
||||
currentIndex,
|
||||
months,
|
||||
yearState,
|
||||
disabled,
|
||||
"home"
|
||||
);
|
||||
if (nextEnabled) {
|
||||
return nextEnabled;
|
||||
if (nextDate) {
|
||||
return nextDate;
|
||||
}
|
||||
}
|
||||
if (key === "End") {
|
||||
const nextEnabled = nextOnRow(
|
||||
const nextDate = nextOnRow(
|
||||
currentIndex,
|
||||
months,
|
||||
yearState,
|
||||
disabled,
|
||||
"end"
|
||||
);
|
||||
if (nextEnabled) {
|
||||
return nextEnabled;
|
||||
if (nextDate) {
|
||||
return nextDate;
|
||||
}
|
||||
}
|
||||
if (key === "PageUp") {
|
||||
|
||||
@@ -136,7 +136,7 @@ export const Select = forwardRef<HTMLSelectElement, SelectProps>(
|
||||
className={cl(
|
||||
"navds-select__input",
|
||||
"navds-body-short",
|
||||
`navds-body--${size ?? "medium"}`
|
||||
`navds-body-short--${size ?? "medium"}`
|
||||
)}
|
||||
size={htmlSize}
|
||||
>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -116,7 +116,7 @@ export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
|
||||
className={cl(
|
||||
"navds-text-field__input",
|
||||
"navds-body-short",
|
||||
`navds-body-${size ?? "medium"}`
|
||||
`navds-body-short--${size ?? "medium"}`
|
||||
)}
|
||||
size={htmlSize}
|
||||
/>
|
||||
|
||||
@@ -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-${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;
|
||||
@@ -33,7 +33,7 @@ export const Combobox = forwardRef<
|
||||
|
||||
const toggleListButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const { currentOption, toggleIsListOpen } = useFilteredOptionsContext();
|
||||
const { activeDecendantId, toggleIsListOpen } = useFilteredOptionsContext();
|
||||
const { selectedOptions } = useSelectedOptionsContext();
|
||||
|
||||
const {
|
||||
@@ -92,7 +92,7 @@ export const Combobox = forwardRef<
|
||||
"navds-combobox__wrapper-inner navds-text-field__input",
|
||||
{
|
||||
"navds-combobox__wrapper-inner--virtually-unfocused":
|
||||
currentOption !== null,
|
||||
activeDecendantId !== null,
|
||||
}
|
||||
)}
|
||||
onClick={focusInput}
|
||||
|
||||
@@ -65,7 +65,7 @@ const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
|
||||
size,
|
||||
}}
|
||||
>
|
||||
<CustomOptionsProvider>
|
||||
<CustomOptionsProvider value={{ isMultiSelect }}>
|
||||
<SelectedOptionsProvider
|
||||
value={{
|
||||
allowNewValues,
|
||||
@@ -81,7 +81,6 @@ const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
|
||||
filteredOptions,
|
||||
isListOpen,
|
||||
isLoading,
|
||||
isMultiSelect,
|
||||
options,
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -6,6 +6,7 @@ import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsCon
|
||||
import { useInputContext } from "../Input/inputContext";
|
||||
import { Loader } from "../../../loader";
|
||||
import { BodyShort, Label } from "../../../typography";
|
||||
import filteredOptionsUtil from "./filtered-options-util";
|
||||
|
||||
const FilteredOptions = () => {
|
||||
const {
|
||||
@@ -18,25 +19,25 @@ const FilteredOptions = () => {
|
||||
isLoading,
|
||||
isListOpen,
|
||||
filteredOptions,
|
||||
filteredOptionsIndex,
|
||||
filteredOptionsRef,
|
||||
setFilteredOptionsRef,
|
||||
isMouseLastUsedInputDevice,
|
||||
setIsMouseLastUsedInputDevice,
|
||||
isValueNew,
|
||||
setFilteredOptionsIndex,
|
||||
toggleIsListOpen,
|
||||
activeDecendantId,
|
||||
virtualFocus,
|
||||
} = useFilteredOptionsContext();
|
||||
const { isMultiSelect, selectedOptions, toggleOption } =
|
||||
useSelectedOptionsContext();
|
||||
|
||||
return (
|
||||
<ul
|
||||
ref={filteredOptionsRef}
|
||||
ref={setFilteredOptionsRef}
|
||||
className={cl("navds-combobox__list", {
|
||||
"navds-combobox__list--closed": !isListOpen,
|
||||
"navds-combobox__list--with-hover": isMouseLastUsedInputDevice,
|
||||
})}
|
||||
id={`${id}-filtered-options`}
|
||||
id={filteredOptionsUtil.getFilteredOptionsId(id)}
|
||||
role="listbox"
|
||||
tabIndex={-1}
|
||||
>
|
||||
@@ -45,7 +46,8 @@ const FilteredOptions = () => {
|
||||
className="navds-combobox__list-item--loading"
|
||||
role="option"
|
||||
aria-selected={false}
|
||||
id={`${id}-is-loading`}
|
||||
id={filteredOptionsUtil.getIsLoadingId(id)}
|
||||
data-no-focus="true"
|
||||
>
|
||||
<Loader aria-label="Søker..." />
|
||||
</li>
|
||||
@@ -54,8 +56,12 @@ const FilteredOptions = () => {
|
||||
<li
|
||||
tabIndex={-1}
|
||||
onMouseMove={() => {
|
||||
if (filteredOptionsIndex !== -1) {
|
||||
setFilteredOptionsIndex(-1);
|
||||
if (
|
||||
activeDecendantId !== filteredOptionsUtil.getAddNewOptionId(id)
|
||||
) {
|
||||
virtualFocus.moveFocusToElement(
|
||||
filteredOptionsUtil.getAddNewOptionId(id)
|
||||
);
|
||||
setIsMouseLastUsedInputDevice(true);
|
||||
}
|
||||
}}
|
||||
@@ -64,10 +70,10 @@ const FilteredOptions = () => {
|
||||
if (!isMultiSelect && !selectedOptions.includes(value))
|
||||
toggleIsListOpen(false);
|
||||
}}
|
||||
id={`${id}-combobox-new-option`}
|
||||
id={filteredOptionsUtil.getAddNewOptionId(id)}
|
||||
className={cl("navds-combobox__list-item__new-option", {
|
||||
"navds-combobox__list-item__new-option--focus":
|
||||
filteredOptionsIndex === -1,
|
||||
activeDecendantId === filteredOptionsUtil.getAddNewOptionId(id),
|
||||
})}
|
||||
role="option"
|
||||
aria-selected={false}
|
||||
@@ -86,24 +92,30 @@ const FilteredOptions = () => {
|
||||
className="navds-combobox__list-item__no-options"
|
||||
role="option"
|
||||
aria-selected={false}
|
||||
id={`${id}-no-hits`}
|
||||
id={filteredOptionsUtil.getNoHitsId(id)}
|
||||
data-no-focus="true"
|
||||
>
|
||||
Ingen søketreff
|
||||
</li>
|
||||
)}
|
||||
{filteredOptions.map((option, index) => (
|
||||
{filteredOptions.map((option) => (
|
||||
<li
|
||||
className={cl("navds-combobox__list-item", {
|
||||
"navds-combobox__list-item--focus": index === filteredOptionsIndex,
|
||||
"navds-combobox__list-item--focus":
|
||||
activeDecendantId === filteredOptionsUtil.getOptionId(id, option),
|
||||
"navds-combobox__list-item--selected":
|
||||
selectedOptions.includes(option),
|
||||
})}
|
||||
id={`${id}-option-${option.replace(" ", "-")}`}
|
||||
id={filteredOptionsUtil.getOptionId(id, option)}
|
||||
key={option}
|
||||
tabIndex={-1}
|
||||
onMouseMove={() => {
|
||||
if (filteredOptionsIndex !== index) {
|
||||
setFilteredOptionsIndex(index);
|
||||
if (
|
||||
activeDecendantId !== filteredOptionsUtil.getOptionId(id, option)
|
||||
) {
|
||||
virtualFocus.moveFocusToElement(
|
||||
filteredOptionsUtil.getOptionId(id, option)
|
||||
);
|
||||
setIsMouseLastUsedInputDevice(true);
|
||||
}
|
||||
}}
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
const normalizeText = (text: string): string =>
|
||||
typeof text === "string" ? text.toLocaleLowerCase().trim() : "";
|
||||
|
||||
const isPartOfText = (value, text) =>
|
||||
normalizeText(text).startsWith(normalizeText(value ?? ""));
|
||||
|
||||
const isValueInList = (value, list) =>
|
||||
list?.find((listItem) => normalizeText(value) === normalizeText(listItem));
|
||||
|
||||
const getMatchingValuesFromList = (value, list) =>
|
||||
list?.filter((listItem) => isPartOfText(value, listItem));
|
||||
|
||||
const getFilteredOptionsId = (comboboxId: string) =>
|
||||
`${comboboxId}-filtered-options`;
|
||||
|
||||
const getOptionId = (comboboxId: string, option: string) =>
|
||||
`${comboboxId.toLocaleLowerCase()}-option-${option
|
||||
.replace(" ", "-")
|
||||
.toLocaleLowerCase()}`;
|
||||
|
||||
const getAddNewOptionId = (comboboxId: string) =>
|
||||
`${comboboxId}-combobox-new-option`;
|
||||
|
||||
const getIsLoadingId = (comboboxId: string) => `${comboboxId}-is-loading`;
|
||||
|
||||
const getNoHitsId = (comboboxId: string) => `${comboboxId}-no-hits`;
|
||||
|
||||
export default {
|
||||
normalizeText,
|
||||
isPartOfText,
|
||||
isValueInList,
|
||||
getMatchingValuesFromList,
|
||||
getFilteredOptionsId,
|
||||
getAddNewOptionId,
|
||||
getOptionId,
|
||||
getIsLoadingId,
|
||||
getNoHitsId,
|
||||
};
|
||||
@@ -1,11 +1,9 @@
|
||||
import React, {
|
||||
useState,
|
||||
useEffect,
|
||||
useMemo,
|
||||
createContext,
|
||||
useContext,
|
||||
useCallback,
|
||||
useRef,
|
||||
SetStateAction,
|
||||
} from "react";
|
||||
import cl from "clsx";
|
||||
@@ -13,26 +11,29 @@ import { useCustomOptionsContext } from "../customOptionsContext";
|
||||
import { useInputContext } from "../Input/inputContext";
|
||||
import usePrevious from "../../../util/usePrevious";
|
||||
import { useClientLayoutEffect } from "../../../util";
|
||||
import filteredOptionsUtils from "./filtered-options-util";
|
||||
import useVirtualFocus, { VirtualFocusType } from "./useVirtualFocus";
|
||||
import { ComboboxProps } from "../types";
|
||||
|
||||
const normalizeText = (text: string): string =>
|
||||
typeof text === "string" ? `${text}`.toLowerCase().trim() : "";
|
||||
|
||||
const isPartOfText = (value, text) =>
|
||||
normalizeText(text).startsWith(normalizeText(value ?? ""));
|
||||
|
||||
const isValueInList = (value, list) =>
|
||||
list?.find((listItem) => normalizeText(value) === normalizeText(listItem));
|
||||
|
||||
const getMatchingValuesFromList = (value, list) =>
|
||||
list?.filter((listItem) => isPartOfText(value, listItem));
|
||||
type FilteredOptionsProps = {
|
||||
children: any;
|
||||
value: Pick<
|
||||
ComboboxProps,
|
||||
| "allowNewValues"
|
||||
| "filteredOptions"
|
||||
| "isListOpen"
|
||||
| "isLoading"
|
||||
| "options"
|
||||
>;
|
||||
};
|
||||
|
||||
type FilteredOptionsContextType = {
|
||||
activeDecendantId?: string;
|
||||
allowNewValues?: boolean;
|
||||
ariaDescribedBy?: string;
|
||||
filteredOptionsRef: React.RefObject<HTMLUListElement>;
|
||||
filteredOptionsIndex: number | null;
|
||||
setFilteredOptionsIndex: (index: number) => void;
|
||||
setFilteredOptionsRef: React.Dispatch<
|
||||
React.SetStateAction<HTMLUListElement | null>
|
||||
>;
|
||||
isListOpen: boolean;
|
||||
isLoading?: boolean;
|
||||
filteredOptions: string[];
|
||||
@@ -40,19 +41,18 @@ type FilteredOptionsContextType = {
|
||||
setIsMouseLastUsedInputDevice: React.Dispatch<SetStateAction<boolean>>;
|
||||
isValueNew: boolean;
|
||||
toggleIsListOpen: (newState?: boolean) => void;
|
||||
currentOption: string | null;
|
||||
resetFilteredOptionsIndex: () => void;
|
||||
moveFocusUp: () => void;
|
||||
moveFocusDown: () => void;
|
||||
moveFocusToInput: () => void;
|
||||
moveFocusToEnd: () => void;
|
||||
currentOption?: string;
|
||||
shouldAutocomplete?: boolean;
|
||||
virtualFocus: VirtualFocusType;
|
||||
};
|
||||
const FilteredOptionsContext = createContext<FilteredOptionsContextType>(
|
||||
{} as FilteredOptionsContextType
|
||||
);
|
||||
|
||||
export const FilteredOptionsProvider = ({ children, value: props }) => {
|
||||
export const FilteredOptionsProvider = ({
|
||||
children,
|
||||
value: props,
|
||||
}: FilteredOptionsProps) => {
|
||||
const {
|
||||
allowNewValues,
|
||||
filteredOptions: externalFilteredOptions,
|
||||
@@ -60,7 +60,9 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
||||
isLoading,
|
||||
options,
|
||||
} = props;
|
||||
const filteredOptionsRef = useRef<HTMLUListElement | null>(null);
|
||||
const [filteredOptionsRef, setFilteredOptionsRef] =
|
||||
useState<HTMLUListElement | null>(null);
|
||||
const virtualFocus = useVirtualFocus(filteredOptionsRef);
|
||||
const {
|
||||
inputProps: { "aria-describedby": partialAriaDescribedBy, id },
|
||||
value,
|
||||
@@ -70,9 +72,6 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
||||
shouldAutocomplete,
|
||||
} = useInputContext();
|
||||
|
||||
const [filteredOptionsIndex, setFilteredOptionsIndex] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const [isInternalListOpen, setInternalListOpen] = useState(false);
|
||||
const { customOptions } = useCustomOptionsContext();
|
||||
|
||||
@@ -81,8 +80,7 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
||||
return externalFilteredOptions;
|
||||
}
|
||||
const opts = [...customOptions, ...options];
|
||||
setFilteredOptionsIndex(null);
|
||||
return getMatchingValuesFromList(searchTerm, opts);
|
||||
return filteredOptionsUtils.getMatchingValuesFromList(searchTerm, opts);
|
||||
}, [customOptions, externalFilteredOptions, options, searchTerm]);
|
||||
|
||||
const previousSearchTerm = usePrevious(searchTerm);
|
||||
@@ -90,13 +88,28 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
||||
const [isMouseLastUsedInputDevice, setIsMouseLastUsedInputDevice] =
|
||||
useState(false);
|
||||
|
||||
const filteredOptionsMap = useMemo(
|
||||
() =>
|
||||
options.reduce(
|
||||
(map, _option) => ({
|
||||
...map,
|
||||
[filteredOptionsUtils.getOptionId(id, _option)]: _option,
|
||||
}),
|
||||
{
|
||||
[filteredOptionsUtils.getAddNewOptionId(id)]: allowNewValues
|
||||
? value
|
||||
: undefined,
|
||||
}
|
||||
),
|
||||
[allowNewValues, id, options, value]
|
||||
);
|
||||
|
||||
useClientLayoutEffect(() => {
|
||||
if (
|
||||
shouldAutocomplete &&
|
||||
normalizeText(searchTerm) !== "" &&
|
||||
filteredOptionsUtils.normalizeText(searchTerm) !== "" &&
|
||||
(previousSearchTerm?.length || 0) < searchTerm.length &&
|
||||
filteredOptions.length > 0 &&
|
||||
!isValueInList(searchTerm, filteredOptions)
|
||||
filteredOptions.length > 0
|
||||
) {
|
||||
setValue(
|
||||
`${searchTerm}${filteredOptions[0].substring(searchTerm.length)}`
|
||||
@@ -116,29 +129,30 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
||||
return isExternalListOpen ?? isInternalListOpen;
|
||||
}, [isExternalListOpen, isInternalListOpen]);
|
||||
|
||||
const toggleIsListOpen = useCallback((newState?: boolean) => {
|
||||
setFilteredOptionsIndex(null);
|
||||
setInternalListOpen((oldState) => newState ?? !oldState);
|
||||
}, []);
|
||||
|
||||
const isValueNew = useMemo(
|
||||
() => Boolean(value) && !isValueInList(value, filteredOptions),
|
||||
[value, filteredOptions]
|
||||
const toggleIsListOpen = useCallback(
|
||||
(newState?: boolean) => {
|
||||
virtualFocus.moveFocusToTop();
|
||||
setInternalListOpen((oldState) => newState ?? !oldState);
|
||||
},
|
||||
[virtualFocus]
|
||||
);
|
||||
|
||||
const getMinimumIndex = useCallback(() => {
|
||||
return isValueNew && allowNewValues ? -1 : 0;
|
||||
}, [allowNewValues, isValueNew]);
|
||||
const isValueNew = useMemo(
|
||||
() =>
|
||||
Boolean(value) &&
|
||||
!filteredOptionsMap[filteredOptionsUtils.getOptionId(id, value)],
|
||||
[filteredOptionsMap, id, value]
|
||||
);
|
||||
|
||||
const ariaDescribedBy = useMemo(() => {
|
||||
let activeOption;
|
||||
if (!isLoading && filteredOptions.length === 0) {
|
||||
activeOption = `${id}-no-hits`;
|
||||
activeOption = filteredOptionsUtils.getNoHitsId(id);
|
||||
} else if ((value && value !== "") || isLoading) {
|
||||
if (shouldAutocomplete && filteredOptions[0]) {
|
||||
activeOption = `${id}-option-${filteredOptions[0].replace(" ", "-")}`;
|
||||
activeOption = filteredOptionsUtils.getOptionId(id, filteredOptions[0]);
|
||||
} else if (isListOpen && isLoading) {
|
||||
activeOption = `${id}-is-loading`;
|
||||
activeOption = filteredOptionsUtils.getIsLoadingId(id);
|
||||
}
|
||||
}
|
||||
return cl(activeOption, partialAriaDescribedBy) || undefined;
|
||||
@@ -152,102 +166,21 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
||||
id,
|
||||
]);
|
||||
|
||||
const currentOption = useMemo(() => {
|
||||
if (filteredOptionsIndex == null) {
|
||||
return null;
|
||||
}
|
||||
if (filteredOptionsIndex === -1) {
|
||||
return value;
|
||||
}
|
||||
return filteredOptions[filteredOptionsIndex];
|
||||
}, [filteredOptionsIndex, filteredOptions, value]);
|
||||
const currentOption = useMemo(
|
||||
() =>
|
||||
filteredOptionsMap[virtualFocus.activeElement?.getAttribute("id") || -1],
|
||||
[filteredOptionsMap, virtualFocus]
|
||||
);
|
||||
|
||||
const resetFilteredOptionsIndex = () => {
|
||||
setFilteredOptionsIndex(getMinimumIndex());
|
||||
};
|
||||
|
||||
const scrollToOption = useCallback((newIndex: number) => {
|
||||
if (
|
||||
filteredOptionsRef.current &&
|
||||
filteredOptionsRef.current.children[newIndex]
|
||||
) {
|
||||
const child = filteredOptionsRef.current.children[newIndex];
|
||||
const { top, bottom } = child.getBoundingClientRect();
|
||||
const parentRect = filteredOptionsRef.current.getBoundingClientRect();
|
||||
if (top < parentRect.top || bottom > parentRect.bottom) {
|
||||
child.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (filteredOptionsIndex !== null && isListOpen) {
|
||||
scrollToOption(filteredOptionsIndex);
|
||||
}
|
||||
}, [filteredOptionsIndex, isListOpen, scrollToOption]);
|
||||
|
||||
const moveFocusToInput = useCallback(() => {
|
||||
setFilteredOptionsIndex(null);
|
||||
toggleIsListOpen(false);
|
||||
}, [toggleIsListOpen]);
|
||||
|
||||
const moveFocusToEnd = useCallback(() => {
|
||||
const lastIndex = filteredOptions.length - 1;
|
||||
toggleIsListOpen(true);
|
||||
setFilteredOptionsIndex(lastIndex);
|
||||
}, [filteredOptions.length, toggleIsListOpen]);
|
||||
|
||||
const moveFocusUp = useCallback(() => {
|
||||
if (filteredOptionsIndex === null) {
|
||||
return;
|
||||
}
|
||||
if (filteredOptionsIndex === getMinimumIndex()) {
|
||||
toggleIsListOpen(false);
|
||||
setFilteredOptionsIndex(null);
|
||||
} else {
|
||||
const newIndex = Math.max(getMinimumIndex(), filteredOptionsIndex - 1);
|
||||
setFilteredOptionsIndex(newIndex);
|
||||
}
|
||||
}, [filteredOptionsIndex, getMinimumIndex, toggleIsListOpen]);
|
||||
|
||||
const moveFocusDown = useCallback(() => {
|
||||
if (filteredOptionsIndex === null || !isListOpen) {
|
||||
toggleIsListOpen(true);
|
||||
if (allowNewValues || filteredOptions.length >= 1) {
|
||||
setFilteredOptionsIndex(getMinimumIndex());
|
||||
}
|
||||
return;
|
||||
}
|
||||
const newIndex = Math.min(
|
||||
filteredOptionsIndex + 1,
|
||||
Math.max(getMinimumIndex(), filteredOptions.length - 1)
|
||||
);
|
||||
setFilteredOptionsIndex(newIndex);
|
||||
}, [
|
||||
allowNewValues,
|
||||
filteredOptions.length,
|
||||
filteredOptionsIndex,
|
||||
getMinimumIndex,
|
||||
isListOpen,
|
||||
toggleIsListOpen,
|
||||
]);
|
||||
|
||||
const activeDecendantId = useMemo(() => {
|
||||
if (filteredOptionsIndex === null) {
|
||||
return undefined;
|
||||
} else if (filteredOptionsIndex === -1) {
|
||||
return `${id}-combobox-new-option`;
|
||||
} else {
|
||||
return `${id}-option-${currentOption?.replace(" ", "-")}`;
|
||||
}
|
||||
}, [filteredOptionsIndex, currentOption, id]);
|
||||
const activeDecendantId = useMemo(
|
||||
() => virtualFocus.activeElement?.getAttribute("id") || undefined,
|
||||
[virtualFocus.activeElement]
|
||||
);
|
||||
|
||||
const filteredOptionsState = {
|
||||
activeDecendantId,
|
||||
allowNewValues,
|
||||
filteredOptionsRef,
|
||||
filteredOptionsIndex,
|
||||
setFilteredOptionsIndex,
|
||||
setFilteredOptionsRef,
|
||||
shouldAutocomplete,
|
||||
isListOpen,
|
||||
isLoading,
|
||||
@@ -257,11 +190,7 @@ export const FilteredOptionsProvider = ({ children, value: props }) => {
|
||||
isValueNew,
|
||||
toggleIsListOpen,
|
||||
currentOption,
|
||||
resetFilteredOptionsIndex,
|
||||
moveFocusUp,
|
||||
moveFocusDown,
|
||||
moveFocusToInput,
|
||||
moveFocusToEnd,
|
||||
virtualFocus,
|
||||
ariaDescribedBy,
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { Dispatch, SetStateAction, useState } from "react";
|
||||
|
||||
export type VirtualFocusType = {
|
||||
activeElement: HTMLElement | undefined;
|
||||
getElementById: (id: string) => HTMLElement | undefined;
|
||||
isFocusOnTheTop: boolean;
|
||||
isFocusOnTheBottom: boolean;
|
||||
setIndex: Dispatch<SetStateAction<number>>;
|
||||
moveFocusUp: () => void;
|
||||
moveFocusDown: () => void;
|
||||
moveFocusToElement: (id: string) => void;
|
||||
moveFocusToTop: () => void;
|
||||
moveFocusToBottom: () => void;
|
||||
};
|
||||
|
||||
const useVirtualFocus = (
|
||||
containerRef: HTMLElement | null
|
||||
): VirtualFocusType => {
|
||||
const [index, setIndex] = useState(-1);
|
||||
|
||||
const listOfAllChildren: Array<HTMLElement> = containerRef?.children
|
||||
? Array.prototype.slice.call(containerRef?.children)
|
||||
: [];
|
||||
const elementsAbleToReceiveFocus = listOfAllChildren.filter(
|
||||
(child) => child.getAttribute("data-no-focus") !== "true"
|
||||
);
|
||||
|
||||
const activeElement = elementsAbleToReceiveFocus[index];
|
||||
const getElementById = (id: string) =>
|
||||
listOfAllChildren.find((element) => element.id === id);
|
||||
const isFocusOnTheTop = index === 0;
|
||||
const isFocusOnTheBottom = index === elementsAbleToReceiveFocus.length - 1;
|
||||
|
||||
const scrollToOption = (newIndex: number) => {
|
||||
const indexOfElementToScrollTo = Math.min(
|
||||
Math.max(newIndex, 0),
|
||||
containerRef?.children.length || 0
|
||||
);
|
||||
if (containerRef?.children[indexOfElementToScrollTo]) {
|
||||
const child = containerRef.children[indexOfElementToScrollTo];
|
||||
const { top, bottom } = child.getBoundingClientRect();
|
||||
const parentRect = containerRef.getBoundingClientRect();
|
||||
if (top < parentRect.top || bottom > parentRect.bottom) {
|
||||
child.scrollIntoView({ block: "nearest" });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const _moveFocusAndScrollTo = (_index: number) => {
|
||||
setIndex(_index);
|
||||
scrollToOption(_index);
|
||||
};
|
||||
const moveFocusUp = () => _moveFocusAndScrollTo(Math.max(index - 1, -1));
|
||||
const moveFocusDown = () =>
|
||||
_moveFocusAndScrollTo(
|
||||
Math.min(index + 1, elementsAbleToReceiveFocus.length - 1)
|
||||
);
|
||||
const moveFocusToTop = () => _moveFocusAndScrollTo(-1);
|
||||
const moveFocusToBottom = () =>
|
||||
_moveFocusAndScrollTo(elementsAbleToReceiveFocus.length - 1);
|
||||
const moveFocusToElement = (id: string) => {
|
||||
const thisElement = elementsAbleToReceiveFocus.find(
|
||||
(_element) => _element.getAttribute("id") === id
|
||||
);
|
||||
const indexOfElement = thisElement
|
||||
? elementsAbleToReceiveFocus.indexOf(thisElement)
|
||||
: -1;
|
||||
if (indexOfElement >= 0) {
|
||||
setIndex(indexOfElement);
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
activeElement,
|
||||
getElementById,
|
||||
isFocusOnTheTop,
|
||||
isFocusOnTheBottom,
|
||||
setIndex,
|
||||
moveFocusUp,
|
||||
moveFocusDown,
|
||||
moveFocusToElement,
|
||||
moveFocusToTop,
|
||||
moveFocusToBottom,
|
||||
};
|
||||
};
|
||||
|
||||
export default useVirtualFocus;
|
||||
@@ -9,6 +9,7 @@ import cl from "clsx";
|
||||
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
|
||||
import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
|
||||
import { useInputContext } from "./inputContext";
|
||||
import filteredOptionsUtil from "../FilteredOptions/filtered-options-util";
|
||||
|
||||
interface InputProps
|
||||
extends Omit<InputHTMLAttributes<HTMLInputElement>, "value"> {
|
||||
@@ -31,44 +32,55 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
allowNewValues,
|
||||
currentOption,
|
||||
filteredOptions,
|
||||
isValueNew,
|
||||
toggleIsListOpen,
|
||||
isListOpen,
|
||||
filteredOptionsIndex,
|
||||
moveFocusUp,
|
||||
moveFocusDown,
|
||||
ariaDescribedBy,
|
||||
moveFocusToInput,
|
||||
moveFocusToEnd,
|
||||
setFilteredOptionsIndex,
|
||||
setIsMouseLastUsedInputDevice,
|
||||
shouldAutocomplete,
|
||||
virtualFocus,
|
||||
} = useFilteredOptionsContext();
|
||||
|
||||
const onEnter = useCallback(
|
||||
(event: React.KeyboardEvent) => {
|
||||
const isTextInSelectedOptions = (text: string) => {
|
||||
return selectedOptions.find(
|
||||
(item) => item.toLocaleLowerCase() === text.toLocaleLowerCase()
|
||||
);
|
||||
};
|
||||
|
||||
if (currentOption) {
|
||||
event.preventDefault();
|
||||
// Selecting a value from the dropdown / FilteredOptions
|
||||
toggleOption(currentOption, event);
|
||||
if (!isMultiSelect && !selectedOptions.includes(currentOption))
|
||||
if (!isMultiSelect && !isTextInSelectedOptions(currentOption)) {
|
||||
toggleIsListOpen(false);
|
||||
} else if (shouldAutocomplete && selectedOptions.includes(value)) {
|
||||
}
|
||||
} else if (shouldAutocomplete && isTextInSelectedOptions(value)) {
|
||||
event.preventDefault();
|
||||
// Trying to set the same value that is already set, so just clearing the input
|
||||
clearInput(event);
|
||||
} else if ((allowNewValues || shouldAutocomplete) && value !== "") {
|
||||
event.preventDefault();
|
||||
// Autocompleting or adding a new value
|
||||
toggleOption(value, event);
|
||||
if (!isMultiSelect && !selectedOptions.includes(value))
|
||||
const selectedValue =
|
||||
allowNewValues && isValueNew ? value : filteredOptions[0];
|
||||
toggleOption(selectedValue, event);
|
||||
if (
|
||||
!isMultiSelect &&
|
||||
!isTextInSelectedOptions(filteredOptions[0] || selectedValue)
|
||||
) {
|
||||
toggleIsListOpen(false);
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
allowNewValues,
|
||||
clearInput,
|
||||
currentOption,
|
||||
filteredOptions,
|
||||
isMultiSelect,
|
||||
isValueNew,
|
||||
selectedOptions,
|
||||
shouldAutocomplete,
|
||||
toggleIsListOpen,
|
||||
@@ -89,10 +101,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
onEnter(e);
|
||||
break;
|
||||
case "Home":
|
||||
moveFocusToInput();
|
||||
virtualFocus.moveFocusToTop();
|
||||
break;
|
||||
case "End":
|
||||
moveFocusToEnd();
|
||||
virtualFocus.moveFocusToBottom();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
@@ -113,14 +125,20 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
// so we don't interfere with text editing
|
||||
if (e.target.selectionStart === value?.length) {
|
||||
e.preventDefault();
|
||||
moveFocusDown();
|
||||
if (virtualFocus.activeElement === null || !isListOpen) {
|
||||
toggleIsListOpen(true);
|
||||
}
|
||||
virtualFocus.moveFocusDown();
|
||||
}
|
||||
} else if (e.key === "ArrowUp") {
|
||||
// Check that the FilteredOptions list is open and has virtual focus.
|
||||
// Otherwise ignore keystrokes, so it doesn't interfere with text editing
|
||||
if (isListOpen && filteredOptionsIndex !== null) {
|
||||
if (isListOpen && activeDecendantId) {
|
||||
e.preventDefault();
|
||||
moveFocusUp();
|
||||
if (virtualFocus.isFocusOnTheTop) {
|
||||
toggleIsListOpen(false);
|
||||
}
|
||||
virtualFocus.moveFocusUp();
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -128,11 +146,11 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
value,
|
||||
selectedOptions,
|
||||
removeSelectedOption,
|
||||
moveFocusDown,
|
||||
isListOpen,
|
||||
filteredOptionsIndex,
|
||||
moveFocusUp,
|
||||
activeDecendantId,
|
||||
setIsMouseLastUsedInputDevice,
|
||||
toggleIsListOpen,
|
||||
virtualFocus,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -144,13 +162,14 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
} else if (filteredOptions.length === 0) {
|
||||
toggleIsListOpen(false);
|
||||
}
|
||||
virtualFocus.moveFocusToTop();
|
||||
onChange(event);
|
||||
},
|
||||
[filteredOptions.length, onChange, toggleIsListOpen]
|
||||
[filteredOptions.length, virtualFocus, onChange, toggleIsListOpen]
|
||||
);
|
||||
|
||||
const onBlur = () => {
|
||||
setFilteredOptionsIndex(-1);
|
||||
virtualFocus.moveFocusToTop();
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -165,7 +184,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
onBlur={onBlur}
|
||||
onKeyUp={handleKeyUp}
|
||||
onKeyDown={handleKeyDown}
|
||||
aria-controls={`${inputProps.id}-filtered-options`}
|
||||
aria-controls={filteredOptionsUtil.getFilteredOptionsId(inputProps.id)}
|
||||
aria-expanded={!!isListOpen}
|
||||
autoComplete="off"
|
||||
aria-autocomplete={shouldAutocomplete ? "both" : "list"}
|
||||
@@ -176,7 +195,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
inputClassName,
|
||||
"navds-combobox__input",
|
||||
"navds-body-short",
|
||||
`navds-body-${size}`
|
||||
`navds-body-short--${size}`
|
||||
)}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -8,8 +8,8 @@ import React, {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { useFormField, FormFieldType } from "../../useFormField";
|
||||
import { useClientLayoutEffect } from "../../../util";
|
||||
import { FormFieldType, useFormField } from "../../useFormField";
|
||||
|
||||
interface InputContextType extends FormFieldType {
|
||||
clearInput: (event: React.PointerEvent | React.KeyboardEvent) => void;
|
||||
@@ -63,10 +63,10 @@ export const InputContextProvider = ({ children, value: props }) => {
|
||||
|
||||
const onChange = useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => {
|
||||
const value = event.currentTarget.value;
|
||||
externalValue ?? setInternalValue(value);
|
||||
const newValue = event.currentTarget.value;
|
||||
externalValue ?? setInternalValue(newValue);
|
||||
externalOnChange?.(event);
|
||||
setSearchTerm(value);
|
||||
setSearchTerm(newValue);
|
||||
},
|
||||
[externalValue, externalOnChange]
|
||||
);
|
||||
|
||||
@@ -7,8 +7,8 @@ import React, {
|
||||
} from "react";
|
||||
import usePrevious from "../../../util/usePrevious";
|
||||
import { useInputContext } from "../Input/inputContext";
|
||||
import { ComboboxProps } from "../types";
|
||||
import { useCustomOptionsContext } from "../customOptionsContext";
|
||||
import { ComboboxProps } from "../types";
|
||||
|
||||
type SelectedOptionsContextType = {
|
||||
addSelectedOption: (option: string) => void;
|
||||
@@ -71,8 +71,8 @@ export const SelectedOptionsProvider = ({
|
||||
allowNewValues && addCustomOption(option);
|
||||
!isMultiSelect && setSelectedOptions([]);
|
||||
} else if (isMultiSelect) {
|
||||
setSelectedOptions((prevSelectedOptions) => [
|
||||
...prevSelectedOptions,
|
||||
setSelectedOptions((oldSelectedOptions) => [
|
||||
...oldSelectedOptions,
|
||||
option,
|
||||
]);
|
||||
} else {
|
||||
@@ -97,8 +97,8 @@ export const SelectedOptionsProvider = ({
|
||||
if (isCustomOption) {
|
||||
removeCustomOption(option);
|
||||
} else {
|
||||
setSelectedOptions((prevSelectedOptions) =>
|
||||
prevSelectedOptions.filter(
|
||||
setSelectedOptions((oldSelectedOptions) =>
|
||||
oldSelectedOptions.filter(
|
||||
(selectedOption) => selectedOption !== option
|
||||
)
|
||||
);
|
||||
|
||||
@@ -467,6 +467,50 @@ export const TestThatCallbacksOnlyFireWhenExpected: StoryObj<{
|
||||
},
|
||||
};
|
||||
|
||||
export const TestCasingWhenAutoCompleting = {
|
||||
args: {
|
||||
onChange: jest.fn(),
|
||||
onClear: jest.fn(),
|
||||
onToggleSelected: jest.fn(),
|
||||
},
|
||||
render: (props) => {
|
||||
return (
|
||||
<UNSAFE_Combobox
|
||||
options={["Camel Case", "lowercase", "UPPERCASE"]}
|
||||
label="Liker du best store eller små bokstaver?"
|
||||
shouldAutocomplete
|
||||
allowNewValues
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const input = canvas.getByRole<HTMLInputElement>("combobox");
|
||||
|
||||
// With exisiting option
|
||||
userEvent.click(input);
|
||||
await userEvent.type(input, "cAmEl CaSe", { delay: 250 });
|
||||
await sleep(250);
|
||||
expect(input.value).toBe("cAmEl CaSe");
|
||||
await userEvent.type(input, "{Enter}");
|
||||
await sleep(250);
|
||||
const chips = canvas.getAllByRole("list")[0];
|
||||
const selectedUpperCaseChip = within(chips).getAllByRole("listitem")[0];
|
||||
expect(selectedUpperCaseChip).toHaveTextContent("Camel Case"); // A weird issue is preventing the accessible name from being used in the test, even if it works in VoiceOver
|
||||
|
||||
// With custom option
|
||||
userEvent.click(input);
|
||||
await userEvent.type(input, "cAmEl{Backspace}", { delay: 250 });
|
||||
await sleep(250);
|
||||
expect(input.value).toBe("cAmEl");
|
||||
await userEvent.type(input, "{Enter}");
|
||||
await sleep(250);
|
||||
const selectedNewValueChip = within(chips).getAllByRole("listitem")[0];
|
||||
expect(selectedNewValueChip).toHaveTextContent("cAmEl"); // A weird issue is preventing the accessible name from being used in the test, even if it works in VoiceOver
|
||||
},
|
||||
};
|
||||
|
||||
export const TestHoverAndFocusSwitching: StoryObject = {
|
||||
render: () => {
|
||||
return (
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useCallback, createContext, useContext } from "react";
|
||||
import { useInputContext } from "./Input/inputContext";
|
||||
import { useSelectedOptionsContext } from "./SelectedOptions/selectedOptionsContext";
|
||||
|
||||
type CustomOptionsContextType = {
|
||||
customOptions: string[];
|
||||
@@ -13,13 +12,19 @@ const CustomOptionsContext = createContext<CustomOptionsContextType>(
|
||||
{} as CustomOptionsContextType
|
||||
);
|
||||
|
||||
export const CustomOptionsProvider = ({ children }) => {
|
||||
export const CustomOptionsProvider = ({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
children: any;
|
||||
value: { isMultiSelect?: boolean };
|
||||
}) => {
|
||||
const [customOptions, setCustomOptions] = useState<string[]>([]);
|
||||
const { focusInput } = useInputContext();
|
||||
const { isMultiSelect } = useSelectedOptionsContext();
|
||||
const { isMultiSelect } = value;
|
||||
|
||||
const removeCustomOption = useCallback(
|
||||
(option) => {
|
||||
(option: string) => {
|
||||
setCustomOptions((prevCustomOptions) =>
|
||||
prevCustomOptions.filter((o) => o !== option)
|
||||
);
|
||||
@@ -29,7 +34,7 @@ export const CustomOptionsProvider = ({ children }) => {
|
||||
);
|
||||
|
||||
const addCustomOption = useCallback(
|
||||
(option) => {
|
||||
(option: string) => {
|
||||
if (isMultiSelect) {
|
||||
setCustomOptions((prevOptions) => [...prevOptions, option]);
|
||||
} else {
|
||||
|
||||
@@ -1,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";
|
||||
@@ -212,7 +212,7 @@ export const Search = forwardRef<HTMLInputElement, SearchProps>(
|
||||
`navds-search__input--${variant}`,
|
||||
"navds-text-field__input",
|
||||
"navds-body-short",
|
||||
`navds-body-${size}`
|
||||
`navds-body-short--${size}`
|
||||
)}
|
||||
{...(htmlSize ? { size: Number(htmlSize) } : {})}
|
||||
/>
|
||||
|
||||
@@ -1,59 +1,48 @@
|
||||
import { Meta } 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";
|
||||
export default {
|
||||
|
||||
const meta: Meta<typeof Textarea> = {
|
||||
title: "ds-react/Textarea",
|
||||
component: Textarea,
|
||||
argTypes: {
|
||||
size: {
|
||||
control: {
|
||||
type: "radio",
|
||||
options: ["medium", "small"],
|
||||
},
|
||||
},
|
||||
description: {
|
||||
type: "string",
|
||||
},
|
||||
error: {
|
||||
type: "string",
|
||||
},
|
||||
hideLabel: {
|
||||
type: "boolean",
|
||||
},
|
||||
disabled: {
|
||||
type: "boolean",
|
||||
},
|
||||
maxRows: {
|
||||
type: "number",
|
||||
},
|
||||
minRows: {
|
||||
type: "number",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
};
|
||||
export default meta;
|
||||
|
||||
export const Default = {
|
||||
export const Default: StoryObj<typeof Textarea> = {
|
||||
render: (props) => {
|
||||
return (
|
||||
<Textarea
|
||||
{...props}
|
||||
maxLength={props?.maxLength ?? 100}
|
||||
label="Ipsum enim quis culpa"
|
||||
/>
|
||||
);
|
||||
return <Textarea {...props} />;
|
||||
},
|
||||
|
||||
args: {
|
||||
maxLength: 100,
|
||||
resize: false,
|
||||
maxLength: 0,
|
||||
label: "Ipsum enim quis culpa",
|
||||
},
|
||||
|
||||
argTypes: {
|
||||
resize: {
|
||||
control: { type: "radio" },
|
||||
options: [true, "vertical", "horizontal"],
|
||||
},
|
||||
size: {
|
||||
control: { type: "radio" },
|
||||
options: ["medium", "small"],
|
||||
},
|
||||
description: { type: "string" },
|
||||
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
|
||||
@@ -69,7 +58,7 @@ export const Description = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Error = () => {
|
||||
export const Error: StoryFn = () => {
|
||||
return (
|
||||
<div className="colgap">
|
||||
<Textarea
|
||||
@@ -82,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 />
|
||||
@@ -95,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}
|
||||
@@ -117,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
|
||||
@@ -139,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" },
|
||||
};
|
||||
@@ -1,7 +1,7 @@
|
||||
import { InformationIcon } from "@navikt/aksel-icons";
|
||||
import { Meta } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { BodyLong, GuidePanel, VStack } from "../index";
|
||||
import { Meta } from "@storybook/react";
|
||||
import { InformationIcon } from "@navikt/aksel-icons";
|
||||
|
||||
export default {
|
||||
title: "ds-react/GuidePanel",
|
||||
|
||||
@@ -90,6 +90,7 @@ export const HelpText = forwardRef<HTMLButtonElement, HelpTextProps>(
|
||||
anchorEl={buttonRef.current}
|
||||
placement={placement}
|
||||
strategy={strategy}
|
||||
offset={12}
|
||||
>
|
||||
<Popover.Content className="navds-body-short">
|
||||
{children}
|
||||
|
||||
@@ -30,7 +30,7 @@ export const HelpTextIcon = ({
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="11"
|
||||
strokeWidth="2"
|
||||
strokeWidth="1.5"
|
||||
stroke="currentColor"
|
||||
fill={filled ? "currentColor" : "transparent"}
|
||||
/>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import {
|
||||
ResponsiveProp,
|
||||
SpacingScale,
|
||||
getResponsiveProps,
|
||||
} from "../utilities/css";
|
||||
import { getResponsiveProps } from "../utilities/css";
|
||||
import { ResponsiveProp, SpacingScale } from "../utilities/types";
|
||||
import { Slot } from "../../util/Slot";
|
||||
|
||||
export type BleedSpacingInline = "0" | "full" | "px" | SpacingScale;
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import { OverridableComponent } from "../../util/OverridableComponent";
|
||||
import { getResponsiveProps } from "../utilities/css";
|
||||
import {
|
||||
SpaceDelimitedAttribute,
|
||||
ResponsiveProp,
|
||||
SpacingScale,
|
||||
getResponsiveProps,
|
||||
} from "../utilities/css";
|
||||
import {
|
||||
BackgroundToken,
|
||||
BorderColorToken,
|
||||
ShadowToken,
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import React, { forwardRef, HTMLAttributes } from "react";
|
||||
import cl from "clsx";
|
||||
import {
|
||||
getResponsiveProps,
|
||||
getResponsiveValue,
|
||||
ResponsiveProp,
|
||||
SpacingScale,
|
||||
} from "../utilities/css";
|
||||
import { getResponsiveProps, getResponsiveValue } from "../utilities/css";
|
||||
import { ResponsiveProp, SpacingScale } from "../utilities/types";
|
||||
|
||||
export interface HGridProps extends HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
);
|
||||
Loaded 100 of 813 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user