Compare commits

...
Author SHA1 Message Date
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
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
125 changed files with 4372 additions and 897 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 🧩"]
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 🥰"]
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:
+8
View File
@@ -1,5 +1,13 @@
# @navikt/ds-codemod
## 2.1.2
## 2.1.1
## 2.1.0
## 2.0.18
## 2.0.17
## 2.0.16
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-codemod",
"version": "2.0.17",
"version": "2.1.2",
"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.17",
"@navikt/ds-react": "^2.0.17",
"@navikt/ds-icons": "^2.1.2",
"@navikt/ds-react": "^2.1.2",
"clsx": "^1.1.1"
},
"devDependencies": {
+12
View File
@@ -1,5 +1,17 @@
# @navikt/ds-css
## 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
+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.17",
"version": "2.1.2",
"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.17",
"@navikt/ds-tokens": "^2.1.2",
"normalize.css": "^8.0.1",
"postcss": "^8.4.0",
"postcss-cli": "^9.0.0",
+36
View File
@@ -1,5 +1,41 @@
# @navikt/ds-react
## 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
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "2.0.17",
"version": "2.1.2",
"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.17",
"@navikt/ds-icons": "^2.1.2",
"@radix-ui/react-tabs": "1.0.0",
"@radix-ui/react-toggle-group": "1.0.0",
"clsx": "^1.2.1",
@@ -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,7 +181,13 @@ 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));
@@ -179,7 +195,13 @@ export const useMonthpicker = (
};
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));
};
@@ -209,7 +231,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,6 @@
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
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 +71,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 +202,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 +323,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 +341,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;
}
@@ -426,7 +439,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 +508,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;
@@ -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,
+8
View File
@@ -1,5 +1,13 @@
# @navikt/ds-tailwind
## 2.1.2
## 2.1.1
## 2.1.0
## 2.0.18
## 2.0.17
## 2.0.16
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tailwind",
"version": "2.0.17",
"version": "2.1.2",
"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.17",
"@navikt/ds-tokens": "^2.1.2",
"@types/jest": "^29.0.0",
"color": "4.2.3",
"jest": "^29.0.0",
+13
View File
@@ -1,5 +1,18 @@
# @navikt/ds-tokens
## 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
+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.17",
"version": "2.1.2",
"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": {
+12
View File
@@ -1,5 +1,17 @@
# @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
## 2.1.0
## 2.0.18
## 2.0.17
### 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"));
});
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-icons",
"version": "2.0.17",
"version": "2.1.2",
"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"
},
+13
View File
@@ -1,5 +1,18 @@
# @navikt/ds-css-internal
## 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
+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.17",
"version": "2.1.2",
"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"
}
}
+37
View File
@@ -1,5 +1,42 @@
# @navikt/ds-react-internal
## 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
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react-internal",
"version": "2.0.17",
"version": "2.1.2",
"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.17",
"@navikt/ds-react": "^2.0.17",
"@navikt/ds-icons": "^2.1.2",
"@navikt/ds-react": "^2.1.2",
"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.17",
"@navikt/ds-css": "^2.0.17",
"@navikt/ds-css-internal": "^2.0.17",
"@navikt/ds-icons": "^2.0.17",
"@navikt/ds-react": "^2.0.17",
"@navikt/ds-react-internal": "^2.0.17",
"@navikt/ds-tailwind": "^2.0.17",
"@navikt/ds-tokens": "^2.0.17"
"@navikt/ds-codemod": "^2.1.2",
"@navikt/ds-css": "^2.1.2",
"@navikt/ds-css-internal": "^2.1.2",
"@navikt/ds-icons": "^2.1.2",
"@navikt/ds-react": "^2.1.2",
"@navikt/ds-react-internal": "^2.1.2",
"@navikt/ds-tailwind": "^2.1.2",
"@navikt/ds-tokens": "^2.1.2"
},
"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";
@@ -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";
@@ -55,7 +55,7 @@ const AkselArtikkelTemplate = ({
</Heading>
<div className="card-grid-3-1 mt-6 px-4">
{data.relevante_artikler.map((x: any) =>
x && x?._id ? <ArtikkelCard {...x} key={x._id} /> : null
x && x?._id ? <ArtikkelCard level="3" {...x} key={x._id} /> : null
)}
</div>
</div>
@@ -123,18 +123,22 @@ 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 && (
@@ -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 "..";
@@ -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";
@@ -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 />
<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,8 +55,8 @@ 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"
/>
)}
@@ -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";
@@ -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 = ({
@@ -1,7 +1,7 @@
import { withErrorBoundary } from "@/error-boundary";
import Link from "next/link";
import React from "react";
import cl from "classnames";
import cl from "clsx";
import { logNav } from "@/utils";
const AkselLink = ({
@@ -1,6 +1,6 @@
import NextLink from "next/link";
import { useRouter } from "next/router";
import cl from "classnames";
import cl from "clsx";
export const BreadCrumbs = ({
href,
@@ -1,5 +1,5 @@
import { Heading } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import React from "react";
export const LevelTwoHeading = ({
@@ -3,7 +3,7 @@ import NextLink from "next/link";
import { dateStr } from "@/utils";
import { BodyLong, BodyShort, Heading, Link } from "@navikt/ds-react";
import { SanityT, urlFor } from "@/lib";
import cl from "classnames";
import cl from "clsx";
import { getImage } from "components/website-modules/utils/get-image";
import { SanityBlockContent } from "@/sanity-block";
@@ -1,6 +1,6 @@
import { removeEmojies } from "@/utils";
import { BodyShort, Heading, Link } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import throttle from "lodash/throttle";
import * as React from "react";
import { useEffect, useState } from "react";
@@ -1,6 +1,6 @@
import { removeEmojies } from "@/utils";
import { BodyShort, Link } from "@navikt/ds-react";
import cl from "classnames";
import cl from "clsx";
import throttle from "lodash/throttle";
import * as React from "react";
import { useEffect, useState } from "react";
@@ -5,10 +5,9 @@ export const ScrollTop = () => {
<button
onClick={() => {
window.scrollTo(0, 0);
document.getElementById("hovedinnhold")?.focus?.();
}}
className="text-xlarge text-text-on-inverted focus-visible:shadow-focus-inverted group -mt-[10px] flex h-[48px] w-fit items-center justify-center gap-2 rounded p-2 font-semibold hover:underline focus:outline-none"
aria-hidden
id="scroll-top-button"
>
<svg
fill="none"
@@ -1,5 +1,5 @@
import styles from "./cube.module.css";
import cl from "classnames";
import cl from "clsx";
const Rect = ({ ...props }) => (
<svg
@@ -1,6 +1,6 @@
import { ComponentType } from "react";
import styles from "./examples.module.css";
import cl from "classnames";
import cl from "clsx";
export const withDsExample = (
Component: ComponentType,
Loaded 100 of 125 files, more files were not shown because too many files have changed in this diff. Show more