Compare commits

..
Author SHA1 Message Date
Ken bcfd6747d9 📝 Changeset versions 2023-01-31 11:09:43 +01:00
Ken 122bbf0090 🐛 Date-fns import feilet fra ESM 2023-01-31 11:08:45 +01:00
Ken b60d047b95 Merge branch 'next' 2023-01-31 10:57:00 +01:00
github-actions[bot]andgithub-actions[bot] 98dbc155fe Version Packages (#1774)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-31 10:55:24 +01:00
Ken 87f194c1dc 📝 Changeset 2023-01-31 10:49:41 +01:00
Ken 7f73315264 🐛 useRangepicker edgecase ved valg av start etter slutt 2023-01-31 10:48:42 +01:00
Ken 1399f6f084 Merge branch 'next' of https://github.com/navikt/Designsystemet into next 2023-01-30 13:10:56 +01:00
Kai Erik SchierenandKen 1cf90c1753 Feature/suggestion box (#1773)
* 🎨 SuggestionBlock

* ✨ SuggestionBlock for grunnleggende/komponentsider

* 🎨 Flyttet options ut til eget object

* 🎨 optional chain suggestion block

* 🎨 add issue url to new icon

* 🎨 add urls for updates requests

* 🎨 add labels to links

* 🎨 add cml label to href

* 🎨 url to new and beta

* 🎨 add fitting labels to hrefs

* fix reference 🐛

* 💄 Oppdatert ikoner/heading for beta og Ny

---------

Co-authored-by: Ken <ken.aleksander@gmail.com>
2023-01-30 13:02:20 +01:00
Ken c027c2e464 ♿ role=img på ikoner fra sanity 2023-01-30 12:35:13 +01:00
Ken 92dd582d68 🐛 Bedre error-handling for feilede lenker 2023-01-30 11:34:18 +01:00
Ken bb302bbbd6 Merge branch 'master' of https://github.com/navikt/Designsystemet 2023-01-30 10:55:58 +01:00
Ken 55005c80f3 🐛 Fiket sanity-preview av Accordion 2023-01-30 09:58:02 +01:00
Ken 27e6423b54 Update README.md 2023-01-30 09:32:53 +01:00
Ken 21a842d419 🎨 Tilbakemeldingsvisning på 'forsiden' i sanity 2023-01-27 15:27:15 +01:00
github-actions[bot]andgithub-actions[bot] e7a62a9082 Version Packages (#1772)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-27 13:20:54 +01:00
Ken 494566e604 🐛 Monthpicker oppdaterte ikke år riktig (#1771) 2023-01-27 12:33:17 +01:00
Kai Erik Schieren 1c492fda56 🎨 add labels (#1770) 2023-01-27 11:53:01 +01:00
github-actions[bot]andgithub-actions[bot] 9691ac4713 Version Packages (#1768)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-27 11:51:25 +01:00
Ken 7174bbe7bf 🐛 Fikset visning av tema-grupperinger i sanity 2023-01-27 11:47:05 +01:00
Ken 62f36da5e7 📝 Changeset 2023-01-27 10:23:41 +01:00
Ken 9ec5133ba2 💄 +4px horisontal padding small textfield (team-aksel/issues/89) 2023-01-27 10:23:01 +01:00
Ken 0a7906187f 🔥 Linting 2023-01-27 10:17:56 +01:00
Ken 28f2a3f532 ♿ Aria-live polite på tilbakemeldinger 2023-01-27 10:16:54 +01:00
Ken f5d404b925 🐛 Fikset proptabell-feil med dl-lister 2023-01-27 10:00:06 +01:00
Ken 05955acaf7 ♿ Fikset tabindex scrolltop, skiplink 2023-01-27 09:51:05 +01:00
Ken 55362f3ea7 🎨 Fikset team-aksel#145 issue god-praksis forfatter og dato 2023-01-27 09:42:20 +01:00
Ken 28f8f7a408 🐛 System-theme light fungerte ikke i Sanity 2023-01-27 09:34:38 +01:00
Ken 6778e902f3 ♿ Fikset heading-nivå for artikkelkort på landingssider god-praksis 2023-01-26 16:04:36 +01:00
Ken 92566d5750 Merge branch 'master' of https://github.com/navikt/Designsystemet 2023-01-26 15:38:11 +01:00
Kai Erik Schieren 6911913748 ✨ cmp label (#1765) 2023-01-26 13:49:40 +01:00
Kai Erik Schieren 7f3e96ae01 🎨 add labels to issue templates (#1764) 2023-01-26 12:55:42 +01:00
Ken 19791c0339 Merge branch 'master' into next 2023-01-26 12:17:03 +01:00
Kai Erik Schieren 6a00fca823 Issue templates (#1763)
* ✨ new component issue template

* ✨ add templates for feeback icon + cmp
2023-01-26 09:40:54 +01:00
Ole Jørgen Bakken f5d3ed8eee Endret til textarea i svaralternativet (#1762) 2023-01-25 14:56:07 +01:00
Ole Jørgen Bakken 5c00b91dfb Legger til en issue form for nye ikoner (#1761) 2023-01-25 14:53:13 +01:00
Ken 5b82182fda 🔥 Fjernet gamle sanity-felt 2023-01-25 09:26:55 +01:00
Ken 6246c1d311 📝 Readmes 2023-01-25 09:18:46 +01:00
Ken 04ad4d09b2 ⬆️ Oppdatert sanity 2023-01-24 16:02:07 +01:00
Ken 65ddd4bd18 Merge branch 'master' into next 2023-01-24 15:47:30 +01:00
github-actions[bot]andgithub-actions[bot] 8bb5b8f7b9 Version Packages (#1760)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-24 11:40:38 +01:00
Ken dcf5cfc06b 🐛 Fikset typefeil introdusert i v2.0.6 for ikoner og loader (#1759) 2023-01-24 11:35:28 +01:00
Ken ff97e7f3f8 📝 Readmes 2023-01-24 09:50:21 +01:00
Ken 9e1165de0a 🎨 Oppdatert backup-token bruk 2023-01-24 09:47:25 +01:00
Ken 812573ac43 🎨 Visning av antall god-praksis tema på forside 2023-01-23 10:13:06 +01:00
Ken 5f340eba49 🎨 Bedre feilmelding SEO-tab 2023-01-23 08:50:07 +01:00
Ken 703e7e3ab5 Merge branch 'master' of https://github.com/navikt/Designsystemet 2023-01-20 14:05:28 +01:00
github-actions[bot]andgithub-actions[bot] b42101f6f1 Version Packages (#1757)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-20 14:04:04 +01:00
Ken 8b0c4d8f59 🐛 Fikset oppretting av ny profilside Sanity 2023-01-20 13:54:51 +01:00
6ab84e581f [FEATURE] Timeline interne-flater (#1665)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
Co-authored-by: Kai Erik Schieren <kai.schieren@soprasteria.com>
Co-authored-by: Kai Erik Schieren <111572054+kschieren@users.noreply.github.com>
2023-01-20 13:50:52 +01:00
Ken 42f3a298a2 🔥 Lint 2023-01-20 13:14:47 +01:00
Ken e49922d3fe ✨ Copy-knapp for tokens 2023-01-20 13:05:44 +01:00
Ken fc06e8f7b4 🎨 Default å tillate 2-siffer år (yy) 2023-01-20 12:41:51 +01:00
Ken 0273b42fb4 Revert "🎨 År med 2 tall er standard i datepicker nå"
This reverts commit aebb22bf1b.
2023-01-20 12:40:43 +01:00
Ken 0873fe6520 📝 changeset 2023-01-20 12:39:37 +01:00
Ken fdcf502a64 ♿ Aria-labels for sanity riktekst-editor 2023-01-20 12:34:46 +01:00
Ken aebb22bf1b 🎨 År med 2 tall er standard i datepicker nå 2023-01-20 12:32:38 +01:00
Ken 4529e64c6e ✨ Støtte for 'yy'-format i datepicker/monthpicker (#1755)
* 🎨 Støtte for 2-digit år

* 🎨 Props for toggling av twoDigitYear

* ✅ Tester for parsing av twoDigitYears

* 🎨 twoDigitYear demo

* 📝 Docs
2023-01-20 08:53:58 +01:00
Ken 01395e65ba 🎨 Utfaset gammel sanity-løsning 2023-01-19 21:45:29 +01:00
Ken 21dd0450e7 ⬆️ Yarn.lock 2023-01-19 21:15:04 +01:00
github-actions[bot]andgithub-actions[bot] ffac16cb05 Version Packages (#1756)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-19 15:53:44 +01:00
Ken 98177d0cd3 📝 changeset 2023-01-19 15:19:46 +01:00
Ken 2c5be718e6 🎨 Fikset hjelp ønskes modul 2023-01-19 10:00:56 +01:00
github-actions[bot]andgithub-actions[bot] 019d92dd7b Version Packages (#1754)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-18 15:24:10 +01:00
Ken 5b72afbb93 Merge branch 'master' of https://github.com/navikt/Designsystemet 2023-01-18 15:14:13 +01:00
Ken 56348cdddf ♿ Aria-labels for sanity riktekst-editor 2023-01-18 15:13:58 +01:00
github-actions[bot]andgithub-actions[bot] 973d79ee99 Version Packages (#1753)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-18 14:56:36 +01:00
Ken 5bfb912531 🎨 Bedre error-handling på akselartikkel templates 2023-01-18 14:55:45 +01:00
Ken 27e3dfee07 📝 Changeset 2023-01-18 13:08:38 +01:00
KenandKenAJoh 20b2bf6f89 Ikonsynk Figma (#1752)
Co-authored-by: KenAJoh <KenAJoh@users.noreply.github.com>
2023-01-18 13:07:48 +01:00
Ken 4b870986c8 🐛 Fikset intentbutton for feddbackløsning 2023-01-18 12:44:48 +01:00
Ken f51d43c06a Revert "✨ Ny tokenvisning"
This reverts commit ff2515fabb.
2023-01-10 10:44:41 +01:00
Ken 8c81ae46e0 Revert "💄 Copytoken med tooltip"
This reverts commit 21da657eb0.
2023-01-10 10:43:50 +01:00
Ken d64da78d8b Merge branch 'master' into next 2023-01-09 09:05:25 +01:00
Ken 21da657eb0 💄 Copytoken med tooltip 2023-01-05 11:03:20 +01:00
Ken 52d2d5f41d Merge branch 'tokens/refactor-v2' into next 2023-01-03 14:18:30 +01:00
Ken ff2515fabb ✨ Ny tokenvisning 2023-01-03 13:31:05 +01:00
Ken 23066e9503 ⬆️ Oppdatert aksel til å bruke clsx 2022-12-25 14:19:25 +01:00
144 changed files with 4944 additions and 970 deletions

No files matched your search

+24
View File
@@ -0,0 +1,24 @@
name: Ny komponent
description: Send et ønske om en eller flere nye komponenter
title: "[Ny komponent]: "
labels: ["nytt ✨", "forespørsel 🥰", "komponenter 🧩"]
assignees:
- olejorgenbakken
body:
- type: markdown
attributes:
value: |
Takk for at du bidrar inn i Aksel!
- type: input
id: title
attributes:
label: Navn på komponent
description: Hva er navnet på komponenten du ønsker deg?
placeholder: eks. datepicker
validations:
required: true
- type: textarea
id: examples
attributes:
label: Eksempel på komponenten
description: Send en lenke til eksempler på komponenten
+24
View File
@@ -0,0 +1,24 @@
name: Nytt ikon
description: Send et ønske om et eller flere nye ikoner
title: "[Nytt ikon]: "
labels: ["nytt ✨", "ikoner 🖼", "forespørsel 🥰"]
assignees:
- olejorgenbakken
body:
- type: markdown
attributes:
value: |
Takk for at du bidrar inn i Aksel!
- type: input
id: title
attributes:
label: Navn på ikonet
description: Hva er navnet på ikonet du ønsker deg?
placeholder: eks. send
validations:
required: true
- type: textarea
id: examples
attributes:
label: Eksempel på ikonet
description: Send en lenke til eksempler på ikonet
@@ -0,0 +1,16 @@
name: Innspill til komponent
description: Send inn innspill til komponent
title: "[Innspill til komponent]: "
labels: ["forespørsel 🥰", "komponenter 🧩", "nytt ✨", "beta 🧪"]
assignees:
- olejorgenbakken
body:
- type: markdown
attributes:
value: |
Takk for at du bidrar inn i Aksel!
- type: textarea
id: feeback
attributes:
label: Tilbakemelding
description: Beskriv hvilke tilbakemeldinger du har til komponenten
+16
View File
@@ -0,0 +1,16 @@
name: Innspill til ikon
description: Send inn innspill til ikon
title: "[Innspill til ikon]: "
labels: ["forespørsel 🥰", "nytt ✨"]
assignees:
- olejorgenbakken
body:
- type: markdown
attributes:
value: |
Takk for at du bidrar inn i Aksel!
- type: textarea
id: feeback
attributes:
label: Tilbakemelding
description: Beskriv hvilke tilbakemeldinger du har til ikonet
+1 -1
View File
@@ -26,7 +26,7 @@ jobs:
run: cd aksel.nav.no/website && yarn
- name: Export dataset
run: cd aksel.nav.no/website && SANITY_TOKEN=${{secrets.SANITY_DATASET_EDITOR_TOKEN}} yarn backup
run: cd aksel.nav.no/website && SANITY_PREVIEW_TOKEN=${{secrets.SANITY_DATASET_EDITOR_TOKEN}} yarn backup
- name: Upload GCP bucket
uses: "google-github-actions/auth@v0"
with:
+16
View File
@@ -1,5 +1,21 @@
# @navikt/ds-codemod
## 2.1.5
## 2.1.4
## 2.1.3
## 2.1.2
## 2.1.1
## 2.1.0
## 2.0.18
## 2.0.17
## 2.0.16
## 2.0.15
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-codemod",
"version": "2.0.16",
"version": "2.1.5",
"description": "Aksel codemods for NAVs designsystem",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
+2 -2
View File
@@ -39,8 +39,8 @@
"build-pict": "svgr --silent --index-template index-template.js --out-dir src/pictograms pictograms"
},
"dependencies": {
"@navikt/ds-icons": "^2.0.16",
"@navikt/ds-react": "^2.0.16",
"@navikt/ds-icons": "^2.1.5",
"@navikt/ds-react": "^2.1.5",
"clsx": "^1.1.1"
},
"devDependencies": {
+20
View File
@@ -1,5 +1,25 @@
# @navikt/ds-css
## 2.1.5
## 2.1.4
## 2.1.3
## 2.1.2
### Patch Changes
- [`62f36da5e`](https://github.com/navikt/Designsystemet/commit/62f36da5e74fc7f53414d42b310c3b57597795bc) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Small textfield har nå 8px horisontal padding (4px før)
## 2.1.1
## 2.1.0
## 2.0.18
## 2.0.17
## 2.0.16
## 2.0.15
+1 -1
View File
@@ -27,7 +27,7 @@
}
.navds-form-field--small .navds-text-field__input {
padding: 0 0.25rem;
padding: 0 0.5rem;
min-height: 32px;
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "2.0.16",
"version": "2.1.5",
"description": "CSS for NAV Designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -20,7 +20,7 @@
"watch": "postcss --watch --use postcss-import -o dist/index.css index.css"
},
"devDependencies": {
"@navikt/ds-tokens": "^2.0.16",
"@navikt/ds-tokens": "^2.1.5",
"normalize.css": "^8.0.1",
"postcss": "^8.4.0",
"postcss-cli": "^9.0.0",
+70
View File
@@ -1,5 +1,75 @@
# @navikt/ds-react
## 2.1.5
### Patch Changes
- Hotfix: ESM import fra date-fns feilet
- Updated dependencies []:
- @navikt/ds-icons@2.1.5
## 2.1.4
### Patch Changes
- [`87f194c1d`](https://github.com/navikt/Designsystemet/commit/87f194c1dc29b10a604de676b358d117e2dbf52f) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Fikset edgecase i useRangepicker der valg av startdato etter sluttdato ga feil output
- Updated dependencies []:
- @navikt/ds-icons@2.1.4
## 2.1.3
### Patch Changes
- [#1771](https://github.com/navikt/Designsystemet/pull/1771) [`494566e60`](https://github.com/navikt/Designsystemet/commit/494566e604e958363f75ebfa2adbc1669397636a) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Monthpicker håndterer nå visning av År riktig
- Updated dependencies []:
- @navikt/ds-icons@2.1.3
## 2.1.2
### Patch Changes
- Updated dependencies []:
- @navikt/ds-icons@2.1.2
## 2.1.1
### Patch Changes
- [#1759](https://github.com/navikt/Designsystemet/pull/1759) [`dcf5cfc06`](https://github.com/navikt/Designsystemet/commit/dcf5cfc06b751341559ef30085bd974820531e57) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Fikset @types/react v18 feil introdusert i v2.0.6
- Updated dependencies [[`dcf5cfc06`](https://github.com/navikt/Designsystemet/commit/dcf5cfc06b751341559ef30085bd974820531e57)]:
- @navikt/ds-icons@2.1.1
## 2.1.0
### Minor Changes
- [`0873fe652`](https://github.com/navikt/Designsystemet/commit/0873fe6520e817e462197c162b49c05826da4838) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Datepicker/Monthpicker støtter nå år med 2 siffer i input
### Patch Changes
- Updated dependencies []:
- @navikt/ds-icons@2.1.0
## 2.0.18
### Patch Changes
- [`98177d0cd`](https://github.com/navikt/Designsystemet/commit/98177d0cd3802029dcf398ce93c1767d3b0860c5) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Bedre typer for ref i datepicker
- Updated dependencies []:
- @navikt/ds-icons@2.0.18
## 2.0.17
### Patch Changes
- Updated dependencies [[`27e3dfee0`](https://github.com/navikt/Designsystemet/commit/27e3dfee07e135893e9f62cd32f80ed83a3510de)]:
- @navikt/ds-icons@2.0.17
## 2.0.16
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "2.0.16",
"version": "2.1.5",
"description": "Aksel react-components for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -37,7 +37,7 @@
},
"dependencies": {
"@floating-ui/react": "0.17.0",
"@navikt/ds-icons": "^2.0.16",
"@navikt/ds-icons": "^2.1.5",
"@radix-ui/react-tabs": "1.0.0",
"@radix-ui/react-toggle-group": "1.0.0",
"clsx": "^1.2.1",
@@ -164,6 +164,7 @@ export const UseRangedDatepicker = () => {
UNSAFE_useRangeDatepicker({
fromDate: new Date("Aug 23 2019"),
onRangeChange: console.log,
onValidate: console.log,
});
return (
@@ -49,6 +49,13 @@ export interface UseDatepickerOptions
* validation-callback
*/
onValidate?: (val: DateValidationT) => void;
/**
* Allows input of with 'yy' year format.
* @default true
* @Note Decision between 20th and 21st century is based on before(todays year - 80) ? 21st : 20th.
* In 2023 this equals to 1943 - 2042
*/
allowTwoDigitYear?: boolean;
}
interface UseDatepickerValue {
@@ -59,7 +66,10 @@ interface UseDatepickerValue {
/**
* Use: <DatePicker.Input {...inputProps}/>
*/
inputProps: Pick<DateInputProps, "onChange" | "onFocus" | "onBlur" | "value">;
inputProps: Pick<
DateInputProps,
"onChange" | "onFocus" | "onBlur" | "value"
> & { ref: React.RefObject<HTMLInputElement> };
/**
* Resets all states (callback)
*/
@@ -112,6 +122,7 @@ export const useDatepicker = (
inputFormat,
onValidate,
defaultMonth,
allowTwoDigitYear = true,
} = opt;
const locale = getLocaleFromString(_locale);
@@ -188,7 +199,13 @@ export const useDatepicker = (
const handleFocus: React.FocusEventHandler<HTMLInputElement> = (e) => {
!open && setOpen(true);
let day = parseDate(e.target.value, today, locale, "date");
let day = parseDate(
e.target.value,
today,
locale,
"date",
allowTwoDigitYear
);
if (isValidDate(day)) {
setMonth(day);
setInputValue(formatDateForInput(day, locale, "date", inputFormat));
@@ -196,7 +213,13 @@ export const useDatepicker = (
};
const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
let day = parseDate(e.target.value, today, locale, "date");
let day = parseDate(
e.target.value,
today,
locale,
"date",
allowTwoDigitYear
);
isValidDate(day) &&
setInputValue(formatDateForInput(day, locale, "date", inputFormat));
};
@@ -227,7 +250,13 @@ export const useDatepicker = (
// the calendar’s month.
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
setInputValue(e.target.value);
const day = parseDate(e.target.value, today, locale, "date");
const day = parseDate(
e.target.value,
today,
locale,
"date",
allowTwoDigitYear
);
const isBefore =
fromDate && day && differenceInCalendarDays(fromDate, day) > 0;
@@ -35,6 +35,13 @@ export interface UseMonthPickerOptions
* Default shown year
*/
defaultYear?: Date;
/**
* Allows input of with 'yy' year format.
* @default true
* @Note Decision between 20th and 21st century is based on before(todays year - 80) ? 21st : 20th.
* In 2023 this equals to 1943 - 2042
*/
allowTwoDigitYear?: boolean;
}
interface UseMonthPickerValue {
@@ -45,7 +52,9 @@ interface UseMonthPickerValue {
/**
* Use: <MonthPicker.Input {...inputProps} />
*/
inputProps: Pick<DateInputProps, "onChange" | "onFocus" | "value">;
inputProps: Pick<DateInputProps, "onChange" | "onFocus" | "value"> & {
ref: React.RefObject<HTMLInputElement>;
};
/**
* Currently selected Date
* Up to user to validate value and extract month
@@ -94,6 +103,7 @@ export const useMonthpicker = (
inputFormat,
onValidate,
defaultYear,
allowTwoDigitYear = true,
} = opt;
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
@@ -101,7 +111,7 @@ export const useMonthpicker = (
const today = new Date();
const locale = getLocaleFromString(_locale);
const inputRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const monthpickerRef = useRef<HTMLDivElement>(null);
// Initialize states
@@ -171,15 +181,29 @@ export const useMonthpicker = (
const handleFocus: React.FocusEventHandler<HTMLInputElement> = (e) => {
!open && setOpen(true);
let day = parseDate(e.target.value, today, locale, "month");
let day = parseDate(
e.target.value,
today,
locale,
"month",
allowTwoDigitYear
);
if (isValidDate(day)) {
setYear(day);
setInputValue(formatDateForInput(day, locale, "month", inputFormat));
} else {
setYear(defaultSelected ?? defaultYear ?? today);
}
};
const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
let day = parseDate(e.target.value, today, locale, "month");
let day = parseDate(
e.target.value,
today,
locale,
"month",
allowTwoDigitYear
);
isValidDate(day) &&
setInputValue(formatDateForInput(day, locale, "month", inputFormat));
};
@@ -189,12 +213,14 @@ export const useMonthpicker = (
if (month) {
setOpen(false);
inputRef.current && inputRef.current.focus();
setYear(month);
}
if (!required && !month) {
updateMonth(undefined);
updateValidation({ isValidMonth: false, isEmpty: true });
setInputValue("");
setYear(defaultYear ?? today);
return;
}
updateMonth(month);
@@ -209,7 +235,13 @@ export const useMonthpicker = (
// the calendar’s month.
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
setInputValue(e.target.value);
const month = parseDate(e.target.value, today, locale, "month");
const month = parseDate(
e.target.value,
today,
locale,
"month",
allowTwoDigitYear
);
const isBefore =
fromDate &&
@@ -1,6 +1,7 @@
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
import isBefore from "date-fns/isBefore";
import isWeekend from "date-fns/isWeekend";
import { useCallback, useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { DateRange, isMatch } from "react-day-picker";
import { DateInputProps } from "../DateInput";
import { DatePickerProps } from "../datepicker/DatePicker";
@@ -71,14 +72,14 @@ interface UseRangeDatepickerValue {
fromInputProps: Pick<
DateInputProps,
"onChange" | "onFocus" | "onBlur" | "value"
>;
> & { ref: React.RefObject<HTMLInputElement> };
/**
* Use: <DatePicker.Input label="to" {...toInputProps}/>
*/
toInputProps: Pick<
DateInputProps,
"onChange" | "onFocus" | "onBlur" | "value"
>;
> & { ref: React.RefObject<HTMLInputElement> };
/**
* Resets all states (callback)
*/
@@ -202,12 +203,13 @@ export const useRangeDatepicker = (
inputFormat,
onValidate,
defaultMonth,
allowTwoDigitYear = true,
} = opt;
const locale = getLocaleFromString(_locale);
const inputRefTo = useRef<HTMLDivElement>(null);
const inputRefFrom = useRef<HTMLDivElement>(null);
const inputRefTo = useRef<HTMLInputElement>(null);
const inputRefFrom = useRef<HTMLInputElement>(null);
const datePickerRef = useRef<HTMLDivElement | null>(null);
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
@@ -322,7 +324,13 @@ export const useRangeDatepicker = (
const handleFocus = (e, src: RangeT) => {
!open && setOpen(true);
let day = parseDate(e.target.value, today, locale, "date");
let day = parseDate(
e.target.value,
today,
locale,
"date",
allowTwoDigitYear
);
if (isValidDate(day)) {
setMonth(day);
src === RANGE.FROM
@@ -334,7 +342,13 @@ export const useRangeDatepicker = (
};
const handleBlur = (e, src: RangeT) => {
let day = parseDate(e.target.value, today, locale, "date");
let day = parseDate(
e.target.value,
today,
locale,
"date",
allowTwoDigitYear
);
if (!isValidDate(day)) {
return;
}
@@ -358,24 +372,26 @@ export const useRangeDatepicker = (
if (range?.from && range?.to) {
setOpen(false);
}
const prevToRange =
let prevToRange =
!selectedRange?.from && selectedRange?.to ? selectedRange?.to : range?.to;
const resetTo = isBefore(prevToRange, range?.from);
range?.from
? setFromInputValue(
formatDateForInput(range?.from, locale, "date", inputFormat)
)
: setFromInputValue("");
prevToRange
prevToRange && !resetTo
? setToInputValue(
formatDateForInput(prevToRange, locale, "date", inputFormat)
)
: setToInputValue("");
updateValidation(
{ isValidDate: !!range?.from, isEmpty: !range?.from },
{ isValidDate: !!range?.to, isEmpty: !prevToRange }
{ isValidDate: !!range?.to, isEmpty: !prevToRange || resetTo }
);
updateRange({ from: range?.from, to: prevToRange });
updateRange({ from: range?.from, to: resetTo ? undefined : prevToRange });
};
const fromChange = (
@@ -426,7 +442,13 @@ export const useRangeDatepicker = (
}
if (toInputValue && !selectedRange?.to) {
const toDay = parseDate(toInputValue, today, locale, "date");
const toDay = parseDate(
toInputValue,
today,
locale,
"date",
allowTwoDigitYear
);
if (validateDay(toDay)) {
updateRange({ from: day, to: toDay });
setMonth(day);
@@ -489,7 +511,13 @@ export const useRangeDatepicker = (
/* live-update datepicker based on changes in inputfields */
const handleChange = (e, src: RangeT) => {
const day = parseDate(e.target.value, today, locale, "date");
const day = parseDate(
e.target.value,
today,
locale,
"date",
allowTwoDigitYear
);
const isBefore = fromDate && differenceInCalendarDays(fromDate, day) > 0;
const isAfter = toDate && differenceInCalendarDays(day, toDate) > 0;
@@ -134,7 +134,7 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
);
const handleSelect = (month?: Date) => {
setSelectedMonth(month);
!onMonthSelect && setSelectedMonth(month);
onMonthSelect?.(month);
month && (onClose?.() ?? setOpen(false));
};
@@ -175,7 +175,7 @@ export const MonthPicker = forwardRef<HTMLDivElement, MonthPickerProps>(
>
<RootProvider
locale={getLocaleFromString(locale)}
selected={selected}
selected={selected ?? selectedMonth}
toDate={toDate}
fromDate={fromDate}
month={selected ?? selectedMonth}
@@ -2,7 +2,9 @@ import { parseDate } from "../parse-date";
import nb from "date-fns/locale/nb";
import { formatDateForInput } from "../format-date";
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date");
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date", false);
const parseTwoDigit = (inp: string) =>
parseDate(inp, new Date(), nb, "date", true);
describe("Format date to correct output", () => {
test("formatDateForInput", () => {
@@ -19,4 +21,12 @@ describe("Format date to correct output", () => {
"15.05.2022"
);
});
test("formatDateForInput with twoDigitYears", () => {
expect(formatDateForInput(parseTwoDigit("15/05/22"), nb, "date")).toEqual(
"15.05.2022"
);
expect(formatDateForInput(parseTwoDigit("1/5/95"), nb, "date")).toEqual(
"01.05.1995"
);
});
});
@@ -4,11 +4,14 @@ import nb from "date-fns/locale/nb";
import getMonth from "date-fns/getMonth";
const check = (inp: string) =>
expect(isValidDate(parseDate(inp, new Date(), nb, "date")));
expect(isValidDate(parseDate(inp, new Date(), nb, "date", false)));
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date");
const checkTwoDigit = (inp: string) =>
expect(isValidDate(parseDate(inp, new Date(), nb, "date", true)));
describe("Parse date-inputs", () => {
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date", false);
describe("Parse date-inputs with 4-digit years", () => {
test("No spaces", () => {
check("11052022").toBeTruthy();
check("15052022").toBeTruthy();
@@ -34,6 +37,44 @@ describe("Parse date-inputs", () => {
});
});
describe("Parse date-inputs with 2-digit years", () => {
test("No spaces", () => {
checkTwoDigit("110522").toBeTruthy();
checkTwoDigit("150522").toBeTruthy();
});
test(". divider", () => {
checkTwoDigit("1.5.22").toBeTruthy();
checkTwoDigit("11.05.22").toBeTruthy();
});
test("/ divider", () => {
checkTwoDigit("1/5/22").toBeTruthy();
checkTwoDigit("10/5/22").toBeTruthy();
checkTwoDigit("1/05/22").toBeTruthy();
checkTwoDigit("10/05/22").toBeTruthy();
});
test("- divider", () => {
checkTwoDigit("1-5-22").toBeTruthy();
checkTwoDigit("10-5-22").toBeTruthy();
checkTwoDigit("1-05-22").toBeTruthy();
checkTwoDigit("10-05-22").toBeTruthy();
});
test("Dissallow 1 and 3 digit years", () => {
checkTwoDigit("11052").toBeFalsy();
checkTwoDigit("1105222").toBeFalsy();
checkTwoDigit("1105999").toBeFalsy();
});
test("Dissallow date before year 1000", () => {
checkTwoDigit("11050999").toBeFalsy();
checkTwoDigit("11050010").toBeFalsy();
checkTwoDigit("11051000").toBeTruthy();
});
});
describe("Parse date-inputs to correct month", () => {
test("No spaces", () => {
expect(getMonth(parse("11052022"))).toEqual(4);
@@ -10,7 +10,7 @@ export const dateIsInCurrentMonth = (
/** @private */
export function isValidDate(day: Date): boolean {
return !isNaN(day.getTime());
return day && !isNaN(day?.getTime()) && day.getFullYear() > 999;
}
export const hasNextYear = (year: Date, years: Date[], val: any): boolean => {
+98 -19
View File
@@ -1,4 +1,6 @@
import parse from "date-fns/parse";
import isBefore from "date-fns/isBefore";
import sub from "date-fns/sub";
import { isValidDate } from ".";
export const INPUT_DATE_STRING_FORMAT_DATE = "dd.MM.yyyy";
@@ -22,7 +24,60 @@ const ALLOWED_INPUT_FORMATS_MONTH = [
...ALLOWED_INPUT_FORMATS_DATE,
];
const isTwoDigitYear = (dateString, today, locale, formats) => {
export const parseDate = (
date: string,
today: Date,
locale: Locale,
type: "date" | "month",
allowTwoDigitYear: boolean
): Date => {
let parsed;
const ALLOWED_FORMATS =
type === "date" ? ALLOWED_INPUT_FORMATS_DATE : ALLOWED_INPUT_FORMATS_MONTH;
if (allowTwoDigitYear) {
for (const format of ALLOWED_FORMATS) {
parsed = parse(date, format, today, { locale });
if (
isValidDate(parsed) &&
!isTwoDigitYear(date, today, locale, ALLOWED_FORMATS)
) {
return parsed;
}
}
for (const format of [
...ALLOWED_FORMATS.map((x) => x.replace("yyyy", "yy")),
]) {
parsed = parse(date, format, today, { locale });
if (
isValidDate(parsed) &&
isTwoDigitYear(date, today, locale, ALLOWED_FORMATS)
) {
const convertedDate = assignCenturyToDate(date, format, today, locale);
if (isValidDate(new Date(convertedDate))) {
return new Date(convertedDate);
} else {
return new Date("Invalid date");
}
}
}
return new Date("Invalid date");
} else {
for (const format of ALLOWED_FORMATS) {
parsed = parse(date, format, today, { locale });
if (
isValidDate(parsed) &&
!isTwoDigitYear(date, today, locale, ALLOWED_FORMATS)
) {
return parsed;
}
}
return new Date("Invalid date");
}
};
function isTwoDigitYear(dateString, today, locale, formats) {
let parsed;
const newFormat = formats.map((x) => x.replace("yyyy", "yy"));
for (const format of newFormat) {
@@ -32,25 +87,49 @@ const isTwoDigitYear = (dateString, today, locale, formats) => {
}
}
return false;
};
}
export const parseDate = (
date: string,
function assignCenturyToDate(
dateStr: string,
format: string,
today: Date,
locale: Locale,
type: "date" | "month"
): Date => {
let parsed;
const ALLOWED_FORMATS =
type === "date" ? ALLOWED_INPUT_FORMATS_DATE : ALLOWED_INPUT_FORMATS_MONTH;
for (const format of ALLOWED_FORMATS) {
parsed = parse(date, format, today, { locale });
if (
isValidDate(parsed) &&
!isTwoDigitYear(date, today, locale, ALLOWED_FORMATS)
) {
return parsed;
}
locale: Locale
) {
const date20Century = parse(
appendCenturyToTwoYearDigitDateString(dateStr, "19"),
format.replace("yy", "yyyy"),
today,
{ locale }
);
const date21Century = parse(
appendCenturyToTwoYearDigitDateString(dateStr, "20"),
format.replace("yy", "yyyy"),
today,
{ locale }
);
if (isValidDate(date20Century) && isValidDate(date21Century)) {
return isBefore(
date20Century,
sub(new Date(), {
years: 80,
})
)
? date21Century
: date20Century;
}
return new Date("Invalid date");
};
}
function appendCenturyToTwoYearDigitDateString(
dateString: string,
century: "19" | "20"
) {
const twoDigitYear = dateString.slice(-2);
return `${dateString.slice(
0,
dateString.length - 2
)}${century}${twoDigitYear}`;
}
@@ -6,11 +6,15 @@ interface SVGRProps {
titleId?: string;
}
export const DefaultIllustration = ({
export type DefaultIllustrationType = React.FunctionComponent<
SVGProps<SVGSVGElement> & SVGRProps
>;
export const DefaultIllustration: DefaultIllustrationType = ({
title,
titleId: _titleId,
...props
}: SVGProps<SVGSVGElement> & SVGRProps) => {
}) => {
let titleId: string | undefined = useId();
titleId = title ? (_titleId ? _titleId : "title-" + titleId) : undefined;
+6 -1
View File
@@ -33,7 +33,12 @@ export interface LoaderProps extends SVGProps<SVGSVGElement> {
variant?: "neutral" | "interaction" | "inverted";
}
export const Loader = forwardRef<SVGSVGElement, LoaderProps>(
/* Workaround for @types/react v17/v18 feil */
export type LoaderType = React.ForwardRefExoticComponent<
LoaderProps & React.RefAttributes<SVGSVGElement>
>;
export const Loader: LoaderType = forwardRef<SVGSVGElement, LoaderProps>(
(
{
children,
+16
View File
@@ -1,5 +1,21 @@
# @navikt/ds-tailwind
## 2.1.5
## 2.1.4
## 2.1.3
## 2.1.2
## 2.1.1
## 2.1.0
## 2.0.18
## 2.0.17
## 2.0.16
## 2.0.15
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tailwind",
"version": "2.0.16",
"version": "2.1.5",
"description": "Tailwind config based on @navikt/ds-tokens",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -22,7 +22,7 @@
"directory": "@navikt/core/tailwind"
},
"devDependencies": {
"@navikt/ds-tokens": "^2.0.16",
"@navikt/ds-tokens": "^2.1.5",
"@types/jest": "^29.0.0",
"color": "4.2.3",
"jest": "^29.0.0",
+21
View File
@@ -1,5 +1,26 @@
# @navikt/ds-tokens
## 2.1.5
## 2.1.4
## 2.1.3
## 2.1.2
## 2.1.1
## 2.1.0
### Patch Changes
- [#1665](https://github.com/navikt/Designsystemet/pull/1665) [`6ab84e581`](https://github.com/navikt/Designsystemet/commit/6ab84e581f8d3e850bcec340608354335506a051) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :sparkles: Ny Timeline-komponent for interne-flater
:bug: Fikset feil danger-hover token
## 2.0.18
## 2.0.17
## 2.0.16
## 2.0.15
+1 -1
View File
@@ -240,7 +240,7 @@
{ "name": "--a-surface-danger-subtle", "value": "rgba(255, 230, 230, 1)" },
{
"name": "--a-surface-danger-subtle-hover",
"value": "rgba(255, 230, 230, 1)"
"value": "rgba(255, 184, 184, 1)"
},
{ "name": "--a-surface-danger", "value": "rgba(195, 0, 0, 1)" },
{ "name": "--a-surface-danger-hover", "value": "rgba(173, 0, 0, 1)" },
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tokens",
"version": "2.0.16",
"version": "2.1.5",
"description": "Design-tokens for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
+1 -1
View File
@@ -159,7 +159,7 @@
"danger": {
"subtle": {
"hover": { "value": "{a.red.50.value}" },
"hover": { "value": "{a.red.100.value}" },
"@": { "value": "{a.red.50.value}" }
},
"hover": {
+24
View File
@@ -1,5 +1,29 @@
# @navikt/ds-icons
## 2.1.5
## 2.1.4
## 2.1.3
## 2.1.2
## 2.1.1
### Patch Changes
- [#1759](https://github.com/navikt/Designsystemet/pull/1759) [`dcf5cfc06`](https://github.com/navikt/Designsystemet/commit/dcf5cfc06b751341559ef30085bd974820531e57) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Fikset @types/react v18 feil introdusert i v2.0.6
## 2.1.0
## 2.0.18
## 2.0.17
### Patch Changes
- [`27e3dfee0`](https://github.com/navikt/Designsystemet/commit/27e3dfee07e135893e9f62cd32f80ed83a3510de) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Translate og Paragraph ikon
## 2.0.16
### Patch Changes
+20
View File
@@ -0,0 +1,20 @@
/**
* Fikser https://github.com/navikt/Designsystemet/issues/1758
*/
const fs = require("fs");
const files = fs.readdirSync(`./esm`).filter((x) => x.endsWith(".d.ts"));
files.forEach((file) => {
let data = fs.readFileSync(`./esm/${file}`).toString().split("\n");
data = data.map((x) => {
return x.includes("React.ForwardRefExoticComponent")
? x.split(":")[0] +
`: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement> & SVGRProps & React.RefAttributes<SVGSVGElement>>;`
: x;
});
fs.writeFileSync(`./esm/${file}`, data.join("\n"));
});
+26 -14
View File
@@ -867,13 +867,13 @@
"name": "Eu",
"description": "",
"created_at": "2022-02-08T11:45:47.104Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "EuFilled",
"description": "",
"created_at": "2022-02-08T11:45:47.109Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "Expand",
@@ -1479,13 +1479,13 @@
"name": "Law",
"description": "",
"created_at": "2020-07-01T10:37:42.819Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "LawFilled",
"description": "",
"created_at": "2021-01-11T13:37:55.330Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "Left",
@@ -1551,37 +1551,37 @@
"name": "Locked",
"description": "",
"created_at": "2020-07-01T10:37:43.027Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "LockedFilled",
"description": "",
"created_at": "2021-01-11T13:37:55.325Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "Login",
"description": "",
"created_at": "2020-07-01T10:37:43.087Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "LoginFilled",
"description": "",
"created_at": "2021-01-11T13:37:55.531Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "Logout",
"description": "",
"created_at": "2020-07-01T10:37:42.931Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "LogoutFilled",
"description": "",
"created_at": "2021-01-11T13:37:55.609Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "Male",
@@ -1811,6 +1811,12 @@
"created_at": "2021-12-15T15:16:55.869Z",
"pageName": "People"
},
{
"name": "Paragraph",
"description": "",
"created_at": "2023-01-17T13:24:22.989Z",
"pageName": "Privacy and security"
},
{
"name": "Parking",
"description": "",
@@ -1827,13 +1833,13 @@
"name": "Passport",
"description": "",
"created_at": "2020-07-01T10:37:43.078Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "PassportFilled",
"description": "",
"created_at": "2021-01-11T13:37:55.757Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "PensionBag",
@@ -2615,6 +2621,12 @@
"created_at": "2021-01-28T14:00:38.585Z",
"pageName": "Buildings"
},
{
"name": "Translate",
"description": "",
"created_at": "2023-01-18T10:52:22.311Z",
"pageName": "Interface"
},
{
"name": "Truck",
"description": "",
@@ -2643,13 +2655,13 @@
"name": "Unlocked",
"description": "",
"created_at": "2020-07-01T10:37:42.823Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "UnlockedFilled",
"description": "",
"created_at": "2021-01-11T13:37:55.317Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "Up",
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-icons",
"version": "2.0.16",
"version": "2.1.5",
"description": "Aksel icons for NAV",
"author": "Aksel | NAV designsystem team",
"repository": {
@@ -32,7 +32,7 @@
"create-package-json-pointers-to-esm": "node ../../scripts/createPackageJsonsWithESMPointers.js",
"create-icons": "svgr --silent --out-dir src svg",
"clean": "rimraf src cjs esm",
"build": "copyfiles util/* src && yarn create-icons && concurrently \"tsc\" \"tsc -p tsconfig.esm.json\" && yarn create-package-json-pointers-to-esm",
"build": "copyfiles util/* src && yarn create-icons && concurrently \"tsc\" \"tsc -p tsconfig.esm.json\" && node cleanTypes.js && yarn create-package-json-pointers-to-esm",
"update": "ts-node figma/index.ts dotenv/config",
"test": "jest"
},
+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C9.79085 2 7.99999 3.79086 7.99999 6C7.99999 6.66187 8.16311 7.29714 8.45828 7.85918C6.00646 9.12956 5.6173 13.1739 8.76271 14.4845L12.7692 16.1538C13.5145 16.4644 14 17.1926 14 18C14 19.1046 13.1046 20 12 20C10.8954 20 9.99999 19.1046 9.99999 18H7.99999C7.99999 20.2091 9.79085 22 12 22C14.2091 22 16 20.2091 16 18C16 17.3381 15.8369 16.7029 15.5417 16.1408C17.9935 14.8704 18.3827 10.8261 15.2373 9.51554L11.2308 7.84615C10.4855 7.53561 9.99999 6.8074 9.99999 6C9.99999 4.89543 10.8954 4 12 4C13.1046 4 14 4.89543 14 6H16C16 3.79086 14.2091 2 12 2ZM13.9738 14.5208C15.6075 14.4232 16.1245 12.0519 14.4681 11.3617L10.4615 9.69231C10.311 9.62957 10.1657 9.55832 10.0262 9.47924C8.39252 9.57681 7.87544 11.9481 9.53194 12.6383L13.5385 14.3077C13.689 14.3704 13.8343 14.4417 13.9738 14.5208Z" fill="#262626"/>
</svg>

After

Width:  |  Height:  |  Size: 965 B

+13
View File
@@ -0,0 +1,13 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.5 19.5L15.5 10.5L19.5 19.5" stroke="#262626" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13 17H18" stroke="#262626" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<mask id="mask0_6814_1365" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="2" y="4" width="15" height="17">
<path d="M9.90083 18.7893L13.9008 9.78926C14.1817 9.15728 14.8084 8.75 15.5 8.75C16.1193 8.75 16.6865 9.07657 17.0001 9.59861V4H2.00005V21H10.5983C9.85068 20.5519 9.53784 19.606 9.90083 18.7893Z" fill="black"/>
</mask>
<g mask="url(#mask0_6814_1365)">
<path d="M4 7.5H16" stroke="#262626" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M10 7.5V5.5" stroke="#262626" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M13.5 7.5L13.4116 7.78744C12.2005 11.7232 9.30115 14.9162 5.5 16.5" stroke="#262626" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6.5 7.5L6.58844 7.78744C7.79946 11.7232 10.6989 14.9162 14.5 16.5" stroke="#262626" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

+21
View File
@@ -1,5 +1,26 @@
# @navikt/ds-css-internal
## 2.1.5
## 2.1.4
## 2.1.3
## 2.1.2
## 2.1.1
## 2.1.0
### Minor Changes
- [#1665](https://github.com/navikt/Designsystemet/pull/1665) [`6ab84e581`](https://github.com/navikt/Designsystemet/commit/6ab84e581f8d3e850bcec340608354335506a051) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :sparkles: Ny Timeline-komponent for interne-flater
:bug: Fikset feil danger-hover token
## 2.0.18
## 2.0.17
## 2.0.16
## 2.0.15
+1
View File
@@ -1,3 +1,4 @@
@import "copy-to-clipboard.css";
@import "dropdown.css";
@import "header.css";
@import "timeline.css";
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css-internal",
"version": "2.0.16",
"version": "2.1.5",
"description": "CSS for NAV internal Designsystem-components",
"author": "Aksel | NAV designsystem team",
"keywords": [
+281
View File
@@ -0,0 +1,281 @@
.navdsi-timeline {
position: relative;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
width: 100%;
min-width: fit-content;
}
.navdsi-timeline__axislabels {
position: relative;
height: 1rem;
box-sizing: content-box;
grid-column: 2;
margin-bottom: var(--a-spacing-4);
margin-left: var(--a-spacing-8);
}
.navdsi-timeline__row-label {
white-space: nowrap;
width: auto;
margin-right: var(--a-spacing-4);
grid-column: 1;
height: 1.5rem;
display: flex;
align-items: center;
gap: var(--a-spacing-2);
}
.navdsi-timeline__axislabels-label {
position: absolute;
color: var(--ac-timeline-axislabel-text, var(--a-text-subtle));
white-space: nowrap;
}
.navdsi-timeline__row {
display: flex;
background: var(--ac-timeline-row-bg, var(--a-surface-subtle));
margin: 0 0 var(--a-spacing-8);
padding-left: var(--a-spacing-8);
grid-column: 2;
}
.navdsi-timeline__row--active {
background: var(--ac-timeline-row-active-bg, var(--a-surface-selected));
}
.navdsi-timeline__row:last-of-type {
margin-bottom: 0;
}
.navdsi-timeline__row-periods {
height: 1.5rem;
position: relative;
width: 100%;
margin: 0;
}
.navdsi-timeline__row-periods > li {
list-style-type: none;
}
.navdsi-timeline__period {
height: 100%;
border-radius: var(--a-border-radius-full);
position: absolute;
padding: 0;
display: flex;
align-items: center;
z-index: 2;
border: none;
font-size: 1rem;
}
.navdsi-timeline__period--inner {
margin: 0 var(--a-spacing-1) 0 var(--a-spacing-2);
overflow: hidden;
white-space: nowrap;
text-overflow: clip;
text-align: left;
height: 1rem;
}
/* .navdsi-timeline__period > * {
flex-shrink: 0;
} */
.navdsi-timeline__period--clickable {
cursor: pointer;
}
.navdsi-timeline__period--success {
background: var(--ac-timeline-period-success-bg, var(--a-surface-success-subtle));
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-success-border, var(--a-border-success));
}
.navdsi-timeline__period--success.navdsi-timeline__period--clickable:hover {
background: var(--a-surface-success-subtle-hover);
background: var(--ac-timeline-period-success-bg-hover, var(--a-surface-success-subtle-hover));
}
.navdsi-timeline__period--warning {
background: var(--ac-timeline-period-warning-bg, var(--a-surface-warning-subtle));
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-warning-border, var(--a-border-warning));
}
.navdsi-timeline__period--warning.navdsi-timeline__period--clickable:hover {
background: var(--ac-timeline-period-warning-bg-hover, var(--a-surface-warning-subtle-hover));
}
.navdsi-timeline__period--danger {
background: var(--ac-timeline-period-danger-bg, var(--a-surface-danger-subtle));
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-danger-border, var(--a-border-danger));
}
.navdsi-timeline__period--danger.navdsi-timeline__period--clickable:hover {
background: var(--ac-timeline-period-danger-bg-hover, var(--a-surface-danger-subtle-hover));
}
.navdsi-timeline__period--info {
background: var(--ac-timeline-period-info-bg, var(--a-surface-info-subtle));
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-info-border, var(--a-border-info));
}
.navdsi-timeline__period--info.navdsi-timeline__period--clickable:hover {
background: var(--ac-timeline-period-info-bg-hover, var(--a-surface-info-subtle-hover));
}
.navdsi-timeline__period--neutral {
background: var(--ac-timeline-period-neutral-bg, var(--a-bg-subtle));
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-neutral-border, var(--a-border-default));
}
.navdsi-timeline__period--neutral.navdsi-timeline__period--clickable:hover {
background: var(--ac-timeline-period-neutral-bg-hover, var(--a-gray-200));
}
.navdsi-timeline__period--connected-both {
border-radius: 0;
}
.navdsi-timeline__period--connected-right {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.navdsi-timeline__period--connected-left {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
.navdsi-timeline__period--selected {
box-shadow: inset 0 0 0 2px var(--ac-timeline-period-selected-border, var(--a-border-action-selected));
z-index: 3;
}
.navdsi-timeline__pin-wrapper {
position: absolute;
grid-column: 2;
top: 0;
z-index: 1;
height: 100%;
display: flex;
flex-direction: column-reverse;
isolation: isolate;
--navdsc-timeline-pin-size: 0.9rem;
}
.navdsi-timeline__pin-button {
border: none;
cursor: pointer;
background: var(--a-surface-default);
box-shadow: inset 0 0 0 1px var(--ac-timeline-pin-bg, var(--a-surface-danger)),
0 0 0 4px var(--ac-timeline-pin-bg, var(--a-surface-danger)), 0 0 0 5px var(--a-surface-default);
z-index: 2;
border-radius: var(--a-border-radius-full);
padding: 3.5px;
position: relative;
}
.navdsi-timeline__pin-button::before {
content: "";
inset: 0;
position: absolute;
width: 25px;
height: 25px;
border-radius: var(--a-border-radius-full);
left: calc(-12.5px + 3.5px);
top: calc(-12.5px + 3.5px);
}
.navdsi-timeline__pin-button:focus {
box-shadow: inset 0 0 0 1px var(--ac-timeline-pin-bg, var(--a-surface-danger)),
0 0 0 3px var(--ac-timeline-pin-bg, var(--a-surface-danger)), 0 0 0 4px var(--a-surface-default),
0 0 0 6px var(--a-border-focus);
outline: none;
}
.navdsi-timeline__pin-button:hover {
box-shadow: inset 0 0 0 1px var(--ac-timeline-pin-bg-hover, var(--a-surface-danger-hover)),
0 0 0 4px var(--ac-timeline-pin-bg-hover, var(--a-surface-danger-hover)), 0 0 0 5px var(--a-surface-default);
}
.navdsi-timeline__pin-button:hover:focus {
box-shadow: inset 0 0 0 1px var(--ac-timeline-pin-bg-hover, var(--a-surface-danger-hover)),
0 0 0 3px var(--ac-timeline-pin-bg-hover, var(--a-surface-danger-hover)), 0 0 0 4px var(--a-surface-default),
0 0 0 6px var(--a-border-focus);
}
.navdsi-timeline__pin-wrapper::before {
content: "";
top: var(--navdsc-timeline-pin-size);
height: calc(94% - var(--navdsc-timeline-pin-size));
width: 1px;
margin: 0 auto;
background: var(--a-surface-inverted);
}
.navdsi-timeline__zoom {
margin-top: var(--a-spacing-6);
float: right;
display: flex;
list-style-type: none;
}
.navdsi-timeline__zoom-button {
all: unset;
cursor: pointer;
padding: 6px 9px 6px 8px;
border: 1px solid var(--ac-timeline-zoom-border, var(--a-border-default));
border-width: 1px 0 1px 1px;
background: var(--ac-timeline-zoom-bg, var(--a-surface-default));
}
.navdsi-timeline__zoom li:first-child .navdsi-timeline__zoom-button {
border-radius: var(--a-border-radius-medium) 0 0 var(--a-border-radius-medium);
}
.navdsi-timeline__zoom li:last-child .navdsi-timeline__zoom-button {
border-width: 1px;
border-radius: 0 var(--a-border-radius-medium) var(--a-border-radius-medium) 0;
}
.navdsi-timeline__zoom li:only-child .navdsi-timeline__zoom-button {
border-radius: var(--a-border-radius-medium);
}
.navdsi-timeline__zoom-button:not([aria-pressed="true"]):hover {
background: var(--ac-timeline-zoom-hover, var(--a-surface-action-subtle-hover));
}
.navdsi-timeline__zoom-button[aria-pressed="true"] {
background: var(--ac-timeline-zoom-selected-bg, var(--a-surface-inverted));
color: var(--ac-timeline-zoom-selected-text, var(--a-text-on-inverted));
}
.navdsi-timeline__period:focus {
outline: none;
z-index: 4;
box-shadow: 0 0 0 2px var(--a-border-focus);
}
.navdsi-timeline__period--selected:focus {
box-shadow: inset 0 0 0 2px var(--ac-timeline-period-selected-border, var(--a-border-action-selected)),
0 0 0 2px var(--a-border-focus);
z-index: 3;
}
.navdsi-timeline__zoom li:focus-within {
z-index: var(--a-z-index-focus);
}
.navdsi-timeline__zoom-button:focus {
box-shadow: 0 0 0 1px var(--a-surface-default), 0 0 0 3px var(--a-border-focus);
border-width: 1px;
}
.navdsi-timeline__zoom li:not(:last-child) .navdsi-timeline__zoom-button:focus {
margin-right: 2px;
padding-right: 6px;
}
+28
View File
@@ -16,5 +16,33 @@
"--ac-header-text": "--a-text-on-inverted",
"--ac-header-hover-bg": "--a-surface-inverted-hover",
"--ac-header-active-bg": "--a-surface-inverted-active"
},
"timeline": {
"--ac-timeline-row-bg": "--a-surface-subtle",
"--ac-timeline-row-active-bg": "--a-surface-selected",
"--ac-timeline-period-success-bg": "--a-surface-success-subtle",
"--ac-timeline-period-success-border": "--a-border-success",
"--ac-timeline-period-success-bg-hover": "--a-surface-success-subtle-hover",
"--ac-timeline-period-warning-bg": "--a-surface-warning-subtle",
"--ac-timeline-period-warning-border": "--a-border-warning",
"--ac-timeline-period-warning-bg-hover": "--a-surface-warning-subtle-hover",
"--ac-timeline-period-danger-bg": "--a-surface-danger-subtle",
"--ac-timeline-period-danger-border": "--a-border-danger",
"--ac-timeline-period-danger-bg-hover": "--a-surface-danger-subtle-hover",
"--ac-timeline-period-info-bg": "--a-surface-info-subtle",
"--ac-timeline-period-info-border": "--a-border-info",
"--ac-timeline-period-info-bg-hover": "--a-surface-info-subtle-hover",
"--ac-timeline-period-neutral-bg": "--a-bg-subtle",
"--ac-timeline-period-neutral-border": "--a-border-default",
"--ac-timeline-period-neutral-bg-hover": "--a-gray-200",
"--ac-timeline-period-selected-border": "--a-border-action-selected",
"--ac-timeline-pin-bg": "--a-surface-danger",
"--ac-timeline-pin-bg-hover": "--a-surface-danger-hover",
"--ac-timeline-zoom-border": "--a-border-default",
"--ac-timeline-zoom-bg": "--a-surface-default",
"--ac-timeline-zoom-hover": "--a-surface-action-subtle-hover",
"--ac-timeline-zoom-selected-bg": "--a-surface-inverted",
"--ac-timeline-zoom-selected-text": "--a-text-on-inverted",
"--ac-timeline-axislabel-text": "--a-text-subtle"
}
}
+69
View File
@@ -1,5 +1,74 @@
# @navikt/ds-react-internal
## 2.1.5
### Patch Changes
- Updated dependencies []:
- @navikt/ds-react@2.1.5
- @navikt/ds-icons@2.1.5
## 2.1.4
### Patch Changes
- Updated dependencies [[`87f194c1d`](https://github.com/navikt/Designsystemet/commit/87f194c1dc29b10a604de676b358d117e2dbf52f)]:
- @navikt/ds-react@2.1.4
- @navikt/ds-icons@2.1.4
## 2.1.3
### Patch Changes
- Updated dependencies [[`494566e60`](https://github.com/navikt/Designsystemet/commit/494566e604e958363f75ebfa2adbc1669397636a)]:
- @navikt/ds-react@2.1.3
- @navikt/ds-icons@2.1.3
## 2.1.2
### Patch Changes
- Updated dependencies []:
- @navikt/ds-react@2.1.2
- @navikt/ds-icons@2.1.2
## 2.1.1
### Patch Changes
- Updated dependencies [[`dcf5cfc06`](https://github.com/navikt/Designsystemet/commit/dcf5cfc06b751341559ef30085bd974820531e57)]:
- @navikt/ds-react@2.1.1
- @navikt/ds-icons@2.1.1
## 2.1.0
### Minor Changes
- [#1665](https://github.com/navikt/Designsystemet/pull/1665) [`6ab84e581`](https://github.com/navikt/Designsystemet/commit/6ab84e581f8d3e850bcec340608354335506a051) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :sparkles: Ny Timeline-komponent for interne-flater
:bug: Fikset feil danger-hover token
### Patch Changes
- Updated dependencies [[`0873fe652`](https://github.com/navikt/Designsystemet/commit/0873fe6520e817e462197c162b49c05826da4838)]:
- @navikt/ds-react@2.1.0
- @navikt/ds-icons@2.1.0
## 2.0.18
### Patch Changes
- Updated dependencies [[`98177d0cd`](https://github.com/navikt/Designsystemet/commit/98177d0cd3802029dcf398ce93c1767d3b0860c5)]:
- @navikt/ds-react@2.0.18
- @navikt/ds-icons@2.0.18
## 2.0.17
### Patch Changes
- Updated dependencies [[`27e3dfee0`](https://github.com/navikt/Designsystemet/commit/27e3dfee07e135893e9f62cd32f80ed83a3510de)]:
- @navikt/ds-icons@2.0.17
- @navikt/ds-react@2.0.17
## 2.0.16
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react-internal",
"version": "2.0.16",
"version": "2.1.5",
"description": "Aksel react components for internal UIs in NAV",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -35,8 +35,8 @@
"watch": "tsc --watch -p tsconfig.esm.json"
},
"dependencies": {
"@navikt/ds-icons": "^2.0.16",
"@navikt/ds-react": "^2.0.16",
"@navikt/ds-icons": "^2.1.5",
"@navikt/ds-react": "^2.1.5",
"clsx": "^1.1.1",
"copy-to-clipboard": "^3.3.1"
},
+1
View File
@@ -1,3 +1,4 @@
export * from "./copy-to-clipboard";
export * from "./dropdown";
export * from "./header";
export * from "./timeline";
@@ -0,0 +1,142 @@
import { Detail } from "@navikt/ds-react";
import {
addDays,
addMonths,
addYears,
differenceInDays,
differenceInMonths,
differenceInYears,
endOfMonth,
endOfYear,
format,
startOfDay,
startOfMonth,
startOfYear,
subDays,
} from "date-fns";
import React from "react";
import { useTimelineContext } from "./hooks/useTimelineContext";
import { isVisible } from "./utils";
import { horizontalPositionAndWidth } from "./utils/calc";
import { AxisLabel } from "./utils/types.external";
export const dayLabels = (
start: Date,
end: Date,
totalDays: number,
direction: "left" | "right"
): AxisLabel[] => {
const increment = Math.ceil(totalDays / 10);
const lastDay = startOfDay(end);
return new Array(totalDays)
.fill(lastDay)
.map((thisDay, i) => {
if (i % increment !== 0) return null;
const day: Date = subDays(thisDay, i);
const { horizontalPosition, width } = horizontalPositionAndWidth(
day,
addDays(day, 1),
start,
end
);
return {
direction: direction,
horizontalPosition: horizontalPosition,
label: format(day, "MM.dd"),
date: day,
width: width,
};
})
.filter((label) => label !== null) as AxisLabel[];
};
export const monthLabels = (
start: Date,
end: Date,
direction: "left" | "right"
): AxisLabel[] => {
const startMonth = startOfMonth(start);
const endMonth = endOfMonth(end);
const numberOfMonths = differenceInMonths(endMonth, startMonth) + 1;
return new Array(numberOfMonths).fill(startMonth).map((thisMonth, i) => {
const month: Date = addMonths(thisMonth, i);
const { horizontalPosition, width } = horizontalPositionAndWidth(
month,
addMonths(month, 1),
start,
end
);
return {
direction: direction,
horizontalPosition: horizontalPosition,
label: format(month, "MMM yy"),
date: month,
width: width,
};
});
};
export const yearLabels = (
start: Date,
end: Date,
direction: "left" | "right"
): AxisLabel[] => {
const firstYear = startOfYear(start);
const lastYear = endOfYear(end);
const yearCount = differenceInYears(lastYear, start) + 1;
return new Array(yearCount).fill(firstYear).map((thisYear, i) => {
const year: Date = addYears(thisYear, i);
const { horizontalPosition, width } = horizontalPositionAndWidth(
year,
addYears(year, 1),
start,
end
);
return {
direction: direction,
horizontalPosition: horizontalPosition,
label: year.getFullYear().toString(),
date: year,
width: width,
};
});
};
const axisLabels = (
start: Date,
end: Date,
direction: "left" | "right"
): AxisLabel[] => {
const totalDays = differenceInDays(end, start);
if (totalDays < 40) {
return dayLabels(start, end, totalDays, direction);
} else if (totalDays < 370) {
return monthLabels(start, end, direction);
} else {
return yearLabels(start, end, direction);
}
};
export const AxisLabels = () => {
const { endDate, startDate, direction } = useTimelineContext();
const labels = axisLabels(startDate, endDate, direction).filter(isVisible);
return (
<div className="navdsi-timeline__axislabels" aria-hidden="true">
{labels.map((etikett) => (
<Detail
className="navdsi-timeline__axislabels-label"
as="div"
key={etikett.label}
style={{
justifyContent: direction === "left" ? "flex-start" : "flex-end",
[direction]: `${etikett.horizontalPosition}%`,
width: `${etikett.width}%`,
}}
>
{etikett.label}
</Detail>
))}
</div>
);
};
@@ -0,0 +1,66 @@
import { mergeRefs, Popover } from "@navikt/ds-react";
import { format } from "date-fns";
import React, { forwardRef, useMemo, useRef, useState } from "react";
import { useTimelineContext } from "./hooks/useTimelineContext";
import { position } from "./utils/calc";
import { TimelineComponentTypes } from "./utils/types.internal";
export interface TimelinePinProps {
/**
* Date position for the pin.
*/
date: Date;
/**
* Content in Pin Popover.
*/
children?: React.ReactNode;
}
export interface PinType
extends React.ForwardRefExoticComponent<
TimelinePinProps & React.RefAttributes<HTMLButtonElement>
> {
componentType: TimelineComponentTypes;
}
export const Pin = forwardRef<HTMLButtonElement, TimelinePinProps>(
({ date, children, ...rest }, ref) => {
const { startDate, endDate, direction } = useTimelineContext();
const [open, setOpen] = useState(false);
const buttonRef = useRef<HTMLButtonElement | null>(null);
const mergedRef = useMemo(() => mergeRefs([buttonRef, ref]), [ref]);
return (
<>
<div
className="navdsi-timeline__pin-wrapper"
style={{ [direction]: `${position(date, startDate, endDate)}%` }}
>
<button
{...rest}
ref={mergedRef}
className="navdsi-timeline__pin-button"
onClick={() => setOpen(!open)}
aria-label={`pin:${format(date, "dd.MM.yyyy")}`}
aria-expanded={open}
/>
</div>
{children && (
<Popover
open={open}
onClose={() => setOpen(false)}
anchorEl={buttonRef.current}
offset={18}
>
<Popover.Content>{children}</Popover.Content>
</Popover>
)}
</>
);
}
) as PinType;
Pin.componentType = "pin";
export default Pin;
@@ -0,0 +1,209 @@
import { endOfDay, isSameDay, startOfDay } from "date-fns";
import React, { forwardRef, useMemo, useRef, useState } from "react";
import { AxisLabels } from "./AxisLabels";
import { RowContext } from "./hooks/useRowContext";
import { TimelineContext } from "./hooks/useTimelineContext";
import {
useEarliestDate,
useLatestDate,
useTimelineRows,
} from "./hooks/useTimelineRows";
import Period, { PeriodType } from "./period";
import Pin, { PinType } from "./Pin";
import TimelineRow, { TimelineRowType } from "./TimelineRow";
import { parseRows } from "./utils/timeline";
import Zoom, { ZoomType } from "./zoom";
export interface TimelineProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
/**
* Decides startingpoint in timeline.
* Defaults to earliest date among the timeline periods.
*/
startDate?: Date;
/**
* Decides end-date for timeline.
* Defaults to the latest date among the timeline periods.
*/
endDate?: Date;
/**
* Decides direction which periods are sorted/displayed. "left" ascends from earliest date on left.
* @default "left"
*/
direction?: "left" | "right";
}
interface TimelineComponent
extends React.ForwardRefExoticComponent<TimelineProps> {
/**
* Built-in timeline row
*/
Row: TimelineRowType;
/**
* Built-in row period
*/
Period: PeriodType;
/**
* Built-in timeline pin
*/
Pin: PinType;
/**
* Built-in timeline zoom-component
*/
Zoom: ZoomType;
}
export const Timeline = forwardRef<HTMLDivElement, TimelineProps>(
({ children, startDate, endDate, direction = "left", ...rest }, ref) => {
const isMultipleRows = Array.isArray(children);
const firstFocusabled = useRef<
{ ref: HTMLButtonElement | null; id: number }[]
>([]);
if (!isMultipleRows) {
children = [children];
}
const rowChildren = React.Children.toArray(children).filter(
(c: any) => c?.type?.componentType === "row"
);
const pins = React.Children.toArray(children).filter(
(c: any) => c?.type?.componentType === "pin"
);
const zoomComponent = React.Children.toArray(children).find(
(c: any) => c?.type?.componentType === "zoom"
);
const rowsRaw = useMemo(() => {
return parseRows(rowChildren);
}, [rowChildren]);
const rows = rowsRaw.map((r) => {
if (r?.periods) {
return r.periods;
}
return [];
});
const initialStartDate = startOfDay(useEarliestDate({ startDate, rows }));
const [start, setStart] = useState(initialStartDate);
const [activeRow, setActiveRow] = useState<number | null>(null);
const [endInclusive, setEndInclusive] = useState(
endOfDay(useLatestDate({ endDate, rows }))
);
const initialEndDate = endOfDay(useLatestDate({ endDate, rows }));
const processedRows = useTimelineRows(
rowsRaw,
start,
endInclusive,
direction
);
const handleZoomChange = (zoomStart: Date) => {
if (direction === "left") {
if (isSameDay(zoomStart, start)) {
setStart(initialStartDate);
return;
}
setStart(zoomStart);
} else {
if (isSameDay(zoomStart, endInclusive)) {
setEndInclusive(initialEndDate);
return;
}
setEndInclusive(zoomStart);
}
};
const handleActiveRowChange = (key: string) => {
if (activeRow !== null && key === "ArrowDown") {
for (let i = activeRow + 1; i < processedRows.length; i++) {
const row = processedRows[i];
if (row.periods.find((p) => !!p.children || !!p.onSelectPeriod)) {
setActiveRow(i);
firstFocusabled.current.find((x) => x.id === i)?.ref?.focus();
break;
}
}
return;
}
if (activeRow !== null && key === "ArrowUp") {
for (let i = activeRow - 1; i >= 0; i--) {
const row = processedRows[i];
if (row.periods.find((p) => !!p.children || !!p.onSelectPeriod)) {
setActiveRow(i);
firstFocusabled.current.find((x) => x.id === i)?.ref?.focus();
break;
}
}
return;
}
};
const addFocusable = (ref: HTMLButtonElement | null, id: number) => {
let items = firstFocusabled.current;
items = items.filter((x) => x.id !== id);
items.push({ ref, id });
firstFocusabled.current = items;
};
return (
<TimelineContext.Provider
value={{
startDate: start,
endDate: endInclusive,
direction: direction,
setStart: (d) => handleZoomChange(d),
setEndInclusive: (d) => setEndInclusive(d),
activeRow: activeRow,
setActiveRow: (key) => handleActiveRowChange(key),
initiate: (i) => setActiveRow(i),
addFocusable,
}}
>
<div {...rest} ref={ref}>
<div className="navdsi-timeline">
<AxisLabels />
{pins.map((pin) => {
return pin;
})}
{processedRows.map((row, i) => {
return (
<RowContext.Provider
key={`row-${row.id}`}
value={{
periods: row.periods,
id: row.id,
active: activeRow === i,
index: i,
}}
>
<TimelineRow
{...row?.restProps}
ref={row?.ref}
label={row.label}
icon={row.icon}
headingTag={row.headingTag}
/>
</RowContext.Provider>
);
})}
</div>
{zoomComponent && zoomComponent}
</div>
</TimelineContext.Provider>
);
}
) as TimelineComponent;
Timeline.Row = TimelineRow;
Timeline.Period = Period;
Timeline.Pin = Pin;
Timeline.Zoom = Zoom;
export default Timeline;
@@ -0,0 +1,122 @@
import { BodyShort } from "@navikt/ds-react";
import { format } from "date-fns";
import React, { forwardRef } from "react";
import { PeriodContext } from "./hooks/usePeriodContext";
import { useRowContext } from "./hooks/useRowContext";
import { useTimelineContext } from "./hooks/useTimelineContext";
import Period from "./period";
import {
PositionedPeriod,
TimelineComponentTypes,
} from "./utils/types.internal";
import cl from "clsx";
export interface TimelineRowProps
extends React.HTMLAttributes<HTMLOListElement> {
/**
* Label for the timeline row
*/
label: string;
/**
* Heading level for the label e.g h2, h3...
* @default "h3"
*/
headingTag?: "h2" | "h3" | "h4" | "h5" | "h6";
/**
* Icon next to label
*/
icon?: React.ReactNode;
}
export interface TimelineRowType
extends React.ForwardRefExoticComponent<
TimelineRowProps & React.RefAttributes<HTMLOListElement>
> {
componentType: TimelineComponentTypes;
}
export const TimelineRow = forwardRef<HTMLOListElement, TimelineRowProps>(
({ label, headingTag = "h3", icon, ...rest }, ref) => {
const { periods, id, active } = useRowContext();
const { setActiveRow } = useTimelineContext();
const latest = periods.reduce((a, b) => {
return a.end > b.end ? a : b;
}, {} as PositionedPeriod);
const earliest = periods.reduce((a, b) => {
return a.end < b.end ? a : b;
}, {} as PositionedPeriod);
const firstFocusable = periods.find(
(p) => !!p.children || !!p.onSelectPeriod
);
return (
<>
{label && (
<BodyShort
as={headingTag}
id={`timeline-row-${id}`}
className="navdsi-timeline__row-label"
size="small"
>
{icon}
{label}
</BodyShort>
)}
<div
className={cl("navdsi-timeline__row", {
"navdsi-timeline__row--active": active,
})}
>
<ol
{...rest}
ref={ref}
aria-label={
periods.length === 0
? "Ingen perioder"
: `${format(earliest.start, "dd.MM.yyyy")} til ${format(
latest.end,
"dd.MM.yyyy"
)}`
}
className="navdsi-timeline__row-periods"
onKeyDown={(e) => {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
setActiveRow(e.key);
}
}}
>
{periods &&
periods.map((period) => {
return (
<li key={`period-${period.id}`}>
<PeriodContext.Provider
value={{
periodId: period.id,
firstFocus: firstFocusable?.id === period.id,
restProps: period?.restProps,
}}
>
<Period
start={period.start}
end={period.endInclusive}
icon={period.icon}
ref={period?.ref}
/>
</PeriodContext.Provider>
</li>
);
})}
</ol>
</div>
</>
);
}
) as TimelineRowType;
TimelineRow.componentType = "row";
export default TimelineRow;
@@ -0,0 +1,22 @@
import { createContext, useContext } from "react";
interface PeriodContextProps {
periodId: String;
firstFocus: boolean;
restProps?: any;
}
export const PeriodContext = createContext<PeriodContextProps>({
periodId: "",
firstFocus: false,
});
export const usePeriodContext = () => {
const context = useContext(PeriodContext);
if (!context) {
console.warn("usePeriodContext must be used with PeriodContext");
}
return context;
};
@@ -0,0 +1,26 @@
import { createContext, useContext } from "react";
import { PositionedPeriod } from "../utils/types.internal";
interface RowContextProps {
periods: PositionedPeriod[];
id: string;
active: boolean;
index: number;
}
export const RowContext = createContext<RowContextProps>({
periods: [],
id: "",
active: false,
index: 0,
});
export const useRowContext = () => {
const context = useContext(RowContext);
if (!context) {
console.warn("useRowContext must be used with RowContext");
}
return context;
};
@@ -0,0 +1,37 @@
import { createContext, useContext } from "react";
interface TimelineContextProps {
startDate: Date;
endDate: Date;
direction: "left" | "right";
setStart: (d: Date) => void;
setEndInclusive: (d: Date) => void;
activeRow: number | null;
setActiveRow: (i: string) => void;
initiate: (i: number) => void;
addFocusable: (ref: HTMLButtonElement | null, id: number) => void;
}
export const TimelineContext = createContext<TimelineContextProps>({
startDate: new Date(),
endDate: new Date(),
direction: "left",
setStart: () => null,
setEndInclusive: () => null,
activeRow: 0,
setActiveRow: () => null,
initiate: () => null,
addFocusable: () => null,
});
export const useTimelineContext = () => {
const context = useContext(TimelineContext);
if (!context) {
console.warn(
"useTimelineContext must be used with TimelineContext (<Timeline />)"
);
}
return context;
};
@@ -0,0 +1,161 @@
import { useMemo } from "react";
import { addDays, endOfDay, isAfter, startOfDay, subDays } from "date-fns";
import { lastPeriod } from "../utils/sort";
import { Period } from "../utils/types.external";
import {
InternalSimpleTimeline,
PositionedPeriod,
} from "../utils/types.internal";
import { horizontalPositionAndWidth } from "../utils/calc";
import { invisiblePeriods, withinADay } from "../utils/filter";
const spatialPeriod = (
period: Period,
timelineStart: Date,
timelineEndInclusive: Date,
direction: "left" | "right" = "left",
i: number,
periods: PositionedPeriod[],
rowIndex: number
): PositionedPeriod => {
const start = period.start;
const endInclusive = period.end;
const rightOverlap =
i < periods.length - 1 && !isAfter(periods[i + 1].start, endInclusive);
const { horizontalPosition, width } = horizontalPositionAndWidth(
startOfDay(start),
endOfDay(
rightOverlap ? startOfDay(subDays(periods[i + 1].start, 1)) : endInclusive
),
timelineStart,
timelineEndInclusive
);
return {
id: `r-${rowIndex}-p-${i}`,
start: start,
endInclusive: endInclusive,
horizontalPosition: horizontalPosition,
direction: direction,
width: width,
end: endInclusive,
status: period.status,
onSelectPeriod: period.onSelectPeriod,
icon: period.icon,
children: period.children,
isActive: period.isActive,
statusLabel: period.statusLabel,
};
};
const adjustedEdges = (
period: PositionedPeriod,
i: number,
allPeriods: PositionedPeriod[]
): PositionedPeriod => {
const left =
i > 0 && withinADay(period.start, allPeriods[i - 1].endInclusive);
const right =
i < allPeriods.length - 1 &&
withinADay(allPeriods[i + 1].start, period.endInclusive);
return left && right
? { ...period, cropped: "both" }
: left
? { ...period, cropped: "left" }
: right
? { ...period, cropped: "right" }
: period;
};
const trimmedPeriods = (period: PositionedPeriod) => {
let { horizontalPosition, width, cropped } = period;
if (horizontalPosition + width > 100) {
width = 100 - horizontalPosition;
cropped = cropped === "left" || cropped === "both" ? "both" : "right";
}
if (horizontalPosition < 0 && horizontalPosition + width > 0) {
width = horizontalPosition + width;
horizontalPosition = 0;
cropped = cropped === "right" || cropped === "both" ? "both" : "left";
}
return {
...period,
width: width,
horizontalPosition: horizontalPosition,
cropped: cropped,
};
};
export const useTimelineRows = (
rows: any,
startDate: Date,
endDate: Date,
direction: "left" | "right"
): InternalSimpleTimeline[] =>
useMemo(
() =>
rows.map((periods: InternalSimpleTimeline, i: number) => {
const rowIndex = i;
const timelinePeriods = periods.periods
.sort((a: Period, b: Period) => a.start.valueOf() - b.start.valueOf())
.map((period: Period & { restProps?: any; ref?: any }, i) => ({
...spatialPeriod(
period,
startDate,
endDate,
direction,
i,
periods.periods,
rowIndex
),
restProps: period?.restProps,
ref: period?.ref,
}))
.sort(lastPeriod)
.map(adjustedEdges)
.map(trimmedPeriods)
.filter(invisiblePeriods);
return {
id: `tl-row-${rowIndex}`,
label: periods.label,
headingTag: periods.headingTag || "h3",
icon: periods.icon,
periods:
direction === "left" ? timelinePeriods : timelinePeriods.reverse(),
restProps: periods?.restProps,
ref: periods?.ref,
};
}),
[rows, startDate, endDate, direction]
);
const earliestDate = (earliest: Date, period: Period) =>
period.start < earliest ? period.start : earliest;
const earliestFomDate = (rader: Period[][]) =>
rader.flat().reduce(earliestDate, new Date());
export const useEarliestDate = ({ startDate, rows }: any) =>
useMemo(
() => (startDate ? startDate : earliestFomDate(rows)),
[startDate, rows]
);
const latestDate = (latest: Date, period: Period) =>
period.end > latest ? period.end : latest;
const latestTomDate = (rows: Period[][]) =>
rows.flat().reduce(latestDate, new Date(0));
export const useLatestDate = ({ endDate, rows }: any) =>
useMemo(
() => (endDate ? endDate : addDays(latestTomDate(rows), 1)),
[endDate, rows]
);
@@ -0,0 +1,6 @@
export { default as Timeline } from "./Timeline";
export type { TimelineProps } from "./Timeline";
export type { TimelineRowProps } from "./TimelineRow";
export type { TimelinePinProps } from "./Pin";
export type { TimelinePeriodProps } from "./period";
export type { TimelineZoomButtonProps } from "./zoom/ZoomButton";
@@ -0,0 +1,91 @@
import { mergeRefs, Popover } from "@navikt/ds-react";
import cl from "clsx";
import React, { useMemo, useRef, useState } from "react";
import { usePeriodContext } from "../hooks/usePeriodContext";
import { useRowContext } from "../hooks/useRowContext";
import { useTimelineContext } from "../hooks/useTimelineContext";
import { ariaLabel, getConditionalClasses } from "../utils/period";
import { PeriodProps } from "./index";
interface TimelineClickablePeriodProps extends PeriodProps {
onSelectPeriod?: () => void;
isActive?: boolean;
periodRef: React.ForwardedRef<HTMLButtonElement>;
}
const ClickablePeriod = React.memo(
({
onSelectPeriod,
start,
end,
status,
cropped,
direction,
left,
width,
icon,
children,
isActive,
statusLabel,
restProps,
periodRef,
}: TimelineClickablePeriodProps) => {
const buttonRef = useRef<HTMLButtonElement | null>(null);
const [selected, setSelected] = useState(false);
const { index } = useRowContext();
const { firstFocus } = usePeriodContext();
const { initiate, addFocusable } = useTimelineContext();
const mergedRef = useMemo(
() => mergeRefs([buttonRef, periodRef]),
[periodRef]
);
return (
<>
<button
{...restProps}
ref={(r) => {
firstFocus && addFocusable(r, index);
mergedRef(r);
}}
onClick={() => {
children && setSelected((x) => !x);
onSelectPeriod?.();
}}
aria-label={ariaLabel(start, end, status, statusLabel)}
className={cl(
"navdsi-timeline__period--clickable",
getConditionalClasses(cropped, direction, status),
restProps?.className,
{
"navdsi-timeline__period--selected": isActive,
}
)}
style={{
width: `${width}%`,
[direction]: `${left}%`,
}}
aria-expanded={children ? selected : undefined}
onFocus={() => {
initiate(index);
}}
>
<div className="navdsi-timeline__period--inner">{icon}</div>
</button>
{children && (
<Popover
open={selected}
onClose={() => setSelected(false)}
anchorEl={buttonRef.current}
strategy="fixed"
>
<Popover.Content>{children}</Popover.Content>
</Popover>
)}
</>
);
}
);
export default ClickablePeriod;
@@ -0,0 +1,44 @@
import React from "react";
import { ariaLabel, getConditionalClasses } from "../utils/period";
import { PeriodProps } from "./index";
import cl from "clsx";
interface TimelineNonClickablePeriodProps extends PeriodProps {
periodRef?: React.ForwardedRef<HTMLDivElement>;
}
const NonClickablePeriod = ({
start,
end,
status,
cropped,
direction,
left,
width,
icon,
statusLabel,
restProps,
periodRef,
}: TimelineNonClickablePeriodProps) => {
return (
<div
ref={periodRef}
{...restProps}
className={cl(
getConditionalClasses(cropped, direction, status),
restProps?.classname
)}
style={{
width: `${width}%`,
[direction]: `${left}%`,
}}
>
<div className="navdsi-timeline__period--inner">
{icon}
<p className="sr-only">{ariaLabel(start, end, status, statusLabel)}</p>
</div>
</div>
);
};
export default NonClickablePeriod;
@@ -0,0 +1,130 @@
import React, { forwardRef } from "react";
import { usePeriodContext } from "../hooks/usePeriodContext";
import { useRowContext } from "../hooks/useRowContext";
import { TimelineComponentTypes } from "../utils/types.internal";
import ClickablePeriod from "./ClickablePeriod";
import NonClickablePeriod from "./NonClickablePeriod";
export interface TimelinePeriodProps
extends React.HTMLAttributes<HTMLDivElement> {
/**
* Period start date.
*/
start: Date;
/**
* Period end date.
*/
end: Date;
/**
* Icon for easier visual identification.
*/
icon?: React.ReactNode;
/**
* Period status.
* @default "neutral"
*/
status?: "success" | "warning" | "danger" | "info" | "neutral";
/**
* Status label for screen-readers
* e.g "Sykemeldt", "foreldrepermisjon"
*/
statusLabel?: string;
/**
* Callback when selecting a period.
*/
onSelectPeriod?: () => void;
/**
* Content displayed in Popover on click.
*/
children?: React.ReactNode;
/**
* Visual active inidcation on period
* @note Make sure only one period is active at a time
*/
isActive?: boolean;
}
export interface PeriodProps {
start: Date;
end: Date;
status: string;
cropped: string;
direction: string;
width: Number;
left: Number;
icon?: React.ReactNode;
children?: React.ReactNode;
statusLabel?: string;
restProps?: any;
}
export interface PeriodType
extends React.ForwardRefExoticComponent<
TimelinePeriodProps &
React.RefAttributes<HTMLDivElement | HTMLButtonElement>
> {
componentType: TimelineComponentTypes;
}
export const Period = forwardRef<HTMLDivElement, TimelinePeriodProps>(
({ icon }, ref) => {
const { periods } = useRowContext();
const { periodId, restProps } = usePeriodContext();
const period = periods.find((p) => p.id === periodId);
if (!period) {
return <></>;
}
const {
start,
endInclusive,
width,
horizontalPosition,
status = "neutral",
onSelectPeriod,
cropped,
direction,
children,
isActive,
statusLabel,
} = period;
return onSelectPeriod || children ? (
<ClickablePeriod
periodRef={ref as React.ForwardedRef<HTMLButtonElement>}
start={start}
end={endInclusive}
status={status}
onSelectPeriod={onSelectPeriod}
cropped={cropped || ""}
direction={direction}
width={width}
left={horizontalPosition}
icon={icon}
children={children}
isActive={isActive}
statusLabel={statusLabel}
restProps={restProps}
/>
) : (
<NonClickablePeriod
periodRef={ref}
start={start}
end={endInclusive}
status={status}
cropped={cropped || ""}
direction={direction}
width={width}
left={horizontalPosition}
icon={icon}
statusLabel={statusLabel}
restProps={restProps}
/>
);
}
) as PeriodType;
Period.componentType = "period";
export default Period;
@@ -0,0 +1,326 @@
import { SuccessFilled } from "@navikt/ds-icons";
import { useState } from "@storybook/addons";
import { Meta } from "@storybook/react";
import * as React from "react";
import Timeline from "./Timeline";
export default {
title: "ds-react-internal/Timeline",
component: Timeline,
argTypes: {},
} as Meta;
const DummyLabel = () => {
return (
<div
style={{
width: "239px",
margin: 0,
textAlign: "left",
fontSize: "14px",
}}
>
<p>Periode: 15.07.2019 - 25.07.2019</p>
<p>Utbetalt: 12 345,00 kr</p>
<p style={{ color: "red" }}>Dager igjen: 0</p>
</div>
);
};
const row1 = [
{
id: "1",
start: new Date("Jan 1 2022"),
end: new Date("Jan 31 2022"),
status: "success",
icon: <SuccessFilled aria-hidden />,
statusLabel: "Sykemeldt",
},
{
id: "2",
start: new Date("Feb 1 2022"),
end: new Date("Mar 15 2022"),
status: "danger",
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
statusLabel: "Sykemeldt",
},
{
id: "3",
start: new Date("May 2 2022"),
end: new Date("May 25 2022"),
status: "success",
icon: <SuccessFilled aria-hidden />,
statusLabel: "Sykemeldt",
},
{
id: "4",
start: new Date("Mar 1 2022"),
end: new Date("Mar 31 2022"),
status: "success",
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
icon: <SuccessFilled aria-hidden />,
label: "test",
statusLabel: "Sykemeldt",
},
{
id: "5",
start: new Date("Jul 1 2022"),
end: new Date("Jul 31 2022"),
status: "warning",
icon: <SuccessFilled aria-hidden />,
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
statusLabel: "Sykemeldt",
},
{
id: "6",
start: new Date("Aug 1 2022"),
end: new Date("Aug 30 2022"),
status: "warning",
icon: <SuccessFilled aria-hidden />,
statusLabel: "Sykemeldt",
},
];
const row2 = [
{
id: "7",
start: new Date("May 13 2022"),
end: new Date("May 25 2022"),
status: "warning",
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
icon: <SuccessFilled aria-hidden />,
children: <DummyLabel />,
},
{
id: "8",
start: new Date("Feb 1 2022"),
end: new Date("May 2 2022"),
status: "neutral",
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
icon: <SuccessFilled aria-hidden />,
children: <DummyLabel />,
},
];
export const Default = () => {
return (
<div style={{ width: "80vw" }}>
<Timeline>
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
{row1.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
{row2.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
</Timeline>
</div>
);
};
export const WithPins = () => {
return (
<div style={{ width: "80vw" }}>
<Timeline>
<Timeline.Pin date={new Date("Apr 15 2022")}>Pin 1</Timeline.Pin>
<Timeline.Pin date={new Date("Jun 12 2022")}>Pin 2</Timeline.Pin>
<Timeline.Pin date={new Date("Jul 28 2022")}>Pin 3</Timeline.Pin>
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
{row1.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
{row2.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
</Timeline>
</div>
);
};
export const WithZoom = () => {
return (
<div style={{ width: "80vw" }}>
<Timeline>
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
{row1.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
{row2.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
<Timeline.Zoom>
<Timeline.Zoom.Button label="3 mnd" interval="month" count={3} />
<Timeline.Zoom.Button label="7 mnd" interval="month" count={7} />
<Timeline.Zoom.Button label="9 mnd" interval="month" count={9} />
</Timeline.Zoom>
</Timeline>
</div>
);
};
export const ActivePeriod = () => {
const [activePeriod, setActivePeriod] = useState(row1[0]);
return (
<div style={{ width: "80vw" }}>
<Timeline>
<Timeline.Pin date={new Date("Apr 15 2022")}>Pin 1</Timeline.Pin>
<Timeline.Pin date={new Date("Jun 12 2022")}>Pin 2</Timeline.Pin>
<Timeline.Pin date={new Date("Jul 28 2022")}>Pin 3</Timeline.Pin>
<Timeline.Row label="Rad 1" icon={<SuccessFilled aria-hidden />}>
{row1.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
onSelectPeriod={() => {
setActivePeriod(p);
p?.onSelectPeriod?.();
}}
icon={p.icon}
isActive={activePeriod.id === p.id}
statusLabel={p.statusLabel}
>
{p.children}
</Timeline.Period>
);
})}
</Timeline.Row>
<Timeline.Row label="Rad 2">
{row2.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
onSelectPeriod={() => {
setActivePeriod(p);
p?.onSelectPeriod?.();
}}
icon={p.icon}
isActive={activePeriod.id === p.id}
statusLabel={p.statusLabel}
>
{p.children}
</Timeline.Period>
);
})}
</Timeline.Row>
<Timeline.Row label="Rad 3">
<Timeline.Period
start={new Date("Jan 1 2022")}
end={new Date("Aug 20 2022")}
status="info"
icon={<SuccessFilled aria-hidden />}
/>
</Timeline.Row>
</Timeline>
</div>
);
};
export const WithDayLabels = () => {
return (
<div style={{ width: "80vw" }}>
<Timeline>
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
<Timeline.Period
start={new Date("Feb 4 2022")}
end={new Date("Feb 13 2022")}
status="success"
/>
</Timeline.Row>
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
<Timeline.Period
start={new Date("Feb 17 2022")}
end={new Date("Feb 22 2022")}
status="warning"
/>
</Timeline.Row>
</Timeline>
</div>
);
};
export const WithYearLabels = () => {
return (
<div style={{ width: "80vw" }}>
<Timeline>
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
<Timeline.Period
start={new Date("Feb 4 2022")}
end={new Date("Feb 13 2028")}
status="success"
/>
</Timeline.Row>
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
<Timeline.Period
start={new Date("Feb 17 2024")}
end={new Date("Feb 22 2031")}
status="warning"
/>
</Timeline.Row>
</Timeline>
</div>
);
};
@@ -0,0 +1,26 @@
import { differenceInMilliseconds } from "date-fns";
export const position = (date: Date, start: Date, endInclusive: Date) => {
const diff = differenceInMilliseconds(endInclusive, start);
return (differenceInMilliseconds(date, start) / diff) * 100;
};
export const horizontalPositionAndWidth = (
start: Date,
endInclusive: Date,
timelineStart: Date,
timelineEndInclusive: Date
) => {
const horizontalPosition = position(
start,
timelineStart,
timelineEndInclusive
);
const width =
position(endInclusive, timelineStart, timelineEndInclusive) -
horizontalPosition;
return {
horizontalPosition: horizontalPosition,
width: width,
};
};
@@ -0,0 +1,32 @@
import { differenceInDays } from "date-fns";
import { PeriodProps } from "../period";
import { Spatial } from "./types.internal";
export interface Positioned {
horizontalPosition: number;
direction: "left" | "right";
}
export const isVisible = ({ horizontalPosition }: Positioned): boolean =>
horizontalPosition <= 98 && horizontalPosition >= 0;
export const getFirstDate = (periods: any) => {
return periods.sort(
(a: PeriodProps, b: PeriodProps) => a.start.getTime() - b.start.getTime()
)[0].start;
};
export const getLastDate = (periods: any) => {
return periods.sort(
(a: PeriodProps, b: PeriodProps) => a.end.getTime() - b.end.getTime()
)[periods.length - 1].end;
};
export const withinADay = (date1: Date, date2: Date): boolean =>
differenceInDays(date1, date2) <= 1;
export const invisiblePeriods = ({
horizontalPosition,
width,
}: Positioned & Spatial) =>
horizontalPosition >= 0 && horizontalPosition <= 100 && width > 0;
@@ -0,0 +1 @@
export { getFirstDate, getLastDate, isVisible, Positioned } from "./filter";
@@ -0,0 +1,48 @@
import cl from "clsx";
import { format } from "date-fns";
export const getConditionalClasses = (
cropped: string,
direction: string,
status: string
) => {
return cl(`navdsi-timeline__period navdsi-timeline__period--${status}`, {
"navdsi-timeline__period--connected-both": cropped === "both",
"navdsi-timeline__period--connected-right":
(cropped === "right" && direction === "left") ||
(cropped === "left" && direction === "right"),
"navdsi-timeline__period--connected-left":
(cropped === "left" && direction === "left") ||
(cropped === "right" && direction === "right"),
});
};
const translateStatus = (status: string): string => {
switch (status) {
case "success":
return "Suksess";
case "warning":
return "Advarsel";
case "danger":
return "Fare";
case "info":
return "Info";
case "neutral":
return "Nøytral";
default:
return status;
}
};
export const ariaLabel = (
startDate: Date,
endDate: Date,
status: string,
statusLabel?: string
): string => {
const start = format(startDate, "dd.MM.yyyy");
const end = format(endDate, "dd.MM.yyyy");
return `${
statusLabel ? statusLabel : translateStatus(status)
} fra ${start} til ${end}`;
};
@@ -0,0 +1,11 @@
import { differenceInMilliseconds } from "date-fns";
import { Period, PositionedPeriod } from "./types.internal";
export const lastDate = (a: Date, b: Date): number =>
differenceInMilliseconds(a, b);
export const lastPeriod = (a: PositionedPeriod, b: PositionedPeriod): number =>
a.horizontalPosition - b.horizontalPosition;
export const lastSinglePeriod = (a: Period, b: Period): number =>
differenceInMilliseconds(b.endInclusive, a.endInclusive);
@@ -0,0 +1,83 @@
import { omit } from "@navikt/ds-react";
import React, { ReactNode } from "react";
import { Period } from "./types.external";
type ParsedChild = {
label?: string;
icon?: React.ReactNode;
headingTag: string;
periods: Omit<Period, "id" | "endInclusive">[];
restProps: any;
ref: any;
};
export const parseRows = (rowChildren: ReactNode[]) => {
let parsedChildren: ParsedChild[] = [];
rowChildren?.forEach((r: React.ReactNode) => {
let periods = [];
if (React.isValidElement(r) && r?.props?.children) {
if (Array.isArray(r.props.children)) {
for (let i = 0; i < r.props.children.length; i++) {
const p = r.props.children[i];
periods.push({
start: p?.props?.start,
end: p?.props?.end,
status: p?.props?.status || "neutral",
onSelectPeriod: p.props?.onSelectPeriod,
label: r.props.label,
icon: p.props.icon,
children: p.props.children,
isActive: p.props.isActive,
statusLabel: p.props.statusLabel,
restProps: omit(p.props, [
"start",
"end",
"status",
"onSelectPeriod",
"label",
"icon",
"children",
"isActive",
"statusLabel",
]),
ref: p?.ref,
});
}
} else {
periods.push({
start: r.props.children.props.start,
end: r.props.children.props.end,
status: r.props.children.props?.status || "neutral",
onSelectPeriod: r.props.children.props?.onSelectPeriod,
label: r.props.label,
icon: r.props.children.props?.icon,
children: r.props.children.props?.children,
statusLabel: r.props.children.props?.statusLabel,
restProps: omit(r.props.children.props, [
"start",
"end",
"status",
"onSelectPeriod",
"label",
"icon",
"children",
"isActive",
"statusLabel",
]),
ref: r.props?.children?.ref,
});
}
parsedChildren.push({
label: r.props.label,
icon: r.props.icon,
headingTag: r.props.headingTag,
periods: periods,
restProps: omit(r.props, ["label", "icon", "headingTag"]),
ref: (r as any)?.ref,
});
}
});
return parsedChildren;
};
@@ -0,0 +1,65 @@
import React from "react";
export type PeriodStatus =
| "success"
| "warning"
| "danger"
| "info"
| "neutral";
export type Percentage = number;
export interface Positioned {
horizontalPosition: number;
direction: "left" | "right";
}
export interface Period {
id: string;
label?: string;
start: Date;
endInclusive: Date;
status?: PeriodStatus;
onSelectPeriod?: () => void;
icon?: React.ReactNode;
children?: React.ReactNode;
end: Date;
isActive?: boolean;
statusLabel?: string;
}
export interface PositionedPeriod extends Period, Positioned {
width: number;
active?: boolean;
cropped?: "left" | "right" | "both";
disabled?: boolean;
className?: string;
hoverLabel?: React.ReactNode;
infoPin?: boolean;
start: Date;
}
export interface Spatial {
width: number;
}
export interface AxisLabel extends Positioned, Spatial {
label: string;
date: Date;
}
export interface InternalSimpleTimeline {
id: string;
periods: PositionedPeriod[];
}
export interface SimplePeriod {
/**
* Startdato for perioden, mao. periodens høyre kant.
*/
start: Date;
/**
* Sluttdato for perioden, mao. periodens venstre kant.
*/
end: Date;
}
@@ -0,0 +1,76 @@
import React from "react";
export type TimelineComponentTypes = "row" | "pin" | "period";
export type PeriodStatus =
| "success"
| "warning"
| "danger"
| "info"
| "neutral";
export type Percentage = number;
export interface Positioned {
horizontalPosition: number;
direction: "left" | "right";
}
export interface Period {
id: string;
start: Date;
endInclusive: Date;
status?: PeriodStatus;
onSelectPeriod?: () => void;
icon?: React.ReactNode;
children?: React.ReactNode;
isActive?: boolean;
}
export interface PositionedPeriod extends Period, Positioned {
width: number;
active?: boolean;
cropped?: "left" | "right" | "both";
disabled?: boolean;
className?: string;
hoverLabel?: React.ReactNode;
infoPin?: boolean;
end: Date;
statusLabel?: string;
restProps?: any;
ref?: any;
}
export interface Spatial {
width: number;
}
export interface AxisLabel extends Positioned, Spatial {
label: string;
date: Date;
}
export interface InternalSimpleTimeline {
label: string;
id: string;
periods: PositionedPeriod[];
icon?: React.ReactNode;
headingTag: "h2" | "h3" | "h4" | "h5" | "h6";
restProps: any;
ref: any;
}
export interface SelectedPeriod {
/**
* Start of the period.
*/
start: Date;
/**
* End of the period.
*/
end: Date;
/**
* Period id
*/
id: string;
}
@@ -0,0 +1,82 @@
import { Detail } from "@navikt/ds-react";
import {
addMonths,
addYears,
format,
isSameDay,
subMonths,
subYears,
} from "date-fns";
import React, { forwardRef } from "react";
import { useTimelineContext } from "../hooks/useTimelineContext";
export interface TimelineZoomButtonProps {
/**
* e.g 3mnd, 6mnd etc
*/
label: string;
/**
* Zoom interval in months or years
*/
interval: "month" | "year";
/**
* How many units of the interval that will be applied
* e.g interval="month" + count={3} equals 3 months
*/
count: number;
}
export type ZoomButtonType = React.ForwardRefExoticComponent<
TimelineZoomButtonProps & React.RefAttributes<HTMLButtonElement>
>;
export const ZoomButton: ZoomButtonType = forwardRef(
({ label, interval, count, ...rest }, ref) => {
const { setStart, endDate, startDate, direction } = useTimelineContext();
let startOfRange: Date;
if (interval === "month") {
startOfRange =
direction === "left"
? subMonths(endDate, count)
: addMonths(startDate, count);
} else {
startOfRange =
direction === "left"
? subYears(endDate, count)
: addYears(startDate, count);
}
const currentZoom =
direction === "left"
? isSameDay(startDate, startOfRange)
: isSameDay(endDate, startOfRange);
return (
<Detail as="li">
<button
aria-label={
!currentZoom
? `Zoom tidslinjen ${format(
startOfRange,
"dd.MM.yyyy"
)} til ${format(endDate, "dd.MM.yyyy")}`
: "Tilbakestill til initiell tidsperspektiv"
}
ref={ref}
{...rest}
className="navdsi-timeline__zoom-button"
onClick={() => {
setStart(startOfRange);
}}
aria-pressed={currentZoom}
>
{label}
</button>
</Detail>
);
}
);
export default ZoomButton;
@@ -0,0 +1,34 @@
import cl from "clsx";
import React, { forwardRef } from "react";
import ZoomButton, { ZoomButtonType } from "./ZoomButton";
interface ZoomProps extends React.HTMLAttributes<HTMLUListElement> {
children: React.ReactNode;
}
export interface ZoomType<Props = ZoomProps>
extends React.ForwardRefExoticComponent<
Props & React.RefAttributes<HTMLUListElement>
> {
Button: ZoomButtonType;
componentType: string;
}
export const Zoom = forwardRef<HTMLUListElement, ZoomProps>(
({ className, children, ...rest }, ref) => {
return (
<ul
ref={ref}
className={cl(className, "navdsi-timeline__zoom")}
{...rest}
>
{children}
</ul>
);
}
) as ZoomType;
Zoom.Button = ZoomButton;
Zoom.componentType = "zoom";
export default Zoom;
+3 -2
View File
@@ -39,7 +39,8 @@
aksel.nav.no/
├── website # NEXT.JS app (v12)
└── sanity # Sanity CMS-app (v2)
├── website/sanity # Sanity CMS-app (v3)
└── sanity # Sanity CMS-app (deprecated v2)
```
## Commands
@@ -71,7 +72,7 @@ Vi tar gjerne i mot PR-er [CONTRIBUTING.md](https://github.com/navikt/Designsyst
## Codeowners
Løsningen forvaltes av team Aksel/Designsystemet
Løsningen forvaltes av team Aksel
[Slack](https://nav-it.slack.com/archives/C7NE7A8UF)
+6 -36
View File
@@ -1,22 +1,17 @@
CMS og nettside for [aksel.nav.no](https://aksel.nav.no/)
```sh
├── /website # NEXT.JS app (v12)
└── /sanity # Sanity CMS-app (v2)
├── /website # NEXT.JS app (v12)
└── /website/sanity # Sanity CMS-app (v3)
```
### /website
[Nextjs](https://nextjs.org/) applikasjon som henter data fra et [sanity-datasett](https://www.sanity.io/).
Siden leverer to løsninger:
### /website/sanity
- Innhold på toppnivå i Aksel: artikler, prinsipper og blogger
- Designsystem-dokumentasjon under https://aksel.nav.no/designsystem
### /sanity
Det publiserte studioet finner man på [aksel.nav.no/studio](https://aksel.nav.no/studio)(krever NAV SSO eller invite).
Det publiserte studioet finner man på [aksel.nav.no/admin](https://aksel.nav.no/admin)(krever NAV SSO eller invite).
`production`-datasettet er privat, så man må ha riktige tilganger for å kunne lese/jobbe med dataen der.
@@ -42,40 +37,15 @@ SANITY_PREVIEW_TOKEN
SANITY_PRIVATE_NO_DRAFTS
```
### Sanity (localhost:3333)
> !! Må være developer i https://verktoykasse.sanity.studio, Ta kontakt med team Aksel om du trenger tilgang !!
Husk at alle endringer i datasettet lokalt også gjøres i prod. Bytt derfor til `development`-dataset hvis man vil teste destruktive handlinger. Dette kan endres i `/sanity/schema.json`
```
yarn install
yarn workspace aksel.nav.no sanity
```
**Deploy av ny sanity-oppdatering**
```
yarn workspace aksel.nav.no sanity:deploy
```
### .env
Blir brukt flere keys i .env under `./website`, men kun `SANITY_PRIVATE_NO_DRAFTS` trengs for å teste lokalt
- SANITY_WRITE_KEY:
For å sende dokumenter til sanity, brukt til å oppdatere sandboxes/examples/farger. Trengs bare hvis kode skal synkes fra lokal branch
- SANITY_PREVIEW_TOKEN: Gir appen tilgang til å lese draft innhold fra sanity i "preview"-mode. Trengs for å teste preview-sider
For å sende dokumenter til sanity, brukt til å oppdatere sandboxes/examples/farger. Trengs bare hvis kode skal synkes fra lokal branch/teste feedback-løsning
- SANITY_PREVIEW_TOKEN: Gir appen tilgang til å lese draft innhold fra sanity i "preview"-mode. Trengs for å teste ikke publiserte-sider
- SANITY_PRIVATE_NO_DRAFTS: Gir appen tilgang til å lese innhold fra Sanity da datasettet er privat
### Søk
Søk-index blir generert av [algolia-scraper](https://github.com/algolia/docsearch-scraper). Kjøres daglig i en github-action, men kan kjøres lokalt også med docker.
```
docker run -it --env-file=.env -e "CONFIG=$(cat crawler.json | jq -r tostring)" algolia/docsearch-scraper
```
## Backups
Kjøres backups hver 3 dag til GCP-bucket under `designsystem-prod`
+8 -8
View File
@@ -15,14 +15,14 @@
"website"
],
"dependencies": {
"@navikt/ds-codemod": "^2.0.16",
"@navikt/ds-css": "^2.0.16",
"@navikt/ds-css-internal": "^2.0.16",
"@navikt/ds-icons": "^2.0.16",
"@navikt/ds-react": "^2.0.16",
"@navikt/ds-react-internal": "^2.0.16",
"@navikt/ds-tailwind": "^2.0.16",
"@navikt/ds-tokens": "^2.0.16"
"@navikt/ds-codemod": "^2.1.5",
"@navikt/ds-css": "^2.1.5",
"@navikt/ds-css-internal": "^2.1.5",
"@navikt/ds-icons": "^2.1.5",
"@navikt/ds-react": "^2.1.5",
"@navikt/ds-react-internal": "^2.1.5",
"@navikt/ds-tailwind": "^2.1.5",
"@navikt/ds-tokens": "^2.1.5"
},
"devDependencies": {
"prettier-plugin-tailwindcss": "^0.1.13"
Binary file not shown.
+44 -48
View File
@@ -1,57 +1,53 @@
import { useEffect } from "react";
import config from "config:sanity";
const BUNDLE_CHECK_INTERVAL = 60 * 5000;
const BUNDLE_CHECK_INTERVAL_DECLINED = 60 * 20000;
const CHANGES_AVAILABLE_MESSAGE =
"Studioet er har nye endringer! For å ta i bruk disse vil siden nå oppdateres for den nyeste versjonen.";
async function getCurrentHash() {
const basePath = (config.project && config.project.basePath) || "/";
const html = await window.fetch(basePath).then((res) => res.text());
const [, hash] = html.match(/app\.bundle\.js\?(\w+)/) || [];
return hash;
}
let hash = null;
let interval = null;
import { Send } from "@navikt/ds-icons";
import { BodyLong, Button, Heading, Modal } from "@navikt/ds-react";
import React, { useRef, useState } from "react";
const BundleChecker = () => {
useEffect(() => {
getCurrentHash().then((newHash) => {
hash = newHash;
});
const [inc, setInc] = useState(0);
const interval = useRef(null);
interval = createInterval();
const start = () => {
interval.current = setInterval(() => {
setInc((x) => x + 0.4);
}, 10);
};
if (inc > 99) {
clearInterval(interval.current);
window.location.replace("https://aksel.nav.no/admin");
}
return () => clearInterval(interval);
document.body.style.opacity = `${100 - inc}%`;
document.body.style.scale = `${1 - inc / 150}`;
document.body.style.rotate = `${inc ** 1.3}deg`;
/*
TODO: @release uncomment for redirecting to new studio
*/
/* window.location.replace(`http://aksel.nav.no/admin`); */
}, []);
return (
<Modal
aria-labelledby="modal-heading"
closeButton={false}
open
onClose={() => null}
>
<Modal.Content style={{ minWidth: "20rem", maxWidth: "40rem" }}>
<Heading spacing id="modal-heading" size="medium" level="1">
Studioet er flyttet!
</Heading>
// We're a react component, in theory, so return null to not render anything
return null;
<BodyLong spacing>
Vi har oppdatert studioet og flyttet det til ny url:
aksel.nav.no/admin. Hvis noe ikke fungerer, ta kontakt på slack
#aksel-redaksjonen! Husk å logge inn med samme login-provider. For de
fleste vil dette være NAV SSO.
</BodyLong>
<Button
onClick={() => start()}
icon={<Send aria-hidden />}
iconPosition="right"
>
Gå til nytt studio
</Button>
</Modal.Content>
</Modal>
);
};
export default BundleChecker;
const createInterval = (declined?: boolean) =>
setInterval(
async () => {
const newHash = await getCurrentHash();
if (hash && newHash !== hash) {
clearInterval(interval);
if (window.confirm(CHANGES_AVAILABLE_MESSAGE)) {
window.location.reload();
} else {
interval = createInterval(true);
}
}
},
declined ? BUNDLE_CHECK_INTERVAL_DECLINED : BUNDLE_CHECK_INTERVAL
);
@@ -119,7 +119,6 @@ const adjektiv = [
const client = sanityClient.withConfig({ apiVersion: "2020-06-19" });
const generateName = async () => {
console.log("ran");
const names = await client.fetch(`*[_type == "editor"].anon_navn.current`);
let c = 0;
@@ -131,7 +130,6 @@ const generateName = async () => {
c += 1;
}
console.log(res);
return res;
};
+2 -2
View File
@@ -1,10 +1,10 @@
const exportDataset = require("@sanity/export");
const sanityClient = require("@sanity/client");
const sanityToken = process.env.SANITY_TOKEN;
const sanityToken = process.env.SANITY_PREVIEW_TOKEN;
const SanityConfig = require("../sanity.json");
if (!sanityToken) {
throw new Error("Could not find token from SANITY_TOKEN");
throw new Error("Could not find token from SANITY_PREVIEW_TOKEN");
}
const client = sanityClient({
@@ -1,7 +1,7 @@
import { ExternalLink } from "@navikt/ds-icons";
import { BodyLong, Detail, Heading, Ingress, Link } from "@navikt/ds-react";
import BlockContent from "@sanity/block-content-to-react";
import cl from "classnames";
import cl from "clsx";
import InnholdsKort from "components/sanity-modules/cards/InnholdsKort";
import NextLink from "next/link";
import React, { createContext, useContext } from "react";
@@ -182,6 +182,9 @@ const serializers = {
mark: any;
children: any;
}) => {
if (!href) {
return children;
}
if (href && href.startsWith("mailto:")) {
return (
<NextLink href={href} passHref>
@@ -56,13 +56,14 @@ const FooterForm = () => {
return (
<>
<div className="flex w-full max-w-md flex-col gap-8" data-theme="dark">
{sent.status ? (
<div>
<div aria-live="polite">
{sent.status && (
<Label spacing as="p">
Meldingen er sendt til Aksel, takk for tilbakemeldingen!
</Label>
</div>
) : (
)}
</div>
{!sent.status && (
<form onSubmit={(e) => handleSubmit(e)} className="w-full">
<div className="mb-4 flex flex-col gap-4">
<Heading level="2" size="small">
@@ -1,6 +1,6 @@
import { Close, Hamburger as HamburgerIcon } from "@navikt/ds-icons";
import { Button } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
@@ -1,7 +1,7 @@
import Logo from "components/assets/Logo";
import Link from "next/link";
import { logNav } from "../..";
import cl from "classnames";
import cl from "clsx";
import { useRouter } from "next/router";
import { Hamburger } from "components/layout/header/Hamburger";
@@ -42,7 +42,7 @@ export const Header = ({
}) => {
return (
<>
<a className="skiplink" href="#hovedinnhold" tabIndex={-1}>
<a className="skiplink" href="#hovedinnhold">
Hopp til innhold
</a>
<header
@@ -1,7 +1,7 @@
import { SanityT, urlFor } from "@/lib";
import { SanityBlockContent } from "@/sanity-block";
import { Next } from "@navikt/ds-icons";
import { BodyShort, Heading, Ingress, Label } from "@navikt/ds-react";
import { BodyShort, Detail, Heading, Ingress, Label } from "@navikt/ds-react";
import ArtikkelCard from "components/sanity-modules/cards/ArtikkelCard";
import Head from "next/head";
import NextLink from "next/link";
@@ -54,15 +54,18 @@ const AkselArtikkelTemplate = ({
: `Relevante artikler`}
</Heading>
<div className="card-grid-3-1 mt-6 px-4">
{data.relevante_artikler.map((x: any) => (
<ArtikkelCard {...x} key={x._id} />
))}
{data.relevante_artikler.map((x: any) =>
x && x?._id ? <ArtikkelCard level="3" {...x} key={x._id} /> : null
)}
</div>
</div>
</div>
</aside>
);
const filteredTema =
hasTema && data?.tema?.filter((x: any) => x?.title && x?.slug);
return (
<>
<Head>
@@ -120,23 +123,27 @@ const AkselArtikkelTemplate = ({
{data?.ingress}
</Ingress>
)}
<div className="mt-6 flex gap-3 text-base">
<BodyShort
size="small"
as="span"
className="text-text-subtle whitespace-nowrap"
>
<div className="mt-6 inline-flex flex-wrap items-center gap-2 text-base">
<Detail uppercase as="span">
{dateStr(date)}
</BodyShort>
</Detail>
{authors?.length > 0 && (
<BodyShort size="small" as="div" className="flex flex-wrap gap-1">
<address className="not-italic">{authors[0]}</address>
</BodyShort>
<>
<span className="bg-deepblue-700 h-2 w-2 rotate-45 rounded-[1px] opacity-25" />
<BodyShort
size="small"
as="div"
className="flex flex-wrap gap-1"
>
<address className="not-italic">{authors?.[0]}</address>
</BodyShort>
</>
)}
</div>
{hasTema && (
<div className="mt-8 flex flex-wrap gap-2">
{data.tema.map(({ title, slug }: any) => (
{filteredTema.map(({ title, slug }: any) => (
<span key={title}>
<NextLink
key={title}
@@ -1,7 +1,7 @@
import { SanityT, urlFor } from "@/lib";
import { SanityBlockContent } from "@/sanity-block";
import { BodyShort, Heading, Ingress, Label } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import { Header } from "components/layout/header/Header";
import Head from "next/head";
import { Footer } from "..";
@@ -3,7 +3,6 @@ import { SanityBlockContent } from "@/sanity-block";
import { ExternalLink } from "@navikt/ds-icons";
import { BodyShort, Heading } from "@navikt/ds-react";
import IntroSeksjon from "components/sanity-modules/IntroSeksjon";
import { BetaWarning } from "components/website-modules/BetaWarning";
import Head from "next/head";
import { dateStr, Feedback, TableOfContents } from "../..";
@@ -126,7 +125,6 @@ const KomponentArtikkelTemplate = ({
<TableOfContents changedState={data["content"]} hideToc={false} />
<div className="content-box">
<div className="mt-12">
{data?.status && data.status?.tag === "beta" && <BetaWarning />}
<IntroSeksjon node={data.intro} />
<SanityBlockContent blocks={data["content"]} />
</div>
@@ -5,7 +5,7 @@ import { Header } from "components/layout/header/Header";
import { Sidebar } from "components/layout/sidebar/Sidebar";
import Feedback from "components/website-modules/feedback";
import { ReactNode } from "react";
import cl from "classnames";
import cl from "clsx";
import NextLink from "next/link";
import { capitalize } from "@/utils";
import Image from "next/image";
@@ -2,7 +2,7 @@ import { SidebarT } from "@/lib";
import { logNav } from "@/utils";
import { Expand } from "@navikt/ds-icons";
import { BodyShort, Detail, Tag } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import Link from "next/link";
import { useRouter } from "next/router";
import { useMemo, useState } from "react";
@@ -62,6 +62,9 @@ const NavItem = ({
? "neutral"
: "info"
}
className={cl({
"border-violet-300 bg-violet-50": link.tag === "beta",
})}
>
{link.tag === "new"
? "ny"
@@ -3,7 +3,7 @@ import { withErrorBoundary } from "@/error-boundary";
import { SanityT, urlFor } from "@/lib";
import { ErrorFilled, SuccessFilled, WarningFilled } from "@navikt/ds-icons";
import { BodyShort } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import React from "react";
const GetIcon = (s: string) => {
@@ -2,14 +2,38 @@ import { SanityT } from "@/lib";
import { withErrorBoundary } from "@/error-boundary";
import core from "@navikt/ds-css/tokens.json";
import internal from "@navikt/ds-css-internal/tokens.json";
import { useState } from "react";
import { Expand } from "@navikt/ds-icons";
import { useEffect, useRef, useState } from "react";
import { Expand, SuccessStroke } from "@navikt/ds-icons";
import cl from "classnames";
import { Link, BodyLong, Label } from "@navikt/ds-react";
import NextLink from "next/link";
import copy from "copy-to-clipboard";
import { CopyIcon } from "@sanity/icons";
const TokenTable = ({ node }: { node: SanityT.Schema.token_kategori }) => {
const [open, setOpen] = useState(false);
const [active, setActive] = useState(false);
const timeoutRef = useRef<NodeJS.Timeout>();
const handleCopy = () => {
setActive(true);
timeoutRef.current && clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => setActive(false), 2000);
const str = Object.entries(tokens).reduce(
(prev, cur) =>
prev +
`${cur[0]}: ${
cur[1].startsWith("--") ? `var(${cur[1]})` : `${cur[1]}`
};\n`,
""
);
copy(str);
};
useEffect(() => {
return () => timeoutRef.current && clearTimeout(timeoutRef.current);
}, []);
const tokens: { [key: string]: string } | null =
node.title in core
@@ -52,7 +76,23 @@ const TokenTable = ({ node }: { node: SanityT.Schema.token_kategori }) => {
</tbody>
</table>
{(open || !showMore) && (
<div className="border-border-divider bg-surface-neutral-subtle w-full rounded-b border border-t-0 p-2">
<div className="border-border-divider bg-surface-neutral-subtle relative w-full rounded-b border border-t-0 p-2">
<button
onClick={handleCopy}
className="text-text-default hover:bg-surface-neutral-subtle-hover focus-visible:shadow-focus absolute top-2 right-2 rounded px-1 py-1 text-2xl focus:outline-none"
>
{active ? (
<>
<SuccessStroke className="text-[1.5rem]" aria-hidden />
<span className="sr-only">Kopier formarterte tokens</span>
</>
) : (
<>
<CopyIcon aria-hidden role="img" />
<span className="sr-only">Kopier formarterte tokens</span>
</>
)}
</button>
<Label size="small" as="span" spacing>
Hva er dette?
</Label>
@@ -73,7 +113,7 @@ const TokenTable = ({ node }: { node: SanityT.Schema.token_kategori }) => {
{!open && showMore && (
<button
onClick={() => setOpen(true)}
className="text-medium group mx-auto flex shrink-0 -translate-y-3/4 flex-col items-center focus:outline-none"
className="text-medium group mx-auto flex shrink-0 -translate-y-3/4 flex-col items-center focus:outline-none"
>
<span className="group-focus-visible:bg-border-focus group-focus-visible:text-text-on-action group-focus-visible:shadow-focus">
Se alle tokens
@@ -1,6 +1,6 @@
/* eslint-disable @next/next/no-img-element */
import { BodyLong, Link } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import React from "react";
import { urlFor, SanityT } from "@/lib";
import { withErrorBoundary } from "@/error-boundary";
@@ -1,6 +1,6 @@
import { withErrorBoundary } from "@/error-boundary";
import { BodyShort } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import NextLink from "next/link";
import Image from "next/image";
import { logNav } from "@/utils";
@@ -1,5 +1,5 @@
import { BodyShort, Heading } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import NextLink from "next/link";
import React from "react";
import { logNav } from "@/components";
@@ -2,7 +2,7 @@ import { capitalize, Snippet } from "@/components";
import { withErrorBoundary } from "@/error-boundary";
import { SanityT } from "@/lib";
import { BodyLong, Link, Chips } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import { useEffect, useState } from "react";
import { CodeSandbox } from "./CodeSandbox";
@@ -1,9 +1,10 @@
import { Copy, SuccessStroke } from "@navikt/ds-icons";
import { SuccessStroke } from "@navikt/ds-icons";
import { useRef, useState, useEffect } from "react";
import copy from "copy-to-clipboard";
import React from "react";
import cl from "classnames";
import cl from "clsx";
import style from "./index.module.css";
import { CopyIcon } from "@sanity/icons";
const copyCode = (content: string) =>
copy(content, {
@@ -23,15 +24,14 @@ const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
const timeoutRef = useRef<NodeJS.Timeout>();
useEffect(() => {
if (active) {
timeoutRef.current = setTimeout(() => setActive(false), 3000);
return () => timeoutRef.current && clearTimeout(timeoutRef.current);
}
}, [active]);
return () => timeoutRef.current && clearTimeout(timeoutRef.current);
}, []);
const handleCopy = () => {
copyCode(content);
setActive(true);
timeoutRef.current && clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => setActive(false), 3000);
};
return (
@@ -55,9 +55,10 @@ const CopyButton = React.forwardRef<HTMLButtonElement, CopyButtonProps>(
aria-label="Kopierte kodesnutt"
/>
) : (
<Copy
className="text-[1.5rem] opacity-75 group-hover:opacity-100"
<CopyIcon
className="text-[2rem] opacity-90 group-hover:opacity-100"
aria-label="Kopier kodesnutt"
role="img"
/>
)}
</button>
@@ -1,6 +1,6 @@
import { withErrorBoundary } from "@/error-boundary";
import { SanityT } from "@/lib";
import cl from "classnames";
import cl from "clsx";
import Highlight, { defaultProps } from "prism-react-renderer";
import React from "react";
import CopyButton from "./CopyButton";
@@ -2,7 +2,7 @@
import { SanityT, urlFor } from "@/lib";
import { Heading } from "@navikt/ds-react";
import Nextlink from "next/link";
import cl from "classnames";
import cl from "clsx";
import Image from "next/image";
import { StatusTag } from "components/website-modules/StatusTag";
@@ -32,7 +32,8 @@ const Filter = ({
>
<Search
variant="simple"
label="Søk i alle NAV-ikoner"
hideLabel={false}
label="Søk"
value={value}
onChange={(e) => {
setValue(e);
@@ -1,7 +1,7 @@
import * as Icons from "@navikt/ds-icons";
import meta from "@navikt/ds-icons/meta.json";
import { BodyShort, Button, Detail, Heading } from "@navikt/ds-react";
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { renderToString } from "react-dom/server";
import {
@@ -11,6 +11,7 @@ import {
Snippet,
} from "@/components";
import { SanityT } from "@/lib";
import { SuggestionBlock } from "components/website-modules/SuggestionBlock";
import { downloadSvg } from "./downloads";
const ModalContent = ({ icon }: { icon: string }) => {
@@ -118,6 +119,11 @@ import ${icon} from "@navikt/ds-icons/svg/${icon}.svg";`,
<Icon />
</div>
</div>
{isNew(doc?.created_at) ? (
<SuggestionBlock variant="ikon-ny" reference={`<${icon} />`} />
) : (
<SuggestionBlock variant="ikon" reference={`<${icon} />`} />
)}
</div>
);
};
@@ -1,6 +1,14 @@
import { AmplitudeEvents, isNew, logAmplitudeEvent } from "@/components";
import {
AmplitudeEvents,
FigmaIcon,
GithubIcon,
isNew,
logAmplitudeEvent,
YarnIcon,
} from "@/components";
import meta from "@navikt/ds-icons/meta.json";
import { BodyLong, Heading, Link, Modal } from "@navikt/ds-react";
import { BodyLong, BodyShort, Heading, Link, Modal } from "@navikt/ds-react";
import { SuggestionBlock } from "components/website-modules/SuggestionBlock";
import dynamic from "next/dynamic";
import { useRouter } from "next/router";
import React, { useCallback, useEffect, useState } from "react";
@@ -148,6 +156,8 @@ const IconSearch = () => {
return (
<div className="relative flex w-full max-w-full flex-col md:w-[1000px] md:max-w-[62vw]">
<Links />
<SuggestionBlock variant="ikoner" />
<div className="flex w-full flex-wrap justify-between gap-x-8 gap-y-4">
<Filter onFilterChange={handleFilterChange} />
</div>
@@ -177,4 +187,62 @@ const IconSearch = () => {
</div>
);
};
export default IconSearch;
function Links() {
return (
<BodyShort
as="span"
size="small"
className="text-text-subtle mb-6 flex flex-wrap gap-4"
>
<>
<a
target="_blank"
rel="noreferrer noopener"
href="https://github.com/navikt/Designsystemet/tree/master/%40navikt/icons"
className="hover:text-text-default focus:text-text-on-inverted focus:shadow-focus flex items-center gap-1 underline hover:no-underline focus:bg-blue-800 focus:no-underline focus:outline-none"
onClick={() =>
logAmplitudeEvent("link", {
kilde: "intro-lenker ikonside",
til: "github",
})
}
>
<GithubIcon /> Github
</a>
<a
target="_blank"
rel="noreferrer noopener"
href="https://yarnpkg.com/package/@navikt/ds-icons"
className="hover:text-text-default focus:text-text-on-inverted focus:shadow-focus flex items-center gap-1 underline hover:no-underline focus:bg-blue-800 focus:no-underline focus:outline-none"
onClick={() =>
logAmplitudeEvent("link", {
kilde: "intro-lenker ikonside",
til: "yarn",
})
}
>
<YarnIcon />
Yarn
</a>
</>
<a
target="_blank"
rel="noreferrer noopener"
href="https://www.figma.com/@nav_aksel"
className="hover:text-text-default focus:text-text-on-inverted focus:shadow-focus flex items-center gap-1 underline hover:no-underline focus:bg-blue-800 focus:no-underline focus:outline-none"
onClick={() =>
logAmplitudeEvent("link", {
kilde: "intro-lenker ikonside",
til: "figma",
})
}
>
<FigmaIcon /> Figma
</a>
</BodyShort>
);
}
@@ -37,15 +37,15 @@ const List = ({ prop, parent }: { prop: any; parent: string }) => {
<span>{prop.type ? <>{Highlighter({ type: prop.type })}</> : ""}</span>
</dt>
{prop.description && (
<dl className="font-sans text-base">{prop.description}</dl>
<dd className="font-sans text-base">{prop.description}</dd>
)}
{prop.name === "ref" && prop.type.includes("Ref<") && (
<dl className="font-sans text-base">
<dd className="font-sans text-base">
{`${parent} extends ${prop.type.slice(
prop.type.indexOf("<") + 1,
prop.type.lastIndexOf(">")
)}`}
</dl>
</dd>
)}
</Detail>
);
@@ -1,6 +1,6 @@
import { withErrorBoundary } from "@/error-boundary";
import { SanityT } from "@/lib";
import cl from "classnames";
import cl from "clsx";
import React from "react";
const KBD = (props: React.HTMLAttributes<HTMLElement>) => (
@@ -3,7 +3,7 @@ import { SanityT } from "@/lib";
import { SanityBlockContent } from "@/sanity-block";
import { Edit, LightBulb } from "@navikt/ds-icons";
import { Detail, Link } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import React from "react";
import style from "./tips.module.css";
@@ -1,5 +1,5 @@
import { Copy, SuccessStroke } from "@navikt/ds-icons";
import cl from "classnames";
import cl from "clsx";
import copystring from "copy-to-clipboard";
import { useEffect, useRef, useState } from "react";
@@ -1,6 +1,6 @@
import { withErrorBoundary } from "@/error-boundary";
import { BodyLong, ReadMore } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import { useState } from "react";
const Video = ({
Loaded 100 of 144 files, more files were not shown because too many files have changed in this diff. Show more