mirror of
https://github.com/navikt/aksel.git
synced 2026-10-13 17:22:02 +00:00
Compare commits
55
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9691ac4713 | ||
|
|
7174bbe7bf | ||
|
|
62f36da5e7 | ||
|
|
9ec5133ba2 | ||
|
|
0a7906187f | ||
|
|
28f2a3f532 | ||
|
|
f5d404b925 | ||
|
|
05955acaf7 | ||
|
|
55362f3ea7 | ||
|
|
28f8f7a408 | ||
|
|
6778e902f3 | ||
|
|
92566d5750 | ||
|
|
6911913748 | ||
|
|
7f3e96ae01 | ||
|
|
19791c0339 | ||
|
|
6a00fca823 | ||
|
|
f5d3ed8eee | ||
|
|
5c00b91dfb | ||
|
|
5b82182fda | ||
|
|
6246c1d311 | ||
|
|
04ad4d09b2 | ||
|
|
65ddd4bd18 | ||
|
|
8bb5b8f7b9 | ||
|
|
dcf5cfc06b | ||
|
|
ff97e7f3f8 | ||
|
|
9e1165de0a | ||
|
|
812573ac43 | ||
|
|
5f340eba49 | ||
|
|
703e7e3ab5 | ||
|
|
b42101f6f1 | ||
|
|
8b0c4d8f59 | ||
|
|
6ab84e581f | ||
|
|
42f3a298a2 | ||
|
|
e49922d3fe | ||
|
|
fc06e8f7b4 | ||
|
|
0273b42fb4 | ||
|
|
0873fe6520 | ||
|
|
fdcf502a64 | ||
|
|
aebb22bf1b | ||
|
|
4529e64c6e | ||
|
|
01395e65ba | ||
|
|
21dd0450e7 | ||
|
|
ffac16cb05 | ||
|
|
98177d0cd3 | ||
|
|
2c5be718e6 | ||
|
|
019d92dd7b | ||
|
|
5b72afbb93 | ||
|
|
56348cdddf | ||
|
|
f51d43c06a | ||
|
|
8c81ae46e0 | ||
|
|
d64da78d8b | ||
|
|
21da657eb0 | ||
|
|
52d2d5f41d | ||
|
|
ff2515fabb | ||
|
|
23066e9503 |
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
}
|
||||
|
||||
.navds-form-field--small .navds-text-field__input {
|
||||
padding: 0 0.25rem;
|
||||
padding: 0 0.5rem;
|
||||
min-height: 32px;
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,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": [
|
||||
|
||||
@@ -159,7 +159,7 @@
|
||||
|
||||
"danger": {
|
||||
"subtle": {
|
||||
"hover": { "value": "{a.red.50.value}" },
|
||||
"hover": { "value": "{a.red.100.value}" },
|
||||
"@": { "value": "{a.red.50.value}" }
|
||||
},
|
||||
"hover": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"));
|
||||
});
|
||||
@@ -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"
|
||||
},
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
@import "copy-to-clipboard.css";
|
||||
@import "dropdown.css";
|
||||
@import "header.css";
|
||||
@import "timeline.css";
|
||||
@@ -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": [
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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,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;
|
||||
@@ -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
@@ -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`
|
||||
|
||||
@@ -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.
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
Reference in new issue
Block a user