Compare commits

...
Author SHA1 Message Date
github-actions[bot]andgithub-actions[bot] b42101f6f1 Version Packages (#1757)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-20 14:04:04 +01:00
6ab84e581f [FEATURE] Timeline interne-flater (#1665)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
Co-authored-by: Kai Erik Schieren <kai.schieren@soprasteria.com>
Co-authored-by: Kai Erik Schieren <111572054+kschieren@users.noreply.github.com>
2023-01-20 13:50:52 +01:00
Ken e49922d3fe ✨ Copy-knapp for tokens 2023-01-20 13:05:44 +01:00
Ken fc06e8f7b4 🎨 Default å tillate 2-siffer år (yy) 2023-01-20 12:41:51 +01:00
Ken 0273b42fb4 Revert "🎨 År med 2 tall er standard i datepicker nå"
This reverts commit aebb22bf1b.
2023-01-20 12:40:43 +01:00
Ken 0873fe6520 📝 changeset 2023-01-20 12:39:37 +01:00
Ken fdcf502a64 ♿ Aria-labels for sanity riktekst-editor 2023-01-20 12:34:46 +01:00
Ken aebb22bf1b 🎨 År med 2 tall er standard i datepicker nå 2023-01-20 12:32:38 +01:00
Ken 4529e64c6e ✨ Støtte for 'yy'-format i datepicker/monthpicker (#1755)
* 🎨 Støtte for 2-digit år

* 🎨 Props for toggling av twoDigitYear

* ✅ Tester for parsing av twoDigitYears

* 🎨 twoDigitYear demo

* 📝 Docs
2023-01-20 08:53:58 +01:00
Ken 01395e65ba 🎨 Utfaset gammel sanity-løsning 2023-01-19 21:45:29 +01:00
Ken 21dd0450e7 ⬆️ Yarn.lock 2023-01-19 21:15:04 +01:00
github-actions[bot]andgithub-actions[bot] ffac16cb05 Version Packages (#1756)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-19 15:53:44 +01:00
Ken 98177d0cd3 📝 changeset 2023-01-19 15:19:46 +01:00
Ken 2c5be718e6 🎨 Fikset hjelp ønskes modul 2023-01-19 10:00:56 +01:00
github-actions[bot]andgithub-actions[bot] 019d92dd7b Version Packages (#1754)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-18 15:24:10 +01:00
Ken 5b72afbb93 Merge branch 'master' of https://github.com/navikt/Designsystemet 2023-01-18 15:14:13 +01:00
Ken 56348cdddf ♿ Aria-labels for sanity riktekst-editor 2023-01-18 15:13:58 +01:00
github-actions[bot]andgithub-actions[bot] 973d79ee99 Version Packages (#1753)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-18 14:56:36 +01:00
Ken 5bfb912531 🎨 Bedre error-handling på akselartikkel templates 2023-01-18 14:55:45 +01:00
Ken 27e3dfee07 📝 Changeset 2023-01-18 13:08:38 +01:00
KenandKenAJoh 20b2bf6f89 Ikonsynk Figma (#1752)
Co-authored-by: KenAJoh <KenAJoh@users.noreply.github.com>
2023-01-18 13:07:48 +01:00
Ken 4b870986c8 🐛 Fikset intentbutton for feddbackløsning 2023-01-18 12:44:48 +01:00
github-actions[bot]andgithub-actions[bot] 8fcf079359 Version Packages (#1751)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-17 14:55:06 +01:00
Ken 54f8b07956 📝 changeset 2023-01-17 14:13:50 +01:00
KenandKenAJoh 9bc251e694 Ikonsynk Figma (#1750)
Co-authored-by: KenAJoh <KenAJoh@users.noreply.github.com>
2023-01-17 14:13:10 +01:00
github-actions[bot]andgithub-actions[bot] e2a9d027dc Version Packages (#1749)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-17 10:32:23 +01:00
Ken 22dfd8e603 :sparkled: Prop for posisjonering av toptekst i Chat (#1748)
* :sparkled: Prop for posisjonering av toptekst i Chat

* 📝 changeset

* 📝 Docs

* 📝 Demo

* 🎨 fliptoptext -> toptextPosition
2023-01-17 10:26:14 +01:00
Ken 53d1947311 📝 Tekstendringer sanity profilside 2023-01-17 09:39:38 +01:00
Ken d4e7c47772 🎨 Sanity feedback preview 2023-01-17 07:37:20 +01:00
Ken e191dffdf8 🔥 Fjernet dårlige editor-navn 2023-01-16 09:12:05 +01:00
Ken 7e0ee60d47 🔥 Fjernet dårlige editor-navn 2023-01-16 08:57:11 +01:00
github-actions[bot]andgithub-actions[bot] 4ecaeb9d04 Version Packages (#1746)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-13 16:03:31 +01:00
Ken 8e5c554431 ⬆️ Bump @floating-ui/react 2023-01-13 15:56:12 +01:00
Ken 727be0a98b 🔥 Fjernet snowfall fra forside 2023-01-13 15:15:47 +01:00
github-actions[bot]andgithub-actions[bot] 4c2e44ced1 Version Packages (#1745)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-01-13 15:07:11 +01:00
Ken 2fd1dd443f 🔥 Fjernet ubrukt kode forside 2023-01-13 14:41:06 +01:00
Ken a6c3391237 📝 changeset 2023-01-13 14:38:45 +01:00
KenandKenAJoh 5ede88a188 Ikonsynk Figma (#1739)
Co-authored-by: KenAJoh <KenAJoh@users.noreply.github.com>
2023-01-13 14:38:05 +01:00
Ken 2785593d20 ⬆️ Yarn lock update 2023-01-13 12:38:54 +01:00
Ken a37c743547 🐛 Bedre logging av temavisninger 2023-01-12 09:36:02 +01:00
Ken 6f0e18c3cf 🐛 E2E tester feilet grunnet aria-hidden 2023-01-12 09:18:10 +01:00
Ken bdfaa54e81 ♿ Gjemte til-toppen knapp for skjermleser 2023-01-12 08:27:03 +01:00
Ken 595796af6c 📝 Oppdatert grunnleggnde config 2023-01-12 06:50:16 +01:00
Ken c85b34e86f 🐛 Fikset henting av sider for testing i e2e! 2023-01-10 10:41:17 +01:00
Ken fc46b5a97b 🔍 Sitemap har nå lastmod-attr 2023-01-10 09:50:10 +01:00
Ken c7fcf57fdf :map: Google search-index ignorerer priority i sitemap 2023-01-10 08:28:21 +01:00
Ken 560d8bd1e8 🔍 Bedre robots.txt 2023-01-10 08:21:32 +01:00
Ken 7a53de186e 📝 1 Artikler -> 1 Artikkel 2023-01-09 13:48:57 +01:00
Ken 4feb798b25 🐛 God-praksis feilet ved visning av god-praksis sider 2023-01-09 12:53:54 +01:00
Ken c713b469f0 🔥 Fjernet ubrukt import Temaview 2023-01-09 12:40:42 +01:00
Ken 676392a5c4 🐛 Nye tema uten sider brakk sanity 2023-01-09 12:39:02 +01:00
Ken c049e08555 🎨 Sanity config refactor 2023-01-09 11:51:33 +01:00
108 changed files with 4681 additions and 1195 deletions

No files matched your search

+14
View File
@@ -1,5 +1,19 @@
# @navikt/ds-codemod
## 2.1.0
## 2.0.18
## 2.0.17
## 2.0.16
## 2.0.15
## 2.0.14
## 2.0.13
## 2.0.12
## 2.0.11
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-codemod",
"version": "2.0.12",
"version": "2.1.0",
"description": "Aksel codemods for NAVs designsystem",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
+2 -2
View File
@@ -39,8 +39,8 @@
"build-pict": "svgr --silent --index-template index-template.js --out-dir src/pictograms pictograms"
},
"dependencies": {
"@navikt/ds-icons": "^2.0.12",
"@navikt/ds-react": "^2.0.12",
"@navikt/ds-icons": "^2.1.0",
"@navikt/ds-react": "^2.1.0",
"clsx": "^1.1.1"
},
"devDependencies": {
+18
View File
@@ -1,5 +1,23 @@
# @navikt/ds-css
## 2.1.0
## 2.0.18
## 2.0.17
## 2.0.16
## 2.0.15
### Patch Changes
- [#1748](https://github.com/navikt/Designsystemet/pull/1748) [`22dfd8e60`](https://github.com/navikt/Designsystemet/commit/22dfd8e60315c421749394d35e41f412a00a7593) Thanks [@KenAJoh](https://github.com/KenAJoh)! - toptextPosition prop i Chat for horisontal plassering av navn og dato.
## 2.0.14
## 2.0.13
## 2.0.12
### Patch Changes
+16
View File
@@ -77,6 +77,22 @@
align-self: flex-end;
}
.navds-chat--top-text-left .navds-chat__top-text {
align-self: flex-start;
}
.navds-chat--top-text-right .navds-chat__top-text {
align-self: flex-end;
}
.navds-chat--left .navds-chat__top-text--right {
align-self: flex-end;
}
.navds-chat--right .navds-chat__top-text--left {
align-self: flex-start;
}
.navds-chat__name {
font-weight: var(--a-font-weight-bold);
}
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "2.0.12",
"version": "2.1.0",
"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.12",
"@navikt/ds-tokens": "^2.1.0",
"normalize.css": "^8.0.1",
"postcss": "^8.4.0",
"postcss-cli": "^9.0.0",
+59
View File
@@ -1,5 +1,64 @@
# @navikt/ds-react
## 2.1.0
### Minor Changes
- [`0873fe652`](https://github.com/navikt/Designsystemet/commit/0873fe6520e817e462197c162b49c05826da4838) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Datepicker/Monthpicker støtter nå år med 2 siffer i input
### Patch Changes
- Updated dependencies []:
- @navikt/ds-icons@2.1.0
## 2.0.18
### Patch Changes
- [`98177d0cd`](https://github.com/navikt/Designsystemet/commit/98177d0cd3802029dcf398ce93c1767d3b0860c5) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Bedre typer for ref i datepicker
- Updated dependencies []:
- @navikt/ds-icons@2.0.18
## 2.0.17
### Patch Changes
- Updated dependencies [[`27e3dfee0`](https://github.com/navikt/Designsystemet/commit/27e3dfee07e135893e9f62cd32f80ed83a3510de)]:
- @navikt/ds-icons@2.0.17
## 2.0.16
### Patch Changes
- Updated dependencies [[`54f8b0795`](https://github.com/navikt/Designsystemet/commit/54f8b07956bbfb7de51f6682a5fc64903bfd1bdf)]:
- @navikt/ds-icons@2.0.16
## 2.0.15
### Patch Changes
- [#1748](https://github.com/navikt/Designsystemet/pull/1748) [`22dfd8e60`](https://github.com/navikt/Designsystemet/commit/22dfd8e60315c421749394d35e41f412a00a7593) Thanks [@KenAJoh](https://github.com/KenAJoh)! - toptextPosition prop i Chat for horisontal plassering av navn og dato.
- Updated dependencies []:
- @navikt/ds-icons@2.0.15
## 2.0.14
### Patch Changes
- [`8e5c55443`](https://github.com/navikt/Designsystemet/commit/8e5c554431c98b1b658c5054c0b3cf08b8a65e52) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Bumpet @floating-ui/react
- Updated dependencies []:
- @navikt/ds-icons@2.0.14
## 2.0.13
### Patch Changes
- Updated dependencies [[`a6c339123`](https://github.com/navikt/Designsystemet/commit/a6c3391237f13394063e4c004ac9efb1875fc944)]:
- @navikt/ds-icons@2.0.13
## 2.0.12
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "2.0.12",
"version": "2.1.0",
"description": "Aksel react-components for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -36,8 +36,8 @@
"test": "TZ=UTC jest"
},
"dependencies": {
"@floating-ui/react-dom-interactions": "0.9.2",
"@navikt/ds-icons": "^2.0.12",
"@floating-ui/react": "0.17.0",
"@navikt/ds-icons": "^2.1.0",
"@radix-ui/react-tabs": "1.0.0",
"@radix-ui/react-toggle-group": "1.0.0",
"clsx": "^1.2.1",
+22 -2
View File
@@ -19,6 +19,10 @@ export interface BubbleProps extends HTMLAttributes<HTMLDivElement> {
* Background color on bubble
*/
backgroundColor?: string;
/**
* Overrides hoizontal position of toptext
*/
toptextPosition?: "left" | "right";
}
export type BubbleType = React.ForwardRefExoticComponent<
@@ -26,7 +30,18 @@ export type BubbleType = React.ForwardRefExoticComponent<
>;
const Bubble: BubbleType = forwardRef(
({ children, className, name, timestamp, backgroundColor, ...rest }, ref) => {
(
{
children,
className,
name,
timestamp,
backgroundColor,
toptextPosition,
...rest
},
ref
) => {
return (
<div
ref={ref}
@@ -35,7 +50,12 @@ const Bubble: BubbleType = forwardRef(
{...rest}
>
{(timestamp || name) && (
<div className="navds-chat__top-text">
<div
className={cl(
`navds-chat__top-text`,
toptextPosition && `navds-chat__top-text--${toptextPosition}`
)}
>
{name && <Detail className="navds-chat__name">{name}</Detail>}
{timestamp && (
<Detail className="navds-chat__timestamp">{timestamp}</Detail>
+13 -1
View File
@@ -33,6 +33,11 @@ export interface ChatProps extends HTMLAttributes<HTMLDivElement> {
* @default "left"
*/
position?: "left" | "right";
/**
* Hoizontal position of toptext
* @default Same as chat
*/
toptextPosition?: "left" | "right";
}
interface ChatComponent
@@ -53,6 +58,7 @@ export const Chat = forwardRef<HTMLDivElement, ChatProps>(
position = "left",
avatarBgColor,
backgroundColor,
toptextPosition,
...rest
},
ref
@@ -60,7 +66,13 @@ export const Chat = forwardRef<HTMLDivElement, ChatProps>(
return (
<div
ref={ref}
className={cl("navds-chat", className, `navds-chat--${position}`)}
className={cl(
"navds-chat",
className,
`navds-chat--${position} navds-chat--top-text-${
toptextPosition ?? position
}`
)}
{...rest}
>
<BodyShort
@@ -116,6 +116,57 @@ export const Position = () => {
);
};
export const ToptextPosition = () => {
return (
<div className="colgap">
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 14:00"
backgroundColor="rgba(255, 249, 240, 1)"
avatarBgColor="rgba(255, 236, 204, 1)"
toptextPosition="right"
>
<Chat.Bubble>
Adipisicing laborum est eu laborum est sit in commodo enim sint
laboris labore nisi ut.
</Chat.Bubble>
</Chat>
<Chat
avatar="KH"
name="Kari Høyli"
timestamp="01.01.21 14:32"
position="right"
backgroundColor="rgba(230, 240, 255, 1)"
avatarBgColor="rgba(204, 225, 255, 1)"
toptextPosition="left"
>
<Chat.Bubble>
Reprehenderit pariatur officia exercitation laboris.
</Chat.Bubble>
<Chat.Bubble>
Enim velit deserunt do quis. Eu fugiat magna esse dolore ad sunt sit
est dolore incididunt. Occaecat cupidatat magna.
</Chat.Bubble>
</Chat>
<Chat
avatar="ON"
name="Ola Normann"
timestamp="01.01.21 15:00"
backgroundColor="rgba(255, 249, 240, 1)"
avatarBgColor="rgba(255, 236, 204, 1)"
>
<Chat.Bubble toptextPosition="right">
Exercitation irure Lorem cupidatat culpa anim cillum esse ullamco qui
dolore laborum et Lorem. Labore sunt duis id Lorem voluptate commodo
ea esse. Dolore esse aliqua proident ea ad commodo ut dolore voluptate
labore sunt aute.
</Chat.Bubble>
</Chat>
</div>
);
};
export const Avatar = () => {
return (
<Chat avatar={<Illustration />} timestamp="01.01.21 14:00">
@@ -49,6 +49,13 @@ export interface UseDatepickerOptions
* validation-callback
*/
onValidate?: (val: DateValidationT) => void;
/**
* Allows input of with 'yy' year format.
* @default true
* @Note Decision between 20th and 21st century is based on before(todays year - 80) ? 21st : 20th.
* In 2023 this equals to 1943 - 2042
*/
allowTwoDigitYear?: boolean;
}
interface UseDatepickerValue {
@@ -59,7 +66,10 @@ interface UseDatepickerValue {
/**
* Use: <DatePicker.Input {...inputProps}/>
*/
inputProps: Pick<DateInputProps, "onChange" | "onFocus" | "onBlur" | "value">;
inputProps: Pick<
DateInputProps,
"onChange" | "onFocus" | "onBlur" | "value"
> & { ref: React.RefObject<HTMLInputElement> };
/**
* Resets all states (callback)
*/
@@ -112,6 +122,7 @@ export const useDatepicker = (
inputFormat,
onValidate,
defaultMonth,
allowTwoDigitYear = true,
} = opt;
const locale = getLocaleFromString(_locale);
@@ -188,7 +199,13 @@ export const useDatepicker = (
const handleFocus: React.FocusEventHandler<HTMLInputElement> = (e) => {
!open && setOpen(true);
let day = parseDate(e.target.value, today, locale, "date");
let day = parseDate(
e.target.value,
today,
locale,
"date",
allowTwoDigitYear
);
if (isValidDate(day)) {
setMonth(day);
setInputValue(formatDateForInput(day, locale, "date", inputFormat));
@@ -196,7 +213,13 @@ export const useDatepicker = (
};
const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
let day = parseDate(e.target.value, today, locale, "date");
let day = parseDate(
e.target.value,
today,
locale,
"date",
allowTwoDigitYear
);
isValidDate(day) &&
setInputValue(formatDateForInput(day, locale, "date", inputFormat));
};
@@ -227,7 +250,13 @@ export const useDatepicker = (
// the calendar’s month.
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
setInputValue(e.target.value);
const day = parseDate(e.target.value, today, locale, "date");
const day = parseDate(
e.target.value,
today,
locale,
"date",
allowTwoDigitYear
);
const isBefore =
fromDate && day && differenceInCalendarDays(fromDate, day) > 0;
@@ -35,6 +35,13 @@ export interface UseMonthPickerOptions
* Default shown year
*/
defaultYear?: Date;
/**
* Allows input of with 'yy' year format.
* @default true
* @Note Decision between 20th and 21st century is based on before(todays year - 80) ? 21st : 20th.
* In 2023 this equals to 1943 - 2042
*/
allowTwoDigitYear?: boolean;
}
interface UseMonthPickerValue {
@@ -45,7 +52,9 @@ interface UseMonthPickerValue {
/**
* Use: <MonthPicker.Input {...inputProps} />
*/
inputProps: Pick<DateInputProps, "onChange" | "onFocus" | "value">;
inputProps: Pick<DateInputProps, "onChange" | "onFocus" | "value"> & {
ref: React.RefObject<HTMLInputElement>;
};
/**
* Currently selected Date
* Up to user to validate value and extract month
@@ -94,6 +103,7 @@ export const useMonthpicker = (
inputFormat,
onValidate,
defaultYear,
allowTwoDigitYear = true,
} = opt;
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
@@ -101,7 +111,7 @@ export const useMonthpicker = (
const today = new Date();
const locale = getLocaleFromString(_locale);
const inputRef = useRef<HTMLDivElement>(null);
const inputRef = useRef<HTMLInputElement>(null);
const monthpickerRef = useRef<HTMLDivElement>(null);
// Initialize states
@@ -171,7 +181,13 @@ export const useMonthpicker = (
const handleFocus: React.FocusEventHandler<HTMLInputElement> = (e) => {
!open && setOpen(true);
let day = parseDate(e.target.value, today, locale, "month");
let day = parseDate(
e.target.value,
today,
locale,
"month",
allowTwoDigitYear
);
if (isValidDate(day)) {
setYear(day);
setInputValue(formatDateForInput(day, locale, "month", inputFormat));
@@ -179,7 +195,13 @@ export const useMonthpicker = (
};
const handleBlur: React.FocusEventHandler<HTMLInputElement> = (e) => {
let day = parseDate(e.target.value, today, locale, "month");
let day = parseDate(
e.target.value,
today,
locale,
"month",
allowTwoDigitYear
);
isValidDate(day) &&
setInputValue(formatDateForInput(day, locale, "month", inputFormat));
};
@@ -209,7 +231,13 @@ export const useMonthpicker = (
// the calendar’s month.
const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
setInputValue(e.target.value);
const month = parseDate(e.target.value, today, locale, "month");
const month = parseDate(
e.target.value,
today,
locale,
"month",
allowTwoDigitYear
);
const isBefore =
fromDate &&
@@ -1,6 +1,6 @@
import differenceInCalendarDays from "date-fns/differenceInCalendarDays";
import isWeekend from "date-fns/isWeekend";
import { useCallback, useEffect, useRef, useState } from "react";
import React, { useCallback, useEffect, useRef, useState } from "react";
import { DateRange, isMatch } from "react-day-picker";
import { DateInputProps } from "../DateInput";
import { DatePickerProps } from "../datepicker/DatePicker";
@@ -71,14 +71,14 @@ interface UseRangeDatepickerValue {
fromInputProps: Pick<
DateInputProps,
"onChange" | "onFocus" | "onBlur" | "value"
>;
> & { ref: React.RefObject<HTMLInputElement> };
/**
* Use: <DatePicker.Input label="to" {...toInputProps}/>
*/
toInputProps: Pick<
DateInputProps,
"onChange" | "onFocus" | "onBlur" | "value"
>;
> & { ref: React.RefObject<HTMLInputElement> };
/**
* Resets all states (callback)
*/
@@ -202,12 +202,13 @@ export const useRangeDatepicker = (
inputFormat,
onValidate,
defaultMonth,
allowTwoDigitYear = true,
} = opt;
const locale = getLocaleFromString(_locale);
const inputRefTo = useRef<HTMLDivElement>(null);
const inputRefFrom = useRef<HTMLDivElement>(null);
const inputRefTo = useRef<HTMLInputElement>(null);
const inputRefFrom = useRef<HTMLInputElement>(null);
const datePickerRef = useRef<HTMLDivElement | null>(null);
const [defaultSelected, setDefaultSelected] = useState(_defaultSelected);
@@ -322,7 +323,13 @@ export const useRangeDatepicker = (
const handleFocus = (e, src: RangeT) => {
!open && setOpen(true);
let day = parseDate(e.target.value, today, locale, "date");
let day = parseDate(
e.target.value,
today,
locale,
"date",
allowTwoDigitYear
);
if (isValidDate(day)) {
setMonth(day);
src === RANGE.FROM
@@ -334,7 +341,13 @@ export const useRangeDatepicker = (
};
const handleBlur = (e, src: RangeT) => {
let day = parseDate(e.target.value, today, locale, "date");
let day = parseDate(
e.target.value,
today,
locale,
"date",
allowTwoDigitYear
);
if (!isValidDate(day)) {
return;
}
@@ -426,7 +439,13 @@ export const useRangeDatepicker = (
}
if (toInputValue && !selectedRange?.to) {
const toDay = parseDate(toInputValue, today, locale, "date");
const toDay = parseDate(
toInputValue,
today,
locale,
"date",
allowTwoDigitYear
);
if (validateDay(toDay)) {
updateRange({ from: day, to: toDay });
setMonth(day);
@@ -489,7 +508,13 @@ export const useRangeDatepicker = (
/* live-update datepicker based on changes in inputfields */
const handleChange = (e, src: RangeT) => {
const day = parseDate(e.target.value, today, locale, "date");
const day = parseDate(
e.target.value,
today,
locale,
"date",
allowTwoDigitYear
);
const isBefore = fromDate && differenceInCalendarDays(fromDate, day) > 0;
const isAfter = toDate && differenceInCalendarDays(day, toDate) > 0;
@@ -2,7 +2,9 @@ import { parseDate } from "../parse-date";
import nb from "date-fns/locale/nb";
import { formatDateForInput } from "../format-date";
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date");
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date", false);
const parseTwoDigit = (inp: string) =>
parseDate(inp, new Date(), nb, "date", true);
describe("Format date to correct output", () => {
test("formatDateForInput", () => {
@@ -19,4 +21,12 @@ describe("Format date to correct output", () => {
"15.05.2022"
);
});
test("formatDateForInput with twoDigitYears", () => {
expect(formatDateForInput(parseTwoDigit("15/05/22"), nb, "date")).toEqual(
"15.05.2022"
);
expect(formatDateForInput(parseTwoDigit("1/5/95"), nb, "date")).toEqual(
"01.05.1995"
);
});
});
@@ -4,11 +4,14 @@ import nb from "date-fns/locale/nb";
import getMonth from "date-fns/getMonth";
const check = (inp: string) =>
expect(isValidDate(parseDate(inp, new Date(), nb, "date")));
expect(isValidDate(parseDate(inp, new Date(), nb, "date", false)));
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date");
const checkTwoDigit = (inp: string) =>
expect(isValidDate(parseDate(inp, new Date(), nb, "date", true)));
describe("Parse date-inputs", () => {
const parse = (inp: string) => parseDate(inp, new Date(), nb, "date", false);
describe("Parse date-inputs with 4-digit years", () => {
test("No spaces", () => {
check("11052022").toBeTruthy();
check("15052022").toBeTruthy();
@@ -34,6 +37,44 @@ describe("Parse date-inputs", () => {
});
});
describe("Parse date-inputs with 2-digit years", () => {
test("No spaces", () => {
checkTwoDigit("110522").toBeTruthy();
checkTwoDigit("150522").toBeTruthy();
});
test(". divider", () => {
checkTwoDigit("1.5.22").toBeTruthy();
checkTwoDigit("11.05.22").toBeTruthy();
});
test("/ divider", () => {
checkTwoDigit("1/5/22").toBeTruthy();
checkTwoDigit("10/5/22").toBeTruthy();
checkTwoDigit("1/05/22").toBeTruthy();
checkTwoDigit("10/05/22").toBeTruthy();
});
test("- divider", () => {
checkTwoDigit("1-5-22").toBeTruthy();
checkTwoDigit("10-5-22").toBeTruthy();
checkTwoDigit("1-05-22").toBeTruthy();
checkTwoDigit("10-05-22").toBeTruthy();
});
test("Dissallow 1 and 3 digit years", () => {
checkTwoDigit("11052").toBeFalsy();
checkTwoDigit("1105222").toBeFalsy();
checkTwoDigit("1105999").toBeFalsy();
});
test("Dissallow date before year 1000", () => {
checkTwoDigit("11050999").toBeFalsy();
checkTwoDigit("11050010").toBeFalsy();
checkTwoDigit("11051000").toBeTruthy();
});
});
describe("Parse date-inputs to correct month", () => {
test("No spaces", () => {
expect(getMonth(parse("11052022"))).toEqual(4);
@@ -10,7 +10,7 @@ export const dateIsInCurrentMonth = (
/** @private */
export function isValidDate(day: Date): boolean {
return !isNaN(day.getTime());
return day && !isNaN(day?.getTime()) && day.getFullYear() > 999;
}
export const hasNextYear = (year: Date, years: Date[], val: any): boolean => {
+98 -19
View File
@@ -1,4 +1,6 @@
import parse from "date-fns/parse";
import isBefore from "date-fns/isBefore";
import sub from "date-fns/sub";
import { isValidDate } from ".";
export const INPUT_DATE_STRING_FORMAT_DATE = "dd.MM.yyyy";
@@ -22,7 +24,60 @@ const ALLOWED_INPUT_FORMATS_MONTH = [
...ALLOWED_INPUT_FORMATS_DATE,
];
const isTwoDigitYear = (dateString, today, locale, formats) => {
export const parseDate = (
date: string,
today: Date,
locale: Locale,
type: "date" | "month",
allowTwoDigitYear: boolean
): Date => {
let parsed;
const ALLOWED_FORMATS =
type === "date" ? ALLOWED_INPUT_FORMATS_DATE : ALLOWED_INPUT_FORMATS_MONTH;
if (allowTwoDigitYear) {
for (const format of ALLOWED_FORMATS) {
parsed = parse(date, format, today, { locale });
if (
isValidDate(parsed) &&
!isTwoDigitYear(date, today, locale, ALLOWED_FORMATS)
) {
return parsed;
}
}
for (const format of [
...ALLOWED_FORMATS.map((x) => x.replace("yyyy", "yy")),
]) {
parsed = parse(date, format, today, { locale });
if (
isValidDate(parsed) &&
isTwoDigitYear(date, today, locale, ALLOWED_FORMATS)
) {
const convertedDate = assignCenturyToDate(date, format, today, locale);
if (isValidDate(new Date(convertedDate))) {
return new Date(convertedDate);
} else {
return new Date("Invalid date");
}
}
}
return new Date("Invalid date");
} else {
for (const format of ALLOWED_FORMATS) {
parsed = parse(date, format, today, { locale });
if (
isValidDate(parsed) &&
!isTwoDigitYear(date, today, locale, ALLOWED_FORMATS)
) {
return parsed;
}
}
return new Date("Invalid date");
}
};
function isTwoDigitYear(dateString, today, locale, formats) {
let parsed;
const newFormat = formats.map((x) => x.replace("yyyy", "yy"));
for (const format of newFormat) {
@@ -32,25 +87,49 @@ const isTwoDigitYear = (dateString, today, locale, formats) => {
}
}
return false;
};
}
export const parseDate = (
date: string,
function assignCenturyToDate(
dateStr: string,
format: string,
today: Date,
locale: Locale,
type: "date" | "month"
): Date => {
let parsed;
const ALLOWED_FORMATS =
type === "date" ? ALLOWED_INPUT_FORMATS_DATE : ALLOWED_INPUT_FORMATS_MONTH;
for (const format of ALLOWED_FORMATS) {
parsed = parse(date, format, today, { locale });
if (
isValidDate(parsed) &&
!isTwoDigitYear(date, today, locale, ALLOWED_FORMATS)
) {
return parsed;
}
locale: Locale
) {
const date20Century = parse(
appendCenturyToTwoYearDigitDateString(dateStr, "19"),
format.replace("yy", "yyyy"),
today,
{ locale }
);
const date21Century = parse(
appendCenturyToTwoYearDigitDateString(dateStr, "20"),
format.replace("yy", "yyyy"),
today,
{ locale }
);
if (isValidDate(date20Century) && isValidDate(date21Century)) {
return isBefore(
date20Century,
sub(new Date(), {
years: 80,
})
)
? date21Century
: date20Century;
}
return new Date("Invalid date");
};
}
function appendCenturyToTwoYearDigitDateString(
dateString: string,
century: "19" | "20"
) {
const twoDigitYear = dateString.slice(-2);
return `${dateString.slice(
0,
dateString.length - 2
)}${century}${twoDigitYear}`;
}
+1 -1
View File
@@ -8,7 +8,7 @@ import {
useDismiss,
useFloating,
useInteractions,
} from "@floating-ui/react-dom-interactions";
} from "@floating-ui/react";
import cl from "clsx";
import React, {
forwardRef,
+1 -1
View File
@@ -11,7 +11,7 @@ import {
useFocus,
useHover,
useInteractions,
} from "@floating-ui/react-dom-interactions";
} from "@floating-ui/react";
import cl from "clsx";
import React, {
cloneElement,
+14
View File
@@ -1,5 +1,19 @@
# @navikt/ds-tailwind
## 2.1.0
## 2.0.18
## 2.0.17
## 2.0.16
## 2.0.15
## 2.0.14
## 2.0.13
## 2.0.12
## 2.0.11
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tailwind",
"version": "2.0.12",
"version": "2.1.0",
"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.12",
"@navikt/ds-tokens": "^2.1.0",
"@types/jest": "^29.0.0",
"color": "4.2.3",
"jest": "^29.0.0",
+19
View File
@@ -1,5 +1,24 @@
# @navikt/ds-tokens
## 2.1.0
### Patch Changes
- [#1665](https://github.com/navikt/Designsystemet/pull/1665) [`6ab84e581`](https://github.com/navikt/Designsystemet/commit/6ab84e581f8d3e850bcec340608354335506a051) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :sparkles: Ny Timeline-komponent for interne-flater
:bug: Fikset feil danger-hover token
## 2.0.18
## 2.0.17
## 2.0.16
## 2.0.15
## 2.0.14
## 2.0.13
## 2.0.12
## 2.0.11
+1 -1
View File
@@ -240,7 +240,7 @@
{ "name": "--a-surface-danger-subtle", "value": "rgba(255, 230, 230, 1)" },
{
"name": "--a-surface-danger-subtle-hover",
"value": "rgba(255, 230, 230, 1)"
"value": "rgba(255, 184, 184, 1)"
},
{ "name": "--a-surface-danger", "value": "rgba(195, 0, 0, 1)" },
{ "name": "--a-surface-danger-hover", "value": "rgba(173, 0, 0, 1)" },
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tokens",
"version": "2.0.12",
"version": "2.1.0",
"description": "Design-tokens for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
+1 -1
View File
@@ -159,7 +159,7 @@
"danger": {
"subtle": {
"hover": { "value": "{a.red.50.value}" },
"hover": { "value": "{a.red.100.value}" },
"@": { "value": "{a.red.50.value}" }
},
"hover": {
+26
View File
@@ -1,5 +1,31 @@
# @navikt/ds-icons
## 2.1.0
## 2.0.18
## 2.0.17
### Patch Changes
- [`27e3dfee0`](https://github.com/navikt/Designsystemet/commit/27e3dfee07e135893e9f62cd32f80ed83a3510de) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Translate og Paragraph ikon
## 2.0.16
### Patch Changes
- [`54f8b0795`](https://github.com/navikt/Designsystemet/commit/54f8b07956bbfb7de51f6682a5fc64903bfd1bdf) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Oppdatert ikoner med feile farger
## 2.0.15
## 2.0.14
## 2.0.13
### Patch Changes
- [`a6c339123`](https://github.com/navikt/Designsystemet/commit/a6c3391237f13394063e4c004ac9efb1875fc944) Thanks [@KenAJoh](https://github.com/KenAJoh)! - Ikonjusteringer sirkleikoner
## 2.0.12
## 2.0.11
+26 -14
View File
@@ -867,13 +867,13 @@
"name": "Eu",
"description": "",
"created_at": "2022-02-08T11:45:47.104Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "EuFilled",
"description": "",
"created_at": "2022-02-08T11:45:47.109Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "Expand",
@@ -1479,13 +1479,13 @@
"name": "Law",
"description": "",
"created_at": "2020-07-01T10:37:42.819Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "LawFilled",
"description": "",
"created_at": "2021-01-11T13:37:55.330Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "Left",
@@ -1551,37 +1551,37 @@
"name": "Locked",
"description": "",
"created_at": "2020-07-01T10:37:43.027Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "LockedFilled",
"description": "",
"created_at": "2021-01-11T13:37:55.325Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "Login",
"description": "",
"created_at": "2020-07-01T10:37:43.087Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "LoginFilled",
"description": "",
"created_at": "2021-01-11T13:37:55.531Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "Logout",
"description": "",
"created_at": "2020-07-01T10:37:42.931Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "LogoutFilled",
"description": "",
"created_at": "2021-01-11T13:37:55.609Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "Male",
@@ -1811,6 +1811,12 @@
"created_at": "2021-12-15T15:16:55.869Z",
"pageName": "People"
},
{
"name": "Paragraph",
"description": "",
"created_at": "2023-01-17T13:24:22.989Z",
"pageName": "Privacy and security"
},
{
"name": "Parking",
"description": "",
@@ -1827,13 +1833,13 @@
"name": "Passport",
"description": "",
"created_at": "2020-07-01T10:37:43.078Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "PassportFilled",
"description": "",
"created_at": "2021-01-11T13:37:55.757Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "PensionBag",
@@ -2615,6 +2621,12 @@
"created_at": "2021-01-28T14:00:38.585Z",
"pageName": "Buildings"
},
{
"name": "Translate",
"description": "",
"created_at": "2023-01-18T10:52:22.311Z",
"pageName": "Interface"
},
{
"name": "Truck",
"description": "",
@@ -2643,13 +2655,13 @@
"name": "Unlocked",
"description": "",
"created_at": "2020-07-01T10:37:42.823Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "UnlockedFilled",
"description": "",
"created_at": "2021-01-11T13:37:55.317Z",
"pageName": "Privacy and secutity"
"pageName": "Privacy and security"
},
{
"name": "Up",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-icons",
"version": "2.0.12",
"version": "2.1.0",
"description": "Aksel icons for NAV",
"author": "Aksel | NAV designsystem team",
"repository": {
+3 -1
View File
@@ -1,3 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 0C18.6274 0 24 5.37258 24 12C24 18.6274 18.6274 24 12 24C5.37258 24 0 18.6274 0 12C0 5.37258 5.37258 0 12 0ZM12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM13 6V11H18V13H13V18H11V13H6V11H11V6H13Z" fill="#262626"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.75 2.5C6.64137 2.5 2.5 6.64137 2.5 11.75C2.5 16.8586 6.64137 21 11.75 21C16.8586 21 21 16.8586 21 11.75C21 6.64137 16.8586 2.5 11.75 2.5ZM1 11.75C1 5.81294 5.81294 1 11.75 1C17.6871 1 22.5 5.81294 22.5 11.75C22.5 17.6871 17.6871 22.5 11.75 22.5C5.81294 22.5 1 17.6871 1 11.75Z" fill="#262626"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 12.75H7V11.25H17V12.75Z" fill="#262626"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.25 17L11.25 7L12.75 7L12.75 17H11.25Z" fill="#262626"/>
</svg>

Before

Width:  |  Height:  |  Size: 435 B

After

Width:  |  Height:  |  Size: 655 B

+1 -1
View File
@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 0C18.6274 0 24 5.37258 24 12C24 18.6274 18.6274 24 12 24C5.37258 24 0 18.6274 0 12C0 5.37258 5.37258 0 12 0ZM13 6V11H18V13H13V18H11V13H6V11H11V6H13Z" fill="#262626"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.75 1C5.81294 1 1 5.81294 1 11.75C1 17.6871 5.81294 22.5 11.75 22.5C17.6871 22.5 22.5 17.6871 22.5 11.75C22.5 5.81294 17.6871 1 11.75 1ZM11.25 12.75V17H12.75V12.75H17V11.25H12.75V7H11.25V11.25H7V12.75H11.25Z" fill="#262626"/>
</svg>

Before

Width:  |  Height:  |  Size: 323 B

After

Width:  |  Height:  |  Size: 381 B

+2 -1
View File
@@ -1,3 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M2 12C2 6.81465 5.94668 2.5511 11 2.04938V12.4142L18.3288 19.743C16.6044 21.1544 14.4017 22 12 22C6.47715 22 2 17.5228 2 12ZM19.7488 21.163C17.6587 22.9321 14.9533 24 12 24C5.37258 24 0 18.6274 0 12C0 5.70934 4.84047 0.549225 11 0.0410738V0H12H13V0.0410738C19.1595 0.549225 24 5.70934 24 12C24 14.9533 22.9321 17.6587 21.163 19.7488L21.1924 19.7782L20.4853 20.4853L19.7782 21.1924L19.7488 21.163ZM19.743 18.3288C21.1543 16.6044 22 14.4017 22 12C22 6.81465 18.0533 2.5511 13 2.04938V11.5858L19.743 18.3288Z" fill="#262626"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2.75C6.89137 2.75 2.75 6.89137 2.75 12C2.75 17.1086 6.89137 21.25 12 21.25C17.1086 21.25 21.25 17.1086 21.25 12C21.25 6.89137 17.1086 2.75 12 2.75ZM1.25 12C1.25 6.06294 6.06294 1.25 12 1.25C17.9371 1.25 22.75 6.06294 22.75 12C22.75 17.9371 17.9371 22.75 12 22.75C6.06294 22.75 1.25 17.9371 1.25 12Z" fill="#262626"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.25 2.5H12.75V12.1729L19.5103 18.4504L18.4897 19.5496L11.25 12.8271V2.5Z" fill="#262626"/>
</svg>

Before

Width:  |  Height:  |  Size: 676 B

After

Width:  |  Height:  |  Size: 616 B

+1 -1
View File
@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M11 12V0.0410767C4.84047 0.549228 0 5.70934 0 12C0 18.6274 5.37258 24 12 24C14.9922 24 17.7286 22.9048 19.8303 21.0935L11 12ZM24 12C24 12.9017 23.9005 13.7803 23.712 14.6252C23.297 16.4847 22.4505 18.1814 21.2831 19.6048L13 11V0.0410767C18.2766 0.476386 22.5852 4.32557 23.712 9.37485C23.9005 10.2198 24 11.0983 24 12Z" fill="#262626"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.25 1.27576C5.66285 1.6608 1.25 6.31506 1.25 12C1.25 17.9371 6.06294 22.75 12 22.75C14.5711 22.75 16.9313 21.8474 18.7812 20.3418L11.25 12.8107V1.27576ZM19.8766 19.3159C21.6596 17.3972 22.75 14.8259 22.75 12C22.75 6.31506 18.3372 1.6608 12.75 1.27576V12.1893L19.8766 19.3159Z" fill="#262626"/>
</svg>

Before

Width:  |  Height:  |  Size: 489 B

After

Width:  |  Height:  |  Size: 449 B

+2 -1
View File
@@ -1,3 +1,4 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 0C18.6274 0 24 5.37258 24 12C24 18.6274 18.6274 24 12 24C5.37258 24 0 18.6274 0 12C0 5.37258 5.37258 0 12 0ZM12 2C6.47715 2 2 6.47715 2 12C2 17.5228 6.47715 22 12 22C17.5228 22 22 17.5228 22 12C22 6.47715 17.5228 2 12 2ZM18 11H6V13H18V11Z" fill="#262626"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.75 2.5C6.64137 2.5 2.5 6.64137 2.5 11.75C2.5 16.8586 6.64137 21 11.75 21C16.8586 21 21 16.8586 21 11.75C21 6.64137 16.8586 2.5 11.75 2.5ZM1 11.75C1 5.81294 5.81294 1 11.75 1C17.6871 1 22.5 5.81294 22.5 11.75C22.5 17.6871 17.6871 22.5 11.75 22.5C5.81294 22.5 1 17.6871 1 11.75Z" fill="#262626"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17 12.75H7V11.25H17V12.75Z" fill="#262626"/>
</svg>

Before

Width:  |  Height:  |  Size: 413 B

After

Width:  |  Height:  |  Size: 546 B

+1 -1
View File
@@ -1,3 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 12C24 5.37258 18.6274 0 12 0C5.37258 0 0 5.37258 0 12C0 18.6274 5.37258 24 12 24C18.6274 24 24 18.6274 24 12ZM18 11H6V13H18V11Z" fill="#262626"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M11.75 1C5.81294 1 1 5.81294 1 11.75C1 17.6871 5.81294 22.5 11.75 22.5C17.6871 22.5 22.5 17.6871 22.5 11.75C22.5 5.81294 17.6871 1 11.75 1ZM7 12.75H17V11.25H7V12.75Z" fill="#262626"/>
</svg>

Before

Width:  |  Height:  |  Size: 302 B

After

Width:  |  Height:  |  Size: 336 B

+3
View File
@@ -0,0 +1,3 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M12 2C9.79085 2 7.99999 3.79086 7.99999 6C7.99999 6.66187 8.16311 7.29714 8.45828 7.85918C6.00646 9.12956 5.6173 13.1739 8.76271 14.4845L12.7692 16.1538C13.5145 16.4644 14 17.1926 14 18C14 19.1046 13.1046 20 12 20C10.8954 20 9.99999 19.1046 9.99999 18H7.99999C7.99999 20.2091 9.79085 22 12 22C14.2091 22 16 20.2091 16 18C16 17.3381 15.8369 16.7029 15.5417 16.1408C17.9935 14.8704 18.3827 10.8261 15.2373 9.51554L11.2308 7.84615C10.4855 7.53561 9.99999 6.8074 9.99999 6C9.99999 4.89543 10.8954 4 12 4C13.1046 4 14 4.89543 14 6H16C16 3.79086 14.2091 2 12 2ZM13.9738 14.5208C15.6075 14.4232 16.1245 12.0519 14.4681 11.3617L10.4615 9.69231C10.311 9.62957 10.1657 9.55832 10.0262 9.47924C8.39252 9.57681 7.87544 11.9481 9.53194 12.6383L13.5385 14.3077C13.689 14.3704 13.8343 14.4417 13.9738 14.5208Z" fill="#262626"/>
</svg>

After

Width:  |  Height:  |  Size: 965 B

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

After

Width:  |  Height:  |  Size: 1.2 KiB

+19
View File
@@ -1,5 +1,24 @@
# @navikt/ds-css-internal
## 2.1.0
### Minor Changes
- [#1665](https://github.com/navikt/Designsystemet/pull/1665) [`6ab84e581`](https://github.com/navikt/Designsystemet/commit/6ab84e581f8d3e850bcec340608354335506a051) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :sparkles: Ny Timeline-komponent for interne-flater
:bug: Fikset feil danger-hover token
## 2.0.18
## 2.0.17
## 2.0.16
## 2.0.15
## 2.0.14
## 2.0.13
## 2.0.12
## 2.0.11
+1
View File
@@ -1,3 +1,4 @@
@import "copy-to-clipboard.css";
@import "dropdown.css";
@import "header.css";
@import "timeline.css";
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css-internal",
"version": "2.0.12",
"version": "2.1.0",
"description": "CSS for NAV internal Designsystem-components",
"author": "Aksel | NAV designsystem team",
"keywords": [
+281
View File
@@ -0,0 +1,281 @@
.navdsi-timeline {
position: relative;
display: grid;
grid-template-columns: auto minmax(0, 1fr);
width: 100%;
min-width: fit-content;
}
.navdsi-timeline__axislabels {
position: relative;
height: 1rem;
box-sizing: content-box;
grid-column: 2;
margin-bottom: var(--a-spacing-4);
margin-left: var(--a-spacing-8);
}
.navdsi-timeline__row-label {
white-space: nowrap;
width: auto;
margin-right: var(--a-spacing-4);
grid-column: 1;
height: 1.5rem;
display: flex;
align-items: center;
gap: var(--a-spacing-2);
}
.navdsi-timeline__axislabels-label {
position: absolute;
color: var(--ac-timeline-axislabel-text, var(--a-text-subtle));
white-space: nowrap;
}
.navdsi-timeline__row {
display: flex;
background: var(--ac-timeline-row-bg, var(--a-surface-subtle));
margin: 0 0 var(--a-spacing-8);
padding-left: var(--a-spacing-8);
grid-column: 2;
}
.navdsi-timeline__row--active {
background: var(--ac-timeline-row-active-bg, var(--a-surface-selected));
}
.navdsi-timeline__row:last-of-type {
margin-bottom: 0;
}
.navdsi-timeline__row-periods {
height: 1.5rem;
position: relative;
width: 100%;
margin: 0;
}
.navdsi-timeline__row-periods > li {
list-style-type: none;
}
.navdsi-timeline__period {
height: 100%;
border-radius: var(--a-border-radius-full);
position: absolute;
padding: 0;
display: flex;
align-items: center;
z-index: 2;
border: none;
font-size: 1rem;
}
.navdsi-timeline__period--inner {
margin: 0 var(--a-spacing-1) 0 var(--a-spacing-2);
overflow: hidden;
white-space: nowrap;
text-overflow: clip;
text-align: left;
height: 1rem;
}
/* .navdsi-timeline__period > * {
flex-shrink: 0;
} */
.navdsi-timeline__period--clickable {
cursor: pointer;
}
.navdsi-timeline__period--success {
background: var(--ac-timeline-period-success-bg, var(--a-surface-success-subtle));
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-success-border, var(--a-border-success));
}
.navdsi-timeline__period--success.navdsi-timeline__period--clickable:hover {
background: var(--a-surface-success-subtle-hover);
background: var(--ac-timeline-period-success-bg-hover, var(--a-surface-success-subtle-hover));
}
.navdsi-timeline__period--warning {
background: var(--ac-timeline-period-warning-bg, var(--a-surface-warning-subtle));
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-warning-border, var(--a-border-warning));
}
.navdsi-timeline__period--warning.navdsi-timeline__period--clickable:hover {
background: var(--ac-timeline-period-warning-bg-hover, var(--a-surface-warning-subtle-hover));
}
.navdsi-timeline__period--danger {
background: var(--ac-timeline-period-danger-bg, var(--a-surface-danger-subtle));
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-danger-border, var(--a-border-danger));
}
.navdsi-timeline__period--danger.navdsi-timeline__period--clickable:hover {
background: var(--ac-timeline-period-danger-bg-hover, var(--a-surface-danger-subtle-hover));
}
.navdsi-timeline__period--info {
background: var(--ac-timeline-period-info-bg, var(--a-surface-info-subtle));
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-info-border, var(--a-border-info));
}
.navdsi-timeline__period--info.navdsi-timeline__period--clickable:hover {
background: var(--ac-timeline-period-info-bg-hover, var(--a-surface-info-subtle-hover));
}
.navdsi-timeline__period--neutral {
background: var(--ac-timeline-period-neutral-bg, var(--a-bg-subtle));
box-shadow: inset 0 0 0 1px var(--ac-timeline-period-neutral-border, var(--a-border-default));
}
.navdsi-timeline__period--neutral.navdsi-timeline__period--clickable:hover {
background: var(--ac-timeline-period-neutral-bg-hover, var(--a-gray-200));
}
.navdsi-timeline__period--connected-both {
border-radius: 0;
}
.navdsi-timeline__period--connected-right {
border-bottom-right-radius: 0;
border-top-right-radius: 0;
}
.navdsi-timeline__period--connected-left {
border-bottom-left-radius: 0;
border-top-left-radius: 0;
}
.navdsi-timeline__period--selected {
box-shadow: inset 0 0 0 2px var(--ac-timeline-period-selected-border, var(--a-border-action-selected));
z-index: 3;
}
.navdsi-timeline__pin-wrapper {
position: absolute;
grid-column: 2;
top: 0;
z-index: 1;
height: 100%;
display: flex;
flex-direction: column-reverse;
isolation: isolate;
--navdsc-timeline-pin-size: 0.9rem;
}
.navdsi-timeline__pin-button {
border: none;
cursor: pointer;
background: var(--a-surface-default);
box-shadow: inset 0 0 0 1px var(--ac-timeline-pin-bg, var(--a-surface-danger)),
0 0 0 4px var(--ac-timeline-pin-bg, var(--a-surface-danger)), 0 0 0 5px var(--a-surface-default);
z-index: 2;
border-radius: var(--a-border-radius-full);
padding: 3.5px;
position: relative;
}
.navdsi-timeline__pin-button::before {
content: "";
inset: 0;
position: absolute;
width: 25px;
height: 25px;
border-radius: var(--a-border-radius-full);
left: calc(-12.5px + 3.5px);
top: calc(-12.5px + 3.5px);
}
.navdsi-timeline__pin-button:focus {
box-shadow: inset 0 0 0 1px var(--ac-timeline-pin-bg, var(--a-surface-danger)),
0 0 0 3px var(--ac-timeline-pin-bg, var(--a-surface-danger)), 0 0 0 4px var(--a-surface-default),
0 0 0 6px var(--a-border-focus);
outline: none;
}
.navdsi-timeline__pin-button:hover {
box-shadow: inset 0 0 0 1px var(--ac-timeline-pin-bg-hover, var(--a-surface-danger-hover)),
0 0 0 4px var(--ac-timeline-pin-bg-hover, var(--a-surface-danger-hover)), 0 0 0 5px var(--a-surface-default);
}
.navdsi-timeline__pin-button:hover:focus {
box-shadow: inset 0 0 0 1px var(--ac-timeline-pin-bg-hover, var(--a-surface-danger-hover)),
0 0 0 3px var(--ac-timeline-pin-bg-hover, var(--a-surface-danger-hover)), 0 0 0 4px var(--a-surface-default),
0 0 0 6px var(--a-border-focus);
}
.navdsi-timeline__pin-wrapper::before {
content: "";
top: var(--navdsc-timeline-pin-size);
height: calc(94% - var(--navdsc-timeline-pin-size));
width: 1px;
margin: 0 auto;
background: var(--a-surface-inverted);
}
.navdsi-timeline__zoom {
margin-top: var(--a-spacing-6);
float: right;
display: flex;
list-style-type: none;
}
.navdsi-timeline__zoom-button {
all: unset;
cursor: pointer;
padding: 6px 9px 6px 8px;
border: 1px solid var(--ac-timeline-zoom-border, var(--a-border-default));
border-width: 1px 0 1px 1px;
background: var(--ac-timeline-zoom-bg, var(--a-surface-default));
}
.navdsi-timeline__zoom li:first-child .navdsi-timeline__zoom-button {
border-radius: var(--a-border-radius-medium) 0 0 var(--a-border-radius-medium);
}
.navdsi-timeline__zoom li:last-child .navdsi-timeline__zoom-button {
border-width: 1px;
border-radius: 0 var(--a-border-radius-medium) var(--a-border-radius-medium) 0;
}
.navdsi-timeline__zoom li:only-child .navdsi-timeline__zoom-button {
border-radius: var(--a-border-radius-medium);
}
.navdsi-timeline__zoom-button:not([aria-pressed="true"]):hover {
background: var(--ac-timeline-zoom-hover, var(--a-surface-action-subtle-hover));
}
.navdsi-timeline__zoom-button[aria-pressed="true"] {
background: var(--ac-timeline-zoom-selected-bg, var(--a-surface-inverted));
color: var(--ac-timeline-zoom-selected-text, var(--a-text-on-inverted));
}
.navdsi-timeline__period:focus {
outline: none;
z-index: 4;
box-shadow: 0 0 0 2px var(--a-border-focus);
}
.navdsi-timeline__period--selected:focus {
box-shadow: inset 0 0 0 2px var(--ac-timeline-period-selected-border, var(--a-border-action-selected)),
0 0 0 2px var(--a-border-focus);
z-index: 3;
}
.navdsi-timeline__zoom li:focus-within {
z-index: var(--a-z-index-focus);
}
.navdsi-timeline__zoom-button:focus {
box-shadow: 0 0 0 1px var(--a-surface-default), 0 0 0 3px var(--a-border-focus);
border-width: 1px;
}
.navdsi-timeline__zoom li:not(:last-child) .navdsi-timeline__zoom-button:focus {
margin-right: 2px;
padding-right: 6px;
}
+28
View File
@@ -16,5 +16,33 @@
"--ac-header-text": "--a-text-on-inverted",
"--ac-header-hover-bg": "--a-surface-inverted-hover",
"--ac-header-active-bg": "--a-surface-inverted-active"
},
"timeline": {
"--ac-timeline-row-bg": "--a-surface-subtle",
"--ac-timeline-row-active-bg": "--a-surface-selected",
"--ac-timeline-period-success-bg": "--a-surface-success-subtle",
"--ac-timeline-period-success-border": "--a-border-success",
"--ac-timeline-period-success-bg-hover": "--a-surface-success-subtle-hover",
"--ac-timeline-period-warning-bg": "--a-surface-warning-subtle",
"--ac-timeline-period-warning-border": "--a-border-warning",
"--ac-timeline-period-warning-bg-hover": "--a-surface-warning-subtle-hover",
"--ac-timeline-period-danger-bg": "--a-surface-danger-subtle",
"--ac-timeline-period-danger-border": "--a-border-danger",
"--ac-timeline-period-danger-bg-hover": "--a-surface-danger-subtle-hover",
"--ac-timeline-period-info-bg": "--a-surface-info-subtle",
"--ac-timeline-period-info-border": "--a-border-info",
"--ac-timeline-period-info-bg-hover": "--a-surface-info-subtle-hover",
"--ac-timeline-period-neutral-bg": "--a-bg-subtle",
"--ac-timeline-period-neutral-border": "--a-border-default",
"--ac-timeline-period-neutral-bg-hover": "--a-gray-200",
"--ac-timeline-period-selected-border": "--a-border-action-selected",
"--ac-timeline-pin-bg": "--a-surface-danger",
"--ac-timeline-pin-bg-hover": "--a-surface-danger-hover",
"--ac-timeline-zoom-border": "--a-border-default",
"--ac-timeline-zoom-bg": "--a-surface-default",
"--ac-timeline-zoom-hover": "--a-surface-action-subtle-hover",
"--ac-timeline-zoom-selected-bg": "--a-surface-inverted",
"--ac-timeline-zoom-selected-text": "--a-text-on-inverted",
"--ac-timeline-axislabel-text": "--a-text-subtle"
}
}
+61
View File
@@ -1,5 +1,66 @@
# @navikt/ds-react-internal
## 2.1.0
### Minor Changes
- [#1665](https://github.com/navikt/Designsystemet/pull/1665) [`6ab84e581`](https://github.com/navikt/Designsystemet/commit/6ab84e581f8d3e850bcec340608354335506a051) Thanks [@KenAJoh](https://github.com/KenAJoh)! - :sparkles: Ny Timeline-komponent for interne-flater
:bug: Fikset feil danger-hover token
### Patch Changes
- Updated dependencies [[`0873fe652`](https://github.com/navikt/Designsystemet/commit/0873fe6520e817e462197c162b49c05826da4838)]:
- @navikt/ds-react@2.1.0
- @navikt/ds-icons@2.1.0
## 2.0.18
### Patch Changes
- Updated dependencies [[`98177d0cd`](https://github.com/navikt/Designsystemet/commit/98177d0cd3802029dcf398ce93c1767d3b0860c5)]:
- @navikt/ds-react@2.0.18
- @navikt/ds-icons@2.0.18
## 2.0.17
### Patch Changes
- Updated dependencies [[`27e3dfee0`](https://github.com/navikt/Designsystemet/commit/27e3dfee07e135893e9f62cd32f80ed83a3510de)]:
- @navikt/ds-icons@2.0.17
- @navikt/ds-react@2.0.17
## 2.0.16
### Patch Changes
- Updated dependencies [[`54f8b0795`](https://github.com/navikt/Designsystemet/commit/54f8b07956bbfb7de51f6682a5fc64903bfd1bdf)]:
- @navikt/ds-icons@2.0.16
- @navikt/ds-react@2.0.16
## 2.0.15
### Patch Changes
- Updated dependencies [[`22dfd8e60`](https://github.com/navikt/Designsystemet/commit/22dfd8e60315c421749394d35e41f412a00a7593)]:
- @navikt/ds-react@2.0.15
- @navikt/ds-icons@2.0.15
## 2.0.14
### Patch Changes
- Updated dependencies [[`8e5c55443`](https://github.com/navikt/Designsystemet/commit/8e5c554431c98b1b658c5054c0b3cf08b8a65e52)]:
- @navikt/ds-react@2.0.14
- @navikt/ds-icons@2.0.14
## 2.0.13
### Patch Changes
- Updated dependencies [[`a6c339123`](https://github.com/navikt/Designsystemet/commit/a6c3391237f13394063e4c004ac9efb1875fc944)]:
- @navikt/ds-icons@2.0.13
- @navikt/ds-react@2.0.13
## 2.0.12
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react-internal",
"version": "2.0.12",
"version": "2.1.0",
"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.12",
"@navikt/ds-react": "^2.0.12",
"@navikt/ds-icons": "^2.1.0",
"@navikt/ds-react": "^2.1.0",
"clsx": "^1.1.1",
"copy-to-clipboard": "^3.3.1"
},
+1
View File
@@ -1,3 +1,4 @@
export * from "./copy-to-clipboard";
export * from "./dropdown";
export * from "./header";
export * from "./timeline";
@@ -0,0 +1,142 @@
import { Detail } from "@navikt/ds-react";
import {
addDays,
addMonths,
addYears,
differenceInDays,
differenceInMonths,
differenceInYears,
endOfMonth,
endOfYear,
format,
startOfDay,
startOfMonth,
startOfYear,
subDays,
} from "date-fns";
import React from "react";
import { useTimelineContext } from "./hooks/useTimelineContext";
import { isVisible } from "./utils";
import { horizontalPositionAndWidth } from "./utils/calc";
import { AxisLabel } from "./utils/types.external";
export const dayLabels = (
start: Date,
end: Date,
totalDays: number,
direction: "left" | "right"
): AxisLabel[] => {
const increment = Math.ceil(totalDays / 10);
const lastDay = startOfDay(end);
return new Array(totalDays)
.fill(lastDay)
.map((thisDay, i) => {
if (i % increment !== 0) return null;
const day: Date = subDays(thisDay, i);
const { horizontalPosition, width } = horizontalPositionAndWidth(
day,
addDays(day, 1),
start,
end
);
return {
direction: direction,
horizontalPosition: horizontalPosition,
label: format(day, "MM.dd"),
date: day,
width: width,
};
})
.filter((label) => label !== null) as AxisLabel[];
};
export const monthLabels = (
start: Date,
end: Date,
direction: "left" | "right"
): AxisLabel[] => {
const startMonth = startOfMonth(start);
const endMonth = endOfMonth(end);
const numberOfMonths = differenceInMonths(endMonth, startMonth) + 1;
return new Array(numberOfMonths).fill(startMonth).map((thisMonth, i) => {
const month: Date = addMonths(thisMonth, i);
const { horizontalPosition, width } = horizontalPositionAndWidth(
month,
addMonths(month, 1),
start,
end
);
return {
direction: direction,
horizontalPosition: horizontalPosition,
label: format(month, "MMM yy"),
date: month,
width: width,
};
});
};
export const yearLabels = (
start: Date,
end: Date,
direction: "left" | "right"
): AxisLabel[] => {
const firstYear = startOfYear(start);
const lastYear = endOfYear(end);
const yearCount = differenceInYears(lastYear, start) + 1;
return new Array(yearCount).fill(firstYear).map((thisYear, i) => {
const year: Date = addYears(thisYear, i);
const { horizontalPosition, width } = horizontalPositionAndWidth(
year,
addYears(year, 1),
start,
end
);
return {
direction: direction,
horizontalPosition: horizontalPosition,
label: year.getFullYear().toString(),
date: year,
width: width,
};
});
};
const axisLabels = (
start: Date,
end: Date,
direction: "left" | "right"
): AxisLabel[] => {
const totalDays = differenceInDays(end, start);
if (totalDays < 40) {
return dayLabels(start, end, totalDays, direction);
} else if (totalDays < 370) {
return monthLabels(start, end, direction);
} else {
return yearLabels(start, end, direction);
}
};
export const AxisLabels = () => {
const { endDate, startDate, direction } = useTimelineContext();
const labels = axisLabels(startDate, endDate, direction).filter(isVisible);
return (
<div className="navdsi-timeline__axislabels" aria-hidden="true">
{labels.map((etikett) => (
<Detail
className="navdsi-timeline__axislabels-label"
as="div"
key={etikett.label}
style={{
justifyContent: direction === "left" ? "flex-start" : "flex-end",
[direction]: `${etikett.horizontalPosition}%`,
width: `${etikett.width}%`,
}}
>
{etikett.label}
</Detail>
))}
</div>
);
};
@@ -0,0 +1,66 @@
import { mergeRefs, Popover } from "@navikt/ds-react";
import { format } from "date-fns";
import React, { forwardRef, useMemo, useRef, useState } from "react";
import { useTimelineContext } from "./hooks/useTimelineContext";
import { position } from "./utils/calc";
import { TimelineComponentTypes } from "./utils/types.internal";
export interface TimelinePinProps {
/**
* Date position for the pin.
*/
date: Date;
/**
* Content in Pin Popover.
*/
children?: React.ReactNode;
}
export interface PinType
extends React.ForwardRefExoticComponent<
TimelinePinProps & React.RefAttributes<HTMLButtonElement>
> {
componentType: TimelineComponentTypes;
}
export const Pin = forwardRef<HTMLButtonElement, TimelinePinProps>(
({ date, children, ...rest }, ref) => {
const { startDate, endDate, direction } = useTimelineContext();
const [open, setOpen] = useState(false);
const buttonRef = useRef<HTMLButtonElement | null>(null);
const mergedRef = useMemo(() => mergeRefs([buttonRef, ref]), [ref]);
return (
<>
<div
className="navdsi-timeline__pin-wrapper"
style={{ [direction]: `${position(date, startDate, endDate)}%` }}
>
<button
{...rest}
ref={mergedRef}
className="navdsi-timeline__pin-button"
onClick={() => setOpen(!open)}
aria-label={`pin:${format(date, "dd.MM.yyyy")}`}
aria-expanded={open}
/>
</div>
{children && (
<Popover
open={open}
onClose={() => setOpen(false)}
anchorEl={buttonRef.current}
offset={18}
>
<Popover.Content>{children}</Popover.Content>
</Popover>
)}
</>
);
}
) as PinType;
Pin.componentType = "pin";
export default Pin;
@@ -0,0 +1,209 @@
import { endOfDay, isSameDay, startOfDay } from "date-fns";
import React, { forwardRef, useMemo, useRef, useState } from "react";
import { AxisLabels } from "./AxisLabels";
import { RowContext } from "./hooks/useRowContext";
import { TimelineContext } from "./hooks/useTimelineContext";
import {
useEarliestDate,
useLatestDate,
useTimelineRows,
} from "./hooks/useTimelineRows";
import Period, { PeriodType } from "./period";
import Pin, { PinType } from "./Pin";
import TimelineRow, { TimelineRowType } from "./TimelineRow";
import { parseRows } from "./utils/timeline";
import Zoom, { ZoomType } from "./zoom";
export interface TimelineProps extends React.HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
/**
* Decides startingpoint in timeline.
* Defaults to earliest date among the timeline periods.
*/
startDate?: Date;
/**
* Decides end-date for timeline.
* Defaults to the latest date among the timeline periods.
*/
endDate?: Date;
/**
* Decides direction which periods are sorted/displayed. "left" ascends from earliest date on left.
* @default "left"
*/
direction?: "left" | "right";
}
interface TimelineComponent
extends React.ForwardRefExoticComponent<TimelineProps> {
/**
* Built-in timeline row
*/
Row: TimelineRowType;
/**
* Built-in row period
*/
Period: PeriodType;
/**
* Built-in timeline pin
*/
Pin: PinType;
/**
* Built-in timeline zoom-component
*/
Zoom: ZoomType;
}
export const Timeline = forwardRef<HTMLDivElement, TimelineProps>(
({ children, startDate, endDate, direction = "left", ...rest }, ref) => {
const isMultipleRows = Array.isArray(children);
const firstFocusabled = useRef<
{ ref: HTMLButtonElement | null; id: number }[]
>([]);
if (!isMultipleRows) {
children = [children];
}
const rowChildren = React.Children.toArray(children).filter(
(c: any) => c?.type?.componentType === "row"
);
const pins = React.Children.toArray(children).filter(
(c: any) => c?.type?.componentType === "pin"
);
const zoomComponent = React.Children.toArray(children).find(
(c: any) => c?.type?.componentType === "zoom"
);
const rowsRaw = useMemo(() => {
return parseRows(rowChildren);
}, [rowChildren]);
const rows = rowsRaw.map((r) => {
if (r?.periods) {
return r.periods;
}
return [];
});
const initialStartDate = startOfDay(useEarliestDate({ startDate, rows }));
const [start, setStart] = useState(initialStartDate);
const [activeRow, setActiveRow] = useState<number | null>(null);
const [endInclusive, setEndInclusive] = useState(
endOfDay(useLatestDate({ endDate, rows }))
);
const initialEndDate = endOfDay(useLatestDate({ endDate, rows }));
const processedRows = useTimelineRows(
rowsRaw,
start,
endInclusive,
direction
);
const handleZoomChange = (zoomStart: Date) => {
if (direction === "left") {
if (isSameDay(zoomStart, start)) {
setStart(initialStartDate);
return;
}
setStart(zoomStart);
} else {
if (isSameDay(zoomStart, endInclusive)) {
setEndInclusive(initialEndDate);
return;
}
setEndInclusive(zoomStart);
}
};
const handleActiveRowChange = (key: string) => {
if (activeRow !== null && key === "ArrowDown") {
for (let i = activeRow + 1; i < processedRows.length; i++) {
const row = processedRows[i];
if (row.periods.find((p) => !!p.children || !!p.onSelectPeriod)) {
setActiveRow(i);
firstFocusabled.current.find((x) => x.id === i)?.ref?.focus();
break;
}
}
return;
}
if (activeRow !== null && key === "ArrowUp") {
for (let i = activeRow - 1; i >= 0; i--) {
const row = processedRows[i];
if (row.periods.find((p) => !!p.children || !!p.onSelectPeriod)) {
setActiveRow(i);
firstFocusabled.current.find((x) => x.id === i)?.ref?.focus();
break;
}
}
return;
}
};
const addFocusable = (ref: HTMLButtonElement | null, id: number) => {
let items = firstFocusabled.current;
items = items.filter((x) => x.id !== id);
items.push({ ref, id });
firstFocusabled.current = items;
};
return (
<TimelineContext.Provider
value={{
startDate: start,
endDate: endInclusive,
direction: direction,
setStart: (d) => handleZoomChange(d),
setEndInclusive: (d) => setEndInclusive(d),
activeRow: activeRow,
setActiveRow: (key) => handleActiveRowChange(key),
initiate: (i) => setActiveRow(i),
addFocusable,
}}
>
<div {...rest} ref={ref}>
<div className="navdsi-timeline">
<AxisLabels />
{pins.map((pin) => {
return pin;
})}
{processedRows.map((row, i) => {
return (
<RowContext.Provider
key={`row-${row.id}`}
value={{
periods: row.periods,
id: row.id,
active: activeRow === i,
index: i,
}}
>
<TimelineRow
{...row?.restProps}
ref={row?.ref}
label={row.label}
icon={row.icon}
headingTag={row.headingTag}
/>
</RowContext.Provider>
);
})}
</div>
{zoomComponent && zoomComponent}
</div>
</TimelineContext.Provider>
);
}
) as TimelineComponent;
Timeline.Row = TimelineRow;
Timeline.Period = Period;
Timeline.Pin = Pin;
Timeline.Zoom = Zoom;
export default Timeline;
@@ -0,0 +1,122 @@
import { BodyShort } from "@navikt/ds-react";
import { format } from "date-fns";
import React, { forwardRef } from "react";
import { PeriodContext } from "./hooks/usePeriodContext";
import { useRowContext } from "./hooks/useRowContext";
import { useTimelineContext } from "./hooks/useTimelineContext";
import Period from "./period";
import {
PositionedPeriod,
TimelineComponentTypes,
} from "./utils/types.internal";
import cl from "clsx";
export interface TimelineRowProps
extends React.HTMLAttributes<HTMLOListElement> {
/**
* Label for the timeline row
*/
label: string;
/**
* Heading level for the label e.g h2, h3...
* @default "h3"
*/
headingTag?: "h2" | "h3" | "h4" | "h5" | "h6";
/**
* Icon next to label
*/
icon?: React.ReactNode;
}
export interface TimelineRowType
extends React.ForwardRefExoticComponent<
TimelineRowProps & React.RefAttributes<HTMLOListElement>
> {
componentType: TimelineComponentTypes;
}
export const TimelineRow = forwardRef<HTMLOListElement, TimelineRowProps>(
({ label, headingTag = "h3", icon, ...rest }, ref) => {
const { periods, id, active } = useRowContext();
const { setActiveRow } = useTimelineContext();
const latest = periods.reduce((a, b) => {
return a.end > b.end ? a : b;
}, {} as PositionedPeriod);
const earliest = periods.reduce((a, b) => {
return a.end < b.end ? a : b;
}, {} as PositionedPeriod);
const firstFocusable = periods.find(
(p) => !!p.children || !!p.onSelectPeriod
);
return (
<>
{label && (
<BodyShort
as={headingTag}
id={`timeline-row-${id}`}
className="navdsi-timeline__row-label"
size="small"
>
{icon}
{label}
</BodyShort>
)}
<div
className={cl("navdsi-timeline__row", {
"navdsi-timeline__row--active": active,
})}
>
<ol
{...rest}
ref={ref}
aria-label={
periods.length === 0
? "Ingen perioder"
: `${format(earliest.start, "dd.MM.yyyy")} til ${format(
latest.end,
"dd.MM.yyyy"
)}`
}
className="navdsi-timeline__row-periods"
onKeyDown={(e) => {
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault();
setActiveRow(e.key);
}
}}
>
{periods &&
periods.map((period) => {
return (
<li key={`period-${period.id}`}>
<PeriodContext.Provider
value={{
periodId: period.id,
firstFocus: firstFocusable?.id === period.id,
restProps: period?.restProps,
}}
>
<Period
start={period.start}
end={period.endInclusive}
icon={period.icon}
ref={period?.ref}
/>
</PeriodContext.Provider>
</li>
);
})}
</ol>
</div>
</>
);
}
) as TimelineRowType;
TimelineRow.componentType = "row";
export default TimelineRow;
@@ -0,0 +1,22 @@
import { createContext, useContext } from "react";
interface PeriodContextProps {
periodId: String;
firstFocus: boolean;
restProps?: any;
}
export const PeriodContext = createContext<PeriodContextProps>({
periodId: "",
firstFocus: false,
});
export const usePeriodContext = () => {
const context = useContext(PeriodContext);
if (!context) {
console.warn("usePeriodContext must be used with PeriodContext");
}
return context;
};
@@ -0,0 +1,26 @@
import { createContext, useContext } from "react";
import { PositionedPeriod } from "../utils/types.internal";
interface RowContextProps {
periods: PositionedPeriod[];
id: string;
active: boolean;
index: number;
}
export const RowContext = createContext<RowContextProps>({
periods: [],
id: "",
active: false,
index: 0,
});
export const useRowContext = () => {
const context = useContext(RowContext);
if (!context) {
console.warn("useRowContext must be used with RowContext");
}
return context;
};
@@ -0,0 +1,37 @@
import { createContext, useContext } from "react";
interface TimelineContextProps {
startDate: Date;
endDate: Date;
direction: "left" | "right";
setStart: (d: Date) => void;
setEndInclusive: (d: Date) => void;
activeRow: number | null;
setActiveRow: (i: string) => void;
initiate: (i: number) => void;
addFocusable: (ref: HTMLButtonElement | null, id: number) => void;
}
export const TimelineContext = createContext<TimelineContextProps>({
startDate: new Date(),
endDate: new Date(),
direction: "left",
setStart: () => null,
setEndInclusive: () => null,
activeRow: 0,
setActiveRow: () => null,
initiate: () => null,
addFocusable: () => null,
});
export const useTimelineContext = () => {
const context = useContext(TimelineContext);
if (!context) {
console.warn(
"useTimelineContext must be used with TimelineContext (<Timeline />)"
);
}
return context;
};
@@ -0,0 +1,161 @@
import { useMemo } from "react";
import { addDays, endOfDay, isAfter, startOfDay, subDays } from "date-fns";
import { lastPeriod } from "../utils/sort";
import { Period } from "../utils/types.external";
import {
InternalSimpleTimeline,
PositionedPeriod,
} from "../utils/types.internal";
import { horizontalPositionAndWidth } from "../utils/calc";
import { invisiblePeriods, withinADay } from "../utils/filter";
const spatialPeriod = (
period: Period,
timelineStart: Date,
timelineEndInclusive: Date,
direction: "left" | "right" = "left",
i: number,
periods: PositionedPeriod[],
rowIndex: number
): PositionedPeriod => {
const start = period.start;
const endInclusive = period.end;
const rightOverlap =
i < periods.length - 1 && !isAfter(periods[i + 1].start, endInclusive);
const { horizontalPosition, width } = horizontalPositionAndWidth(
startOfDay(start),
endOfDay(
rightOverlap ? startOfDay(subDays(periods[i + 1].start, 1)) : endInclusive
),
timelineStart,
timelineEndInclusive
);
return {
id: `r-${rowIndex}-p-${i}`,
start: start,
endInclusive: endInclusive,
horizontalPosition: horizontalPosition,
direction: direction,
width: width,
end: endInclusive,
status: period.status,
onSelectPeriod: period.onSelectPeriod,
icon: period.icon,
children: period.children,
isActive: period.isActive,
statusLabel: period.statusLabel,
};
};
const adjustedEdges = (
period: PositionedPeriod,
i: number,
allPeriods: PositionedPeriod[]
): PositionedPeriod => {
const left =
i > 0 && withinADay(period.start, allPeriods[i - 1].endInclusive);
const right =
i < allPeriods.length - 1 &&
withinADay(allPeriods[i + 1].start, period.endInclusive);
return left && right
? { ...period, cropped: "both" }
: left
? { ...period, cropped: "left" }
: right
? { ...period, cropped: "right" }
: period;
};
const trimmedPeriods = (period: PositionedPeriod) => {
let { horizontalPosition, width, cropped } = period;
if (horizontalPosition + width > 100) {
width = 100 - horizontalPosition;
cropped = cropped === "left" || cropped === "both" ? "both" : "right";
}
if (horizontalPosition < 0 && horizontalPosition + width > 0) {
width = horizontalPosition + width;
horizontalPosition = 0;
cropped = cropped === "right" || cropped === "both" ? "both" : "left";
}
return {
...period,
width: width,
horizontalPosition: horizontalPosition,
cropped: cropped,
};
};
export const useTimelineRows = (
rows: any,
startDate: Date,
endDate: Date,
direction: "left" | "right"
): InternalSimpleTimeline[] =>
useMemo(
() =>
rows.map((periods: InternalSimpleTimeline, i: number) => {
const rowIndex = i;
const timelinePeriods = periods.periods
.sort((a: Period, b: Period) => a.start.valueOf() - b.start.valueOf())
.map((period: Period & { restProps?: any; ref?: any }, i) => ({
...spatialPeriod(
period,
startDate,
endDate,
direction,
i,
periods.periods,
rowIndex
),
restProps: period?.restProps,
ref: period?.ref,
}))
.sort(lastPeriod)
.map(adjustedEdges)
.map(trimmedPeriods)
.filter(invisiblePeriods);
return {
id: `tl-row-${rowIndex}`,
label: periods.label,
headingTag: periods.headingTag || "h3",
icon: periods.icon,
periods:
direction === "left" ? timelinePeriods : timelinePeriods.reverse(),
restProps: periods?.restProps,
ref: periods?.ref,
};
}),
[rows, startDate, endDate, direction]
);
const earliestDate = (earliest: Date, period: Period) =>
period.start < earliest ? period.start : earliest;
const earliestFomDate = (rader: Period[][]) =>
rader.flat().reduce(earliestDate, new Date());
export const useEarliestDate = ({ startDate, rows }: any) =>
useMemo(
() => (startDate ? startDate : earliestFomDate(rows)),
[startDate, rows]
);
const latestDate = (latest: Date, period: Period) =>
period.end > latest ? period.end : latest;
const latestTomDate = (rows: Period[][]) =>
rows.flat().reduce(latestDate, new Date(0));
export const useLatestDate = ({ endDate, rows }: any) =>
useMemo(
() => (endDate ? endDate : addDays(latestTomDate(rows), 1)),
[endDate, rows]
);
@@ -0,0 +1,6 @@
export { default as Timeline } from "./Timeline";
export type { TimelineProps } from "./Timeline";
export type { TimelineRowProps } from "./TimelineRow";
export type { TimelinePinProps } from "./Pin";
export type { TimelinePeriodProps } from "./period";
export type { TimelineZoomButtonProps } from "./zoom/ZoomButton";
@@ -0,0 +1,91 @@
import { mergeRefs, Popover } from "@navikt/ds-react";
import cl from "clsx";
import React, { useMemo, useRef, useState } from "react";
import { usePeriodContext } from "../hooks/usePeriodContext";
import { useRowContext } from "../hooks/useRowContext";
import { useTimelineContext } from "../hooks/useTimelineContext";
import { ariaLabel, getConditionalClasses } from "../utils/period";
import { PeriodProps } from "./index";
interface TimelineClickablePeriodProps extends PeriodProps {
onSelectPeriod?: () => void;
isActive?: boolean;
periodRef: React.ForwardedRef<HTMLButtonElement>;
}
const ClickablePeriod = React.memo(
({
onSelectPeriod,
start,
end,
status,
cropped,
direction,
left,
width,
icon,
children,
isActive,
statusLabel,
restProps,
periodRef,
}: TimelineClickablePeriodProps) => {
const buttonRef = useRef<HTMLButtonElement | null>(null);
const [selected, setSelected] = useState(false);
const { index } = useRowContext();
const { firstFocus } = usePeriodContext();
const { initiate, addFocusable } = useTimelineContext();
const mergedRef = useMemo(
() => mergeRefs([buttonRef, periodRef]),
[periodRef]
);
return (
<>
<button
{...restProps}
ref={(r) => {
firstFocus && addFocusable(r, index);
mergedRef(r);
}}
onClick={() => {
children && setSelected((x) => !x);
onSelectPeriod?.();
}}
aria-label={ariaLabel(start, end, status, statusLabel)}
className={cl(
"navdsi-timeline__period--clickable",
getConditionalClasses(cropped, direction, status),
restProps?.className,
{
"navdsi-timeline__period--selected": isActive,
}
)}
style={{
width: `${width}%`,
[direction]: `${left}%`,
}}
aria-expanded={children ? selected : undefined}
onFocus={() => {
initiate(index);
}}
>
<div className="navdsi-timeline__period--inner">{icon}</div>
</button>
{children && (
<Popover
open={selected}
onClose={() => setSelected(false)}
anchorEl={buttonRef.current}
strategy="fixed"
>
<Popover.Content>{children}</Popover.Content>
</Popover>
)}
</>
);
}
);
export default ClickablePeriod;
@@ -0,0 +1,44 @@
import React from "react";
import { ariaLabel, getConditionalClasses } from "../utils/period";
import { PeriodProps } from "./index";
import cl from "clsx";
interface TimelineNonClickablePeriodProps extends PeriodProps {
periodRef?: React.ForwardedRef<HTMLDivElement>;
}
const NonClickablePeriod = ({
start,
end,
status,
cropped,
direction,
left,
width,
icon,
statusLabel,
restProps,
periodRef,
}: TimelineNonClickablePeriodProps) => {
return (
<div
ref={periodRef}
{...restProps}
className={cl(
getConditionalClasses(cropped, direction, status),
restProps?.classname
)}
style={{
width: `${width}%`,
[direction]: `${left}%`,
}}
>
<div className="navdsi-timeline__period--inner">
{icon}
<p className="sr-only">{ariaLabel(start, end, status, statusLabel)}</p>
</div>
</div>
);
};
export default NonClickablePeriod;
@@ -0,0 +1,130 @@
import React, { forwardRef } from "react";
import { usePeriodContext } from "../hooks/usePeriodContext";
import { useRowContext } from "../hooks/useRowContext";
import { TimelineComponentTypes } from "../utils/types.internal";
import ClickablePeriod from "./ClickablePeriod";
import NonClickablePeriod from "./NonClickablePeriod";
export interface TimelinePeriodProps
extends React.HTMLAttributes<HTMLDivElement> {
/**
* Period start date.
*/
start: Date;
/**
* Period end date.
*/
end: Date;
/**
* Icon for easier visual identification.
*/
icon?: React.ReactNode;
/**
* Period status.
* @default "neutral"
*/
status?: "success" | "warning" | "danger" | "info" | "neutral";
/**
* Status label for screen-readers
* e.g "Sykemeldt", "foreldrepermisjon"
*/
statusLabel?: string;
/**
* Callback when selecting a period.
*/
onSelectPeriod?: () => void;
/**
* Content displayed in Popover on click.
*/
children?: React.ReactNode;
/**
* Visual active inidcation on period
* @note Make sure only one period is active at a time
*/
isActive?: boolean;
}
export interface PeriodProps {
start: Date;
end: Date;
status: string;
cropped: string;
direction: string;
width: Number;
left: Number;
icon?: React.ReactNode;
children?: React.ReactNode;
statusLabel?: string;
restProps?: any;
}
export interface PeriodType
extends React.ForwardRefExoticComponent<
TimelinePeriodProps &
React.RefAttributes<HTMLDivElement | HTMLButtonElement>
> {
componentType: TimelineComponentTypes;
}
export const Period = forwardRef<HTMLDivElement, TimelinePeriodProps>(
({ icon }, ref) => {
const { periods } = useRowContext();
const { periodId, restProps } = usePeriodContext();
const period = periods.find((p) => p.id === periodId);
if (!period) {
return <></>;
}
const {
start,
endInclusive,
width,
horizontalPosition,
status = "neutral",
onSelectPeriod,
cropped,
direction,
children,
isActive,
statusLabel,
} = period;
return onSelectPeriod || children ? (
<ClickablePeriod
periodRef={ref as React.ForwardedRef<HTMLButtonElement>}
start={start}
end={endInclusive}
status={status}
onSelectPeriod={onSelectPeriod}
cropped={cropped || ""}
direction={direction}
width={width}
left={horizontalPosition}
icon={icon}
children={children}
isActive={isActive}
statusLabel={statusLabel}
restProps={restProps}
/>
) : (
<NonClickablePeriod
periodRef={ref}
start={start}
end={endInclusive}
status={status}
cropped={cropped || ""}
direction={direction}
width={width}
left={horizontalPosition}
icon={icon}
statusLabel={statusLabel}
restProps={restProps}
/>
);
}
) as PeriodType;
Period.componentType = "period";
export default Period;
@@ -0,0 +1,326 @@
import { SuccessFilled } from "@navikt/ds-icons";
import { useState } from "@storybook/addons";
import { Meta } from "@storybook/react";
import * as React from "react";
import Timeline from "./Timeline";
export default {
title: "ds-react-internal/Timeline",
component: Timeline,
argTypes: {},
} as Meta;
const DummyLabel = () => {
return (
<div
style={{
width: "239px",
margin: 0,
textAlign: "left",
fontSize: "14px",
}}
>
<p>Periode: 15.07.2019 - 25.07.2019</p>
<p>Utbetalt: 12 345,00 kr</p>
<p style={{ color: "red" }}>Dager igjen: 0</p>
</div>
);
};
const row1 = [
{
id: "1",
start: new Date("Jan 1 2022"),
end: new Date("Jan 31 2022"),
status: "success",
icon: <SuccessFilled aria-hidden />,
statusLabel: "Sykemeldt",
},
{
id: "2",
start: new Date("Feb 1 2022"),
end: new Date("Mar 15 2022"),
status: "danger",
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
statusLabel: "Sykemeldt",
},
{
id: "3",
start: new Date("May 2 2022"),
end: new Date("May 25 2022"),
status: "success",
icon: <SuccessFilled aria-hidden />,
statusLabel: "Sykemeldt",
},
{
id: "4",
start: new Date("Mar 1 2022"),
end: new Date("Mar 31 2022"),
status: "success",
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
icon: <SuccessFilled aria-hidden />,
label: "test",
statusLabel: "Sykemeldt",
},
{
id: "5",
start: new Date("Jul 1 2022"),
end: new Date("Jul 31 2022"),
status: "warning",
icon: <SuccessFilled aria-hidden />,
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
statusLabel: "Sykemeldt",
},
{
id: "6",
start: new Date("Aug 1 2022"),
end: new Date("Aug 30 2022"),
status: "warning",
icon: <SuccessFilled aria-hidden />,
statusLabel: "Sykemeldt",
},
];
const row2 = [
{
id: "7",
start: new Date("May 13 2022"),
end: new Date("May 25 2022"),
status: "warning",
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
icon: <SuccessFilled aria-hidden />,
children: <DummyLabel />,
},
{
id: "8",
start: new Date("Feb 1 2022"),
end: new Date("May 2 2022"),
status: "neutral",
onSelectPeriod: () => console.log("PERIOD SELECTED!"),
icon: <SuccessFilled aria-hidden />,
children: <DummyLabel />,
},
];
export const Default = () => {
return (
<div style={{ width: "80vw" }}>
<Timeline>
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
{row1.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
{row2.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
</Timeline>
</div>
);
};
export const WithPins = () => {
return (
<div style={{ width: "80vw" }}>
<Timeline>
<Timeline.Pin date={new Date("Apr 15 2022")}>Pin 1</Timeline.Pin>
<Timeline.Pin date={new Date("Jun 12 2022")}>Pin 2</Timeline.Pin>
<Timeline.Pin date={new Date("Jul 28 2022")}>Pin 3</Timeline.Pin>
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
{row1.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
{row2.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
</Timeline>
</div>
);
};
export const WithZoom = () => {
return (
<div style={{ width: "80vw" }}>
<Timeline>
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
{row1.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
{row2.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
<Timeline.Zoom>
<Timeline.Zoom.Button label="3 mnd" interval="month" count={3} />
<Timeline.Zoom.Button label="7 mnd" interval="month" count={7} />
<Timeline.Zoom.Button label="9 mnd" interval="month" count={9} />
</Timeline.Zoom>
</Timeline>
</div>
);
};
export const ActivePeriod = () => {
const [activePeriod, setActivePeriod] = useState(row1[0]);
return (
<div style={{ width: "80vw" }}>
<Timeline>
<Timeline.Pin date={new Date("Apr 15 2022")}>Pin 1</Timeline.Pin>
<Timeline.Pin date={new Date("Jun 12 2022")}>Pin 2</Timeline.Pin>
<Timeline.Pin date={new Date("Jul 28 2022")}>Pin 3</Timeline.Pin>
<Timeline.Row label="Rad 1" icon={<SuccessFilled aria-hidden />}>
{row1.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
onSelectPeriod={() => {
setActivePeriod(p);
p?.onSelectPeriod?.();
}}
icon={p.icon}
isActive={activePeriod.id === p.id}
statusLabel={p.statusLabel}
>
{p.children}
</Timeline.Period>
);
})}
</Timeline.Row>
<Timeline.Row label="Rad 2">
{row2.map((p: any, i) => {
return (
<Timeline.Period
key={p.id}
start={p.start}
end={p.end}
status={p.status}
onSelectPeriod={() => {
setActivePeriod(p);
p?.onSelectPeriod?.();
}}
icon={p.icon}
isActive={activePeriod.id === p.id}
statusLabel={p.statusLabel}
>
{p.children}
</Timeline.Period>
);
})}
</Timeline.Row>
<Timeline.Row label="Rad 3">
<Timeline.Period
start={new Date("Jan 1 2022")}
end={new Date("Aug 20 2022")}
status="info"
icon={<SuccessFilled aria-hidden />}
/>
</Timeline.Row>
</Timeline>
</div>
);
};
export const WithDayLabels = () => {
return (
<div style={{ width: "80vw" }}>
<Timeline>
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
<Timeline.Period
start={new Date("Feb 4 2022")}
end={new Date("Feb 13 2022")}
status="success"
/>
</Timeline.Row>
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
<Timeline.Period
start={new Date("Feb 17 2022")}
end={new Date("Feb 22 2022")}
status="warning"
/>
</Timeline.Row>
</Timeline>
</div>
);
};
export const WithYearLabels = () => {
return (
<div style={{ width: "80vw" }}>
<Timeline>
<Timeline.Row label="Row 1" icon={<SuccessFilled aria-hidden />}>
<Timeline.Period
start={new Date("Feb 4 2022")}
end={new Date("Feb 13 2028")}
status="success"
/>
</Timeline.Row>
<Timeline.Row label="Row 2" icon={<SuccessFilled aria-hidden />}>
<Timeline.Period
start={new Date("Feb 17 2024")}
end={new Date("Feb 22 2031")}
status="warning"
/>
</Timeline.Row>
</Timeline>
</div>
);
};
@@ -0,0 +1,26 @@
import { differenceInMilliseconds } from "date-fns";
export const position = (date: Date, start: Date, endInclusive: Date) => {
const diff = differenceInMilliseconds(endInclusive, start);
return (differenceInMilliseconds(date, start) / diff) * 100;
};
export const horizontalPositionAndWidth = (
start: Date,
endInclusive: Date,
timelineStart: Date,
timelineEndInclusive: Date
) => {
const horizontalPosition = position(
start,
timelineStart,
timelineEndInclusive
);
const width =
position(endInclusive, timelineStart, timelineEndInclusive) -
horizontalPosition;
return {
horizontalPosition: horizontalPosition,
width: width,
};
};
@@ -0,0 +1,32 @@
import { differenceInDays } from "date-fns";
import { PeriodProps } from "../period";
import { Spatial } from "./types.internal";
export interface Positioned {
horizontalPosition: number;
direction: "left" | "right";
}
export const isVisible = ({ horizontalPosition }: Positioned): boolean =>
horizontalPosition <= 98 && horizontalPosition >= 0;
export const getFirstDate = (periods: any) => {
return periods.sort(
(a: PeriodProps, b: PeriodProps) => a.start.getTime() - b.start.getTime()
)[0].start;
};
export const getLastDate = (periods: any) => {
return periods.sort(
(a: PeriodProps, b: PeriodProps) => a.end.getTime() - b.end.getTime()
)[periods.length - 1].end;
};
export const withinADay = (date1: Date, date2: Date): boolean =>
differenceInDays(date1, date2) <= 1;
export const invisiblePeriods = ({
horizontalPosition,
width,
}: Positioned & Spatial) =>
horizontalPosition >= 0 && horizontalPosition <= 100 && width > 0;
@@ -0,0 +1 @@
export { getFirstDate, getLastDate, isVisible, Positioned } from "./filter";
@@ -0,0 +1,48 @@
import cl from "clsx";
import { format } from "date-fns";
export const getConditionalClasses = (
cropped: string,
direction: string,
status: string
) => {
return cl(`navdsi-timeline__period navdsi-timeline__period--${status}`, {
"navdsi-timeline__period--connected-both": cropped === "both",
"navdsi-timeline__period--connected-right":
(cropped === "right" && direction === "left") ||
(cropped === "left" && direction === "right"),
"navdsi-timeline__period--connected-left":
(cropped === "left" && direction === "left") ||
(cropped === "right" && direction === "right"),
});
};
const translateStatus = (status: string): string => {
switch (status) {
case "success":
return "Suksess";
case "warning":
return "Advarsel";
case "danger":
return "Fare";
case "info":
return "Info";
case "neutral":
return "Nøytral";
default:
return status;
}
};
export const ariaLabel = (
startDate: Date,
endDate: Date,
status: string,
statusLabel?: string
): string => {
const start = format(startDate, "dd.MM.yyyy");
const end = format(endDate, "dd.MM.yyyy");
return `${
statusLabel ? statusLabel : translateStatus(status)
} fra ${start} til ${end}`;
};
@@ -0,0 +1,11 @@
import { differenceInMilliseconds } from "date-fns";
import { Period, PositionedPeriod } from "./types.internal";
export const lastDate = (a: Date, b: Date): number =>
differenceInMilliseconds(a, b);
export const lastPeriod = (a: PositionedPeriod, b: PositionedPeriod): number =>
a.horizontalPosition - b.horizontalPosition;
export const lastSinglePeriod = (a: Period, b: Period): number =>
differenceInMilliseconds(b.endInclusive, a.endInclusive);
@@ -0,0 +1,83 @@
import { omit } from "@navikt/ds-react";
import React, { ReactNode } from "react";
import { Period } from "./types.external";
type ParsedChild = {
label?: string;
icon?: React.ReactNode;
headingTag: string;
periods: Omit<Period, "id" | "endInclusive">[];
restProps: any;
ref: any;
};
export const parseRows = (rowChildren: ReactNode[]) => {
let parsedChildren: ParsedChild[] = [];
rowChildren?.forEach((r: React.ReactNode) => {
let periods = [];
if (React.isValidElement(r) && r?.props?.children) {
if (Array.isArray(r.props.children)) {
for (let i = 0; i < r.props.children.length; i++) {
const p = r.props.children[i];
periods.push({
start: p?.props?.start,
end: p?.props?.end,
status: p?.props?.status || "neutral",
onSelectPeriod: p.props?.onSelectPeriod,
label: r.props.label,
icon: p.props.icon,
children: p.props.children,
isActive: p.props.isActive,
statusLabel: p.props.statusLabel,
restProps: omit(p.props, [
"start",
"end",
"status",
"onSelectPeriod",
"label",
"icon",
"children",
"isActive",
"statusLabel",
]),
ref: p?.ref,
});
}
} else {
periods.push({
start: r.props.children.props.start,
end: r.props.children.props.end,
status: r.props.children.props?.status || "neutral",
onSelectPeriod: r.props.children.props?.onSelectPeriod,
label: r.props.label,
icon: r.props.children.props?.icon,
children: r.props.children.props?.children,
statusLabel: r.props.children.props?.statusLabel,
restProps: omit(r.props.children.props, [
"start",
"end",
"status",
"onSelectPeriod",
"label",
"icon",
"children",
"isActive",
"statusLabel",
]),
ref: r.props?.children?.ref,
});
}
parsedChildren.push({
label: r.props.label,
icon: r.props.icon,
headingTag: r.props.headingTag,
periods: periods,
restProps: omit(r.props, ["label", "icon", "headingTag"]),
ref: (r as any)?.ref,
});
}
});
return parsedChildren;
};
@@ -0,0 +1,65 @@
import React from "react";
export type PeriodStatus =
| "success"
| "warning"
| "danger"
| "info"
| "neutral";
export type Percentage = number;
export interface Positioned {
horizontalPosition: number;
direction: "left" | "right";
}
export interface Period {
id: string;
label?: string;
start: Date;
endInclusive: Date;
status?: PeriodStatus;
onSelectPeriod?: () => void;
icon?: React.ReactNode;
children?: React.ReactNode;
end: Date;
isActive?: boolean;
statusLabel?: string;
}
export interface PositionedPeriod extends Period, Positioned {
width: number;
active?: boolean;
cropped?: "left" | "right" | "both";
disabled?: boolean;
className?: string;
hoverLabel?: React.ReactNode;
infoPin?: boolean;
start: Date;
}
export interface Spatial {
width: number;
}
export interface AxisLabel extends Positioned, Spatial {
label: string;
date: Date;
}
export interface InternalSimpleTimeline {
id: string;
periods: PositionedPeriod[];
}
export interface SimplePeriod {
/**
* Startdato for perioden, mao. periodens høyre kant.
*/
start: Date;
/**
* Sluttdato for perioden, mao. periodens venstre kant.
*/
end: Date;
}
@@ -0,0 +1,76 @@
import React from "react";
export type TimelineComponentTypes = "row" | "pin" | "period";
export type PeriodStatus =
| "success"
| "warning"
| "danger"
| "info"
| "neutral";
export type Percentage = number;
export interface Positioned {
horizontalPosition: number;
direction: "left" | "right";
}
export interface Period {
id: string;
start: Date;
endInclusive: Date;
status?: PeriodStatus;
onSelectPeriod?: () => void;
icon?: React.ReactNode;
children?: React.ReactNode;
isActive?: boolean;
}
export interface PositionedPeriod extends Period, Positioned {
width: number;
active?: boolean;
cropped?: "left" | "right" | "both";
disabled?: boolean;
className?: string;
hoverLabel?: React.ReactNode;
infoPin?: boolean;
end: Date;
statusLabel?: string;
restProps?: any;
ref?: any;
}
export interface Spatial {
width: number;
}
export interface AxisLabel extends Positioned, Spatial {
label: string;
date: Date;
}
export interface InternalSimpleTimeline {
label: string;
id: string;
periods: PositionedPeriod[];
icon?: React.ReactNode;
headingTag: "h2" | "h3" | "h4" | "h5" | "h6";
restProps: any;
ref: any;
}
export interface SelectedPeriod {
/**
* Start of the period.
*/
start: Date;
/**
* End of the period.
*/
end: Date;
/**
* Period id
*/
id: string;
}
@@ -0,0 +1,82 @@
import { Detail } from "@navikt/ds-react";
import {
addMonths,
addYears,
format,
isSameDay,
subMonths,
subYears,
} from "date-fns";
import React, { forwardRef } from "react";
import { useTimelineContext } from "../hooks/useTimelineContext";
export interface TimelineZoomButtonProps {
/**
* e.g 3mnd, 6mnd etc
*/
label: string;
/**
* Zoom interval in months or years
*/
interval: "month" | "year";
/**
* How many units of the interval that will be applied
* e.g interval="month" + count={3} equals 3 months
*/
count: number;
}
export type ZoomButtonType = React.ForwardRefExoticComponent<
TimelineZoomButtonProps & React.RefAttributes<HTMLButtonElement>
>;
export const ZoomButton: ZoomButtonType = forwardRef(
({ label, interval, count, ...rest }, ref) => {
const { setStart, endDate, startDate, direction } = useTimelineContext();
let startOfRange: Date;
if (interval === "month") {
startOfRange =
direction === "left"
? subMonths(endDate, count)
: addMonths(startDate, count);
} else {
startOfRange =
direction === "left"
? subYears(endDate, count)
: addYears(startDate, count);
}
const currentZoom =
direction === "left"
? isSameDay(startDate, startOfRange)
: isSameDay(endDate, startOfRange);
return (
<Detail as="li">
<button
aria-label={
!currentZoom
? `Zoom tidslinjen ${format(
startOfRange,
"dd.MM.yyyy"
)} til ${format(endDate, "dd.MM.yyyy")}`
: "Tilbakestill til initiell tidsperspektiv"
}
ref={ref}
{...rest}
className="navdsi-timeline__zoom-button"
onClick={() => {
setStart(startOfRange);
}}
aria-pressed={currentZoom}
>
{label}
</button>
</Detail>
);
}
);
export default ZoomButton;
@@ -0,0 +1,34 @@
import cl from "clsx";
import React, { forwardRef } from "react";
import ZoomButton, { ZoomButtonType } from "./ZoomButton";
interface ZoomProps extends React.HTMLAttributes<HTMLUListElement> {
children: React.ReactNode;
}
export interface ZoomType<Props = ZoomProps>
extends React.ForwardRefExoticComponent<
Props & React.RefAttributes<HTMLUListElement>
> {
Button: ZoomButtonType;
componentType: string;
}
export const Zoom = forwardRef<HTMLUListElement, ZoomProps>(
({ className, children, ...rest }, ref) => {
return (
<ul
ref={ref}
className={cl(className, "navdsi-timeline__zoom")}
{...rest}
>
{children}
</ul>
);
}
) as ZoomType;
Zoom.Button = ZoomButton;
Zoom.componentType = "zoom";
export default Zoom;
+8 -8
View File
@@ -15,14 +15,14 @@
"website"
],
"dependencies": {
"@navikt/ds-codemod": "^2.0.12",
"@navikt/ds-css": "^2.0.12",
"@navikt/ds-css-internal": "^2.0.12",
"@navikt/ds-icons": "^2.0.12",
"@navikt/ds-react": "^2.0.12",
"@navikt/ds-react-internal": "^2.0.12",
"@navikt/ds-tailwind": "^2.0.12",
"@navikt/ds-tokens": "^2.0.12"
"@navikt/ds-codemod": "^2.1.0",
"@navikt/ds-css": "^2.1.0",
"@navikt/ds-css-internal": "^2.1.0",
"@navikt/ds-icons": "^2.1.0",
"@navikt/ds-react": "^2.1.0",
"@navikt/ds-react-internal": "^2.1.0",
"@navikt/ds-tailwind": "^2.1.0",
"@navikt/ds-tokens": "^2.1.0"
},
"devDependencies": {
"prettier-plugin-tailwindcss": "^0.1.13"
Binary file not shown.
+44 -48
View File
@@ -1,57 +1,53 @@
import { useEffect } from "react";
import config from "config:sanity";
const BUNDLE_CHECK_INTERVAL = 60 * 5000;
const BUNDLE_CHECK_INTERVAL_DECLINED = 60 * 20000;
const CHANGES_AVAILABLE_MESSAGE =
"Studioet er har nye endringer! For å ta i bruk disse vil siden nå oppdateres for den nyeste versjonen.";
async function getCurrentHash() {
const basePath = (config.project && config.project.basePath) || "/";
const html = await window.fetch(basePath).then((res) => res.text());
const [, hash] = html.match(/app\.bundle\.js\?(\w+)/) || [];
return hash;
}
let hash = null;
let interval = null;
import { Send } from "@navikt/ds-icons";
import { BodyLong, Button, Heading, Modal } from "@navikt/ds-react";
import React, { useRef, useState } from "react";
const BundleChecker = () => {
useEffect(() => {
getCurrentHash().then((newHash) => {
hash = newHash;
});
const [inc, setInc] = useState(0);
const interval = useRef(null);
interval = createInterval();
const start = () => {
interval.current = setInterval(() => {
setInc((x) => x + 0.4);
}, 10);
};
if (inc > 99) {
clearInterval(interval.current);
window.location.replace("https://aksel.nav.no/admin");
}
return () => clearInterval(interval);
document.body.style.opacity = `${100 - inc}%`;
document.body.style.scale = `${1 - inc / 150}`;
document.body.style.rotate = `${inc ** 1.3}deg`;
/*
TODO: @release uncomment for redirecting to new studio
*/
/* window.location.replace(`http://aksel.nav.no/admin`); */
}, []);
return (
<Modal
aria-labelledby="modal-heading"
closeButton={false}
open
onClose={() => null}
>
<Modal.Content style={{ minWidth: "20rem", maxWidth: "40rem" }}>
<Heading spacing id="modal-heading" size="medium" level="1">
Studioet er flyttet!
</Heading>
// We're a react component, in theory, so return null to not render anything
return null;
<BodyLong spacing>
Vi har oppdatert studioet og flyttet det til ny url:
aksel.nav.no/admin. Hvis noe ikke fungerer, ta kontakt på slack
#aksel-redaksjonen! Husk å logge inn med samme login-provider. For de
fleste vil dette være NAV SSO.
</BodyLong>
<Button
onClick={() => start()}
icon={<Send aria-hidden />}
iconPosition="right"
>
Gå til nytt studio
</Button>
</Modal.Content>
</Modal>
);
};
export default BundleChecker;
const createInterval = (declined?: boolean) =>
setInterval(
async () => {
const newHash = await getCurrentHash();
if (hash && newHash !== hash) {
clearInterval(interval);
if (window.confirm(CHANGES_AVAILABLE_MESSAGE)) {
window.location.reload();
} else {
interval = createInterval(true);
}
}
},
declined ? BUNDLE_CHECK_INTERVAL_DECLINED : BUNDLE_CHECK_INTERVAL
);
@@ -119,7 +119,6 @@ const adjektiv = [
const client = sanityClient.withConfig({ apiVersion: "2020-06-19" });
const generateName = async () => {
console.log("ran");
const names = await client.fetch(`*[_type == "editor"].anon_navn.current`);
let c = 0;
@@ -131,7 +130,6 @@ const generateName = async () => {
c += 1;
}
console.log(res);
return res;
};
@@ -54,15 +54,18 @@ const AkselArtikkelTemplate = ({
: `Relevante artikler`}
</Heading>
<div className="card-grid-3-1 mt-6 px-4">
{data.relevante_artikler.map((x: any) => (
<ArtikkelCard {...x} key={x._id} />
))}
{data.relevante_artikler.map((x: any) =>
x && x?._id ? <ArtikkelCard {...x} key={x._id} /> : null
)}
</div>
</div>
</div>
</aside>
);
const filteredTema =
hasTema && data?.tema?.filter((x: any) => x?.title && x?.slug);
return (
<>
<Head>
@@ -136,7 +139,7 @@ const AkselArtikkelTemplate = ({
</div>
{hasTema && (
<div className="mt-8 flex flex-wrap gap-2">
{data.tema.map(({ title, slug }: any) => (
{filteredTema.map(({ title, slug }: any) => (
<span key={title}>
<NextLink
key={title}
@@ -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
@@ -11,7 +11,12 @@ interface AkselTemaT extends SanityT.Schema.aksel_tema {
}
const GodPraksisCard = ({ node }: { node: AkselTemaT }) => {
if (!node?.pictogram || !node?.slug?.current || !node?.title) {
if (
!node?.pictogram ||
!node?.slug?.current ||
!node?.title ||
!node?.refCount
) {
return null;
}
@@ -43,7 +48,7 @@ const GodPraksisCard = ({ node }: { node: AkselTemaT }) => {
</NextLink>
<BodyShort className={cl("mb-2 lg:mb-6")}>{node.oppsummering}</BodyShort>
<BodyShort size="small" className="text-text-subtle">
{node.refCount} ARTIKLER
{node.refCount === 1 ? "1 ARTIKKEL" : `${node.refCount} ARTIKLER`}
</BodyShort>
</li>
);
@@ -4,6 +4,7 @@ import copy from "copy-to-clipboard";
import React from "react";
import cl from "classnames";
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"
/>
)}
@@ -174,9 +174,6 @@ export function TableOfContentsv2({
"text-text-subtle": link.id !== activeId || activeSubId,
}
)}
aria-expanded={
link?.lvl3?.length > 0 && link?.id === activeId
}
>
{link.heading}
</Link>
@@ -7,6 +7,8 @@ export const ScrollTop = () => {
window.scrollTo(0, 0);
}}
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"
@@ -220,6 +220,7 @@ const Feedback = ({
x === HelpfulArticleEnum.MISC ? null : HelpfulArticleEnum.MISC
)
}
id="feedback-forbedringer-button"
>
<Label as="span">Foreslå forbedring</Label>
</Button>
@@ -63,7 +63,10 @@ export const usePageView = (router: Router, pageProps: any) => {
const data = {};
try {
if (pageProps?.page && pageProps.page?._type === "aksel_artikkel") {
data["tema"] = pageProps.page.tema.map((x) => x?.slug?.current);
const tema = pageProps.page.tema.map((x) => x?.slug?.current) ?? null;
if (tema && tema.length > 0) {
data["tema"] = tema;
}
}
} catch (error) {
isDevelopment && console.error(error);
+2
View File
@@ -10,6 +10,7 @@ test.describe("Axe a11y", () => {
const accessibilityScanResults = await new AxeBuilder({ page })
.disableRules(["definition-list", "scrollable-region-focusable"])
.exclude("iframe")
.exclude("#scroll-top-button")
.analyze();
expect(accessibilityScanResults.violations).toEqual([]);
});
@@ -20,4 +21,5 @@ test.describe("Axe a11y", () => {
Disabled rules:
- definition-list: 'div' as a direct child of 'dl' should be valid. Ignoring failed test.
- scrollable-region-focusable: Up for discussion. Should code-block be focusable fot easier access and allowing scroll with keyboard?
- Excluded "Til toppen" button to avoid error around focusable items with aria-hidden
*/
+1 -7
View File
@@ -167,7 +167,7 @@ export const akselTema = `*[_type == "godpraksis_landingsside"][0]{
${deRefs}
}
},
"temaer": *[_type == "aksel_tema"]{
"temaer": *[_type == "aksel_tema" && defined(seksjoner[].sider[])]{
...,
"refCount": count(*[_type == "aksel_artikkel" && !(_id in path("drafts.**")) && references(^._id)])
},
@@ -233,8 +233,6 @@ export const akselForsideQuery = `*[_type == "aksel_forside"][0]{
}
}`;
export const akselDocumentsByType = `*[_type in $types]{ _type, _id, 'slug': slug.current }`;
export const akselPrinsippBySlug = `{
"prinsipp": *[slug.current == $slug] | order(_updatedAt desc)[0]
{
@@ -282,8 +280,6 @@ export const akselEditorById = `*[_id == $id][0]
${contributorsAll}
}`;
export const dsDocuments = `*[_type in ["komponent_artikkel", "ds_artikkel"]]{ ..., 'slug': slug.current }`;
const sidebarQuery = `"sidebar": *[_type == $type && defined(kategori)] {
heading,
"slug": slug.current,
@@ -345,8 +341,6 @@ export const grunnleggendeQuery = `{
${sidebarQuery}
}`;
export const akselTemaNames = `*[_type == "aksel_tema" && count(*[references(^._id)]) > 0].slug`;
export const akselTemaDocs = `{
"tema": *[_type == "aksel_tema" && slug.current == $slug] | order(_updatedAt desc)[0]{
...,
+78 -47
View File
@@ -1,6 +1,4 @@
import imageUrlBuilder from "@sanity/image-url";
import { SanityT } from "..";
import { akselDocumentsByType, akselTemaNames, dsDocuments } from "./queries";
import { getClient, noCdnClient, sanityClient } from "./sanity.server";
const imageBuilder = imageUrlBuilder(sanityClient);
@@ -10,23 +8,40 @@ export function urlFor(source: any) {
}
export const getAllPages = async (token?: string) => {
const komponenter = await getDocumentsTmp("komponent_artikkel", token);
const grunnleggende = await getDocumentsTmp("ds_artikkel", token);
const artikler = await getAkselDocuments("all", token);
const temaer = await getAkselTema(token);
return [
"",
"god-praksis",
"produktbloggen",
...komponenter,
...grunnleggende,
...artikler,
...temaer.map((x) => `god-praksis/${x}`),
];
const pages = await sitemapPages(token);
return pages.map((x) => x.path);
};
export async function sitemapPages(
token?: string
): Promise<{ path: string; lastmod: string }[]> {
const client = token ? noCdnClient(token) : getClient();
const artikler = await getDocuments("all");
const temaer = await getAkselTema();
const pages = await client.fetch(
`{
"frontpage": *[_type == "aksel_forside"][0]._updatedAt,
"godpraksis": *[_type == "godpraksis_landingsside"][0]._updatedAt,
"grunnleggende": *[_type == "grunnleggende_landingsside"][0]._updatedAt,
"komponenter": *[_type == "komponenter_landingsside"][0]._updatedAt,
"blogg": *[_type == "blogg_landingsside"][0]._updatedAt,
}`
);
return [
{ path: "", lastmod: pages.frontpage._updatedAt },
{ path: "god-praksis", lastmod: pages.godpraksis._updatedAt },
{ path: "grunnleggende", lastmod: pages.grunnleggende._updatedAt },
{ path: "komponenter", lastmod: pages.komponenter._updatedAt },
{ path: "produktbloggen", lastmod: pages.blogg._updatedAt },
...temaer.map((x) => ({
path: `god-praksis/${x.path}`,
lastmod: x.lastmod,
})),
...artikler.map((x) => ({ ...x, path: x.slug })),
];
}
export const getAkselDocuments = async (
source:
| "aksel_artikkel"
@@ -38,38 +53,60 @@ export const getAkselDocuments = async (
token?: string
): Promise<string[]> => {
if (!source) return [];
const client = token ? noCdnClient(token) : getClient();
const documents: any[] | null = await client.fetch(akselDocumentsByType, {
types:
source === "all"
? [
"aksel_artikkel",
"aksel_blogg",
"aksel_prinsipp",
"aksel_standalone",
]
: [source],
});
const paths = [];
const docs = await getDocuments(source, token);
const nonDrafts = documents.filter((x) => !x._id.startsWith("drafts."));
nonDrafts?.forEach((page) => {
page.slug && paths.push(page.slug);
});
return paths;
return docs.map((x) => x.slug);
};
export const getDocumentsTmp = async (
source: "komponent_artikkel" | "ds_artikkel",
token?: string
): Promise<string[]> => {
const docs = await getDocuments(source, token);
return docs.map((x) => x.slug);
};
export async function getAkselTema(
token?: string
): Promise<{ path: string; lastmod: string }[]> {
const client = token ? noCdnClient(token) : getClient();
const tags: { slug: { current: string }; _updatedAt: string }[] =
await client.fetch(
`*[_type == "aksel_tema" && count(*[references(^._id)]) > 0]{slug,_updatedAt}`
);
return tags.map((x) => ({
path: x?.slug.current,
lastmod: x?._updatedAt,
}));
}
async function getDocuments(
source:
| "komponent_artikkel"
| "ds_artikkel"
| "aksel_artikkel"
| "aksel_blogg"
| "aksel_prinsipp"
| "aksel_standalone"
| "all",
token?: string
): Promise<{ slug: string; lastmod: string }[]> {
const client = token ? noCdnClient(token) : getClient();
const documents: any[] | null = await client.fetch(
`*[_type in $types]{ _type, _id, 'slug': slug.current }`,
`*[_type in $types]{ _type, _id, 'slug': slug.current, _updatedAt }`,
{
types: [source],
types:
source === "all"
? [
"komponent_artikkel",
"ds_artikkel",
"aksel_artikkel",
"aksel_blogg",
"aksel_prinsipp",
"aksel_standalone",
]
: [source],
}
);
const paths = [];
@@ -77,14 +114,8 @@ export const getDocumentsTmp = async (
documents
.filter((x) => !x._id.startsWith("drafts."))
?.forEach((page) => {
page.slug && paths.push(page.slug);
page.slug && paths.push({ slug: page.slug, lastmod: page._updatedAt });
});
return paths;
};
export const getAkselTema = async (token?: string): Promise<string[]> => {
const client = token ? noCdnClient(token) : getClient();
const tags: { current: string }[] = await client.fetch(akselTemaNames);
return tags.map((x) => x?.current);
};
}
-1
View File
@@ -52,7 +52,6 @@
"react": "^18.0.0",
"react-dom": "^18.0.0",
"react-modal": "3.15.1",
"react-snowfall": "1.2.1",
"sanity": "3.0.6",
"sanity-plugin-asset-source-unsplash": "^1.0.0",
"sanity-plugin-iframe-pane": "^2.1.1",
+5 -5
View File
@@ -1,6 +1,6 @@
import { NextApiRequest, NextApiResponse } from "next";
import { SitemapStream, streamToPromise } from "sitemap";
import { getAllPages } from "@/lib";
import { sitemapPages } from "@/lib";
/* https://linguinecode.com/post/add-robots-txt-file-sitemaps-nextjs */
export default async (req: NextApiRequest, res: NextApiResponse) => {
@@ -9,14 +9,14 @@ export default async (req: NextApiRequest, res: NextApiResponse) => {
hostname: `https://${req.headers.host}`,
});
const pages = await getAllPages();
const pages = await sitemapPages();
// Create each URL row
pages.forEach((post) => {
smStream.write({
url: `/${post}`,
changefreq: "weekly",
priority: 0.5,
url: `/${post.path}`,
changefreq: "monthly",
lastmod: post.lastmod,
});
});
@@ -0,0 +1,32 @@
import { Chat } from "@navikt/ds-react";
import { withDsExample } from "components/website-modules/examples/withDsExample";
const Example = () => {
return (
<div className="grid gap-10">
<Chat avatar="EVA" name="EVA" timestamp="01.01.21 14:00">
<Chat.Bubble>Hei! Mitt navn er Eva.</Chat.Bubble>
<Chat.Bubble>Hva kan jeg hjelpe deg med?</Chat.Bubble>
</Chat>
<Chat
avatar="NO"
name="Ola Normann"
timestamp="01.01.21 14:00"
position="right"
toptextPosition="left"
>
<Chat.Bubble>Hei Eva.</Chat.Bubble>
<Chat.Bubble>
Hvor sjekker jeg statusen på foreldrepengersøknaden min?
</Chat.Bubble>
</Chat>
</div>
);
};
export default withDsExample(Example);
export const args = {
index: 3,
desc: "Noen ganger ønsker man mer fleksibilitet for plassering av navn + tid i chatboblen.",
};
@@ -52,6 +52,6 @@ const Example = () => {
export default withDsExample(Example);
export const args = {
index: 10,
index: 11,
desc: "Ved bruk av datepicker i Modal er det viktig at 'Escape' ikke lukker selve modalen hvis datepicker er åpen. Bruk 'shouldCloseOnEsc' for å toggle dette når datepicker er åpen.",
};
@@ -0,0 +1,26 @@
import { UNSAFE_DatePicker, UNSAFE_useDatepicker } from "@navikt/ds-react";
import { withDsExample } from "components/website-modules/examples/withDsExample";
const Example = () => {
const { datepickerProps, inputProps, selectedDay } = UNSAFE_useDatepicker({
fromDate: new Date("Aug 23 2019"),
onDateChange: console.log,
allowTwoDigitYear: true,
});
return (
<div className="min-h-96">
<UNSAFE_DatePicker {...datepickerProps}>
<UNSAFE_DatePicker.Input {...inputProps} label="Velg dato" />
</UNSAFE_DatePicker>
<div className="pt-4">{selectedDay && selectedDay.toDateString()}</div>
</div>
);
};
export default withDsExample(Example);
export const args = {
index: 10,
desc: "Prop allowTwoDigitYear legger til støtte for 'yy'-format. Tilgjengelig year-range vil være fra 1943-2042 (2023).",
};
@@ -0,0 +1,140 @@
import {
Coronavirus,
Edit,
Hospital,
Money,
People,
Vacation,
} from "@navikt/ds-icons";
import { Timeline } from "@navikt/ds-react-internal";
import { withDsExample } from "components/website-modules/examples/withDsExample";
import { useState } from "react";
const Example = () => {
const [activePeriod, setActivePeriod] = useState();
return (
<div className="min-w-[800px] overflow-x-auto">
<Timeline>
<Timeline.Row label="Person" icon={<People aria-hidden />}>
{person.map((p: any, i) => {
return (
<Timeline.Period
key={i}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
onSelectPeriod={() => {
setActivePeriod(p.id);
p?.onSelectPeriod?.();
}}
isActive={activePeriod && activePeriod === p.id}
/>
);
})}
</Timeline.Row>
<Timeline.Row label="Sykehus A" icon={<Hospital aria-hidden />}>
{jobb.map((p: any, i) => {
return (
<Timeline.Period
key={i}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
onSelectPeriod={() => {
setActivePeriod(p.id);
p?.onSelectPeriod?.();
}}
isActive={activePeriod && activePeriod === p.id}
/>
);
})}
</Timeline.Row>
</Timeline>
{activePeriod && (
<div className="mt-8">{`${activePeriod}: ${
[...person, ...jobb].find((x) => x.id === activePeriod).start
}`}</div>
)}
</div>
);
};
const person = [
{
id: 1,
start: new Date("Jan 1 2022"),
end: new Date("Jan 31 2022"),
status: "warning",
icon: <Edit aria-hidden />,
statusLabel: "Sykemeldt",
},
{
id: 2,
start: new Date("Apr 1 2022"),
end: new Date("Apr 30 2022"),
status: "neutral",
icon: <Vacation aria-hidden />,
label: "test",
statusLabel: "Ferie",
},
{
id: 3,
start: new Date("Jul 1 2022"),
end: new Date("Jul 31 2022"),
status: "warning",
icon: <Edit aria-hidden />,
statusLabel: "Sykemeldt",
},
{
id: 4,
start: new Date("Aug 1 2022"),
end: new Date("Aug 30 2022"),
status: "warning",
icon: <Coronavirus aria-hidden />,
statusLabel: "Stønad korona",
},
];
const jobb = [
{
id: 5,
start: new Date("Feb 2 2022"),
end: new Date("Mar 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
},
{
id: 6,
start: new Date("Mar 2 2022"),
end: new Date("Apr 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
},
{
id: 7,
start: new Date("May 2 2022"),
end: new Date("June 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
},
{
id: 8,
start: new Date("June 2 2022"),
end: new Date("July 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
},
];
export default withDsExample(Example);
export const args = {
index: 2,
};
@@ -0,0 +1,142 @@
import {
Coronavirus,
Edit,
Hospital,
Money,
People,
Vacation,
} from "@navikt/ds-icons";
import { Timeline } from "@navikt/ds-react-internal";
import { withDsExample } from "components/website-modules/examples/withDsExample";
const Example = () => {
return (
<div className="min-w-[800px] overflow-x-auto">
<Timeline>
<Timeline.Pin date={new Date("Mar 15 2022")}>
<p>Periode: 01.03.2022 - 01.04.2022</p>
<p>Utbetalt: 12 345,00 kr</p>
<p style={{ color: "red" }}>Dager igjen: 0</p>
</Timeline.Pin>
<Timeline.Pin date={new Date("Jun 12 2022")}>
<p>Periode: 01.06.2022 - 01.07.2022</p>
<p>Utbetalt: 10 123,00 kr</p>
<p style={{ color: "red" }}>Dager igjen: 3</p>
</Timeline.Pin>
<Timeline.Pin date={new Date("Jul 28 2022")}>
<p>Periode: 01.07.2022 - 01.08.2022</p>
<p>Utbetalt: 11 245,00 kr</p>
<p style={{ color: "red" }}>Dager igjen: 1</p>
</Timeline.Pin>
<Timeline.Row label="Person" icon={<People aria-hidden />}>
{person.map((p: any, i) => {
return (
<Timeline.Period
key={i}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
<Timeline.Row label="Sykehus A" icon={<Hospital aria-hidden />}>
{jobb.map((p: any, i) => {
return (
<Timeline.Period
key={i}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
<Timeline.Row label="Sykehus B" icon={<Hospital aria-hidden />}>
{jobb.map((p: any, i) => {
return (
<Timeline.Period
key={i}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
</Timeline>
</div>
);
};
const person = [
{
start: new Date("Jan 1 2022"),
end: new Date("Jan 31 2022"),
status: "warning",
icon: <Edit aria-hidden />,
statusLabel: "Sykemeldt",
},
{
start: new Date("Apr 1 2022"),
end: new Date("Apr 30 2022"),
status: "neutral",
icon: <Vacation aria-hidden />,
label: "test",
statusLabel: "Ferie",
},
{
start: new Date("Jul 1 2022"),
end: new Date("Jul 31 2022"),
status: "warning",
icon: <Edit aria-hidden />,
statusLabel: "Sykemeldt",
},
{
start: new Date("Aug 1 2022"),
end: new Date("Aug 30 2022"),
status: "warning",
icon: <Coronavirus aria-hidden />,
statusLabel: "Stønad korona",
},
];
const jobb = [
{
start: new Date("Feb 2 2022"),
end: new Date("Mar 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
},
{
start: new Date("Mar 2 2022"),
end: new Date("Apr 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
},
{
start: new Date("May 2 2022"),
end: new Date("June 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
},
{
start: new Date("June 2 2022"),
end: new Date("July 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
},
];
export default withDsExample(Example);
export const args = {
index: 1,
};
@@ -0,0 +1,135 @@
import {
Coronavirus,
Edit,
Hospital,
Money,
People,
Vacation,
} from "@navikt/ds-icons";
import { Timeline } from "@navikt/ds-react-internal";
import { withDsExample } from "components/website-modules/examples/withDsExample";
const Example = () => {
return (
<div className="min-w-[800px] overflow-x-auto">
<Timeline>
<Timeline.Row label="Person" icon={<People aria-hidden />}>
{person.map((p: any, i) => {
return (
<Timeline.Period
key={i}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
>
{p?.children ?? null}
</Timeline.Period>
);
})}
</Timeline.Row>
<Timeline.Row label="Sykehus A" icon={<Hospital aria-hidden />}>
{jobb.map((p: any, i) => {
return (
<Timeline.Period
key={i}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
>
{p?.children ?? null}
</Timeline.Period>
);
})}
</Timeline.Row>
<Timeline.Row label="Sykehus B" icon={<Hospital aria-hidden />}>
{jobb.map((p: any, i) => {
return (
<Timeline.Period
key={i}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
</Timeline>
</div>
);
};
const person = [
{
start: new Date("Jan 1 2022"),
end: new Date("Jan 31 2022"),
status: "warning",
icon: <Edit aria-hidden />,
statusLabel: "Sykemeldt",
children: <div>50% sykemeldt</div>,
},
{
start: new Date("Apr 1 2022"),
end: new Date("Apr 30 2022"),
status: "neutral",
icon: <Vacation aria-hidden />,
label: "test",
statusLabel: "Ferie",
},
{
start: new Date("Jul 1 2022"),
end: new Date("Jul 31 2022"),
status: "warning",
icon: <Edit aria-hidden />,
statusLabel: "Sykemeldt",
},
{
start: new Date("Aug 1 2022"),
end: new Date("Aug 30 2022"),
status: "warning",
icon: <Coronavirus aria-hidden />,
statusLabel: "Stønad korona",
},
];
const jobb = [
{
start: new Date("Feb 2 2022"),
end: new Date("Mar 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
children: <div>100% utbetaling</div>,
},
{
start: new Date("Mar 2 2022"),
end: new Date("Apr 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
children: <div>100% utbetaling</div>,
},
{
start: new Date("May 2 2022"),
end: new Date("June 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
children: <div>100% utbetaling</div>,
},
{
start: new Date("June 2 2022"),
end: new Date("July 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
},
];
export default withDsExample(Example);
export const args = {
index: 0,
};
@@ -0,0 +1,134 @@
import {
Coronavirus,
Edit,
Hospital,
Money,
People,
Vacation,
} from "@navikt/ds-icons";
import { Timeline } from "@navikt/ds-react-internal";
import { withDsExample } from "components/website-modules/examples/withDsExample";
const Example = () => {
return (
<div className="min-w-[800px] overflow-x-auto">
<Timeline>
<Timeline.Row label="Person" icon={<People aria-hidden />}>
{person.map((p: any, i) => {
return (
<Timeline.Period
key={i}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
<Timeline.Row label="Sykehus A" icon={<Hospital aria-hidden />}>
{jobb.map((p: any, i) => {
return (
<Timeline.Period
key={i}
start={p.start}
end={p.end}
status={p.status}
icon={p.icon}
/>
);
})}
</Timeline.Row>
<Timeline.Row label="Sykehus B" icon={<Hospital aria-hidden />}>
{jobb.map((p: any, i) => {
return (
<Timeline.Period
key={i}
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.Button label="1.5 år" interval="year" count={1.5} />
</Timeline.Zoom>
</Timeline>
</div>
);
};
const person = [
{
start: new Date("Jan 1 2022"),
end: new Date("Jan 31 2022"),
status: "warning",
icon: <Edit aria-hidden />,
statusLabel: "Sykemeldt",
},
{
start: new Date("Apr 1 2022"),
end: new Date("Apr 30 2022"),
status: "neutral",
icon: <Vacation aria-hidden />,
label: "test",
statusLabel: "Ferie",
},
{
start: new Date("Jul 1 2022"),
end: new Date("Jul 31 2022"),
status: "warning",
icon: <Edit aria-hidden />,
statusLabel: "Sykemeldt",
},
{
start: new Date("Aug 1 2022"),
end: new Date("Aug 30 2022"),
status: "warning",
icon: <Coronavirus aria-hidden />,
statusLabel: "Stønad korona",
},
];
const jobb = [
{
start: new Date("Feb 2 2022"),
end: new Date("Mar 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
},
{
start: new Date("Mar 2 2022"),
end: new Date("Apr 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
},
{
start: new Date("May 2 2022"),
end: new Date("June 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
},
{
start: new Date("June 2 2022"),
end: new Date("July 1 2022"),
status: "success",
statusLabel: "Utbetaling",
icon: <Money aria-hidden />,
},
];
export default withDsExample(Example);
export const args = {
index: 3,
desc: "Lar deg 'zoome' til predefinerte tidsperioder. Bruk 'startDate/endDate'-prop i Timeline for å justere hvilken tidsperiode som blir vist.",
};
@@ -43,6 +43,8 @@ const Page = ({ tema: page }: PageProps): JSX.Element => {
const hasAnsvarlig = !!page?.ansvarlig?.title;
const hasPages = !!page.seksjoner.find((x) => !!x.sider);
return (
<>
<Head>
@@ -143,35 +145,45 @@ const Page = ({ tema: page }: PageProps): JSX.Element => {
</div>
<div className="relative px-4 pt-8 pb-24 md:pt-16 xl:pt-8 ">
<div className="dynamic-wrapper grid gap-20">
{page.seksjoner.map((seksjon) => (
<div key={seksjon._key}>
<Heading level="2" size="medium">
{seksjon.title}
</Heading>
{seksjon.beskrivelse && (
<div className="max-w-prose">
<SanityBlockContent
blocks={seksjon.beskrivelse}
noLastMargin
/>
{hasPages ? (
<div className="dynamic-wrapper grid gap-20">
{page.seksjoner.map((seksjon) =>
seksjon?.sider ? (
<div key={seksjon._key}>
<Heading level="2" size="medium">
{seksjon.title}
</Heading>
{seksjon.beskrivelse && (
<div className="max-w-prose">
<SanityBlockContent
blocks={seksjon.beskrivelse}
noLastMargin
/>
</div>
)}
<div className="card-grid-3-1 mt-6">
{(seksjon?.sider as unknown as ArtiklerT[])?.map(
(x: ArtiklerT) => (
<ArtikkelCard
{...x}
source={page?.slug?.current}
key={x._id}
variant="god-praksis"
/>
)
)}
</div>
</div>
)}
<div className="card-grid-3-1 mt-6">
{(seksjon.sider as unknown as ArtiklerT[]).map(
(x: ArtiklerT) => (
<ArtikkelCard
{...x}
source={page?.slug?.current}
key={x._id}
variant="god-praksis"
/>
)
)}
</div>
</div>
))}
</div>
) : null
)}
</div>
) : (
<div className="dynamic-wrapper mb-20">
<Heading level="2" size="medium">
Fant ingen artikler her enda...
</Heading>
</div>
)}
</div>
</main>
<Footer />
@@ -206,9 +218,9 @@ export default Wrapper;
export const getStaticPaths = async () => {
return {
paths: await getAkselTema().then((paths) =>
paths.map((slug) => ({
paths.map(({ path }) => ({
params: {
slug,
slug: path,
},
}))
),
+1 -11
View File
@@ -20,7 +20,6 @@ import { ToolCard } from "components/website-modules/ToolsCard";
import { PreviewSuspense } from "next-sanity/preview";
import Head from "next/head";
import Link from "next/link";
import Snowfall from "react-snowfall";
import { lazy, useEffect, useState } from "react";
import cl from "classnames";
@@ -110,14 +109,6 @@ const GetStarted = ({
</li>
))}
</ul>
<div aria-hidden>
<Snowfall
color="rgba(230, 241, 248, 0.3)"
speed={reducedMotion || pause ? [0, 0] : [0.1, 0.2]}
wind={reducedMotion || pause ? [0, 0] : [-0.2, 0.2]}
snowflakeCount={60}
/>
</div>
{!reducedMotion && (
<button
className="focus-visible:ring-border-focus-on-inverted absolute top-2 right-2 grid h-11 w-11 place-items-center rounded text-2xl focus:outline-none focus-visible:ring-2"
@@ -352,11 +343,10 @@ export const getStaticProps = async ({
komigang,
slug: "/",
preview,
id: page?._id ?? "",
},
revalidate: 60,
};
};
export default Page;
<div>lorem</div>;
+4 -3
View File
@@ -1,8 +1,6 @@
User-agent: *
Allow: /*
Sitemap: https://aksel.nav.no/api/sitemap
# Next.JS Crawl Budget Performance Updates
# Block files ending in .json, _buildManifest.js, _middlewareManifest.js, _ssgManifest.js, and any other JS files
# The asterisks allows any file name
@@ -18,4 +16,7 @@ Disallow: /*_ssgManifest.js$
Disallow: /*.js$
Disallow: /eksempler/
Disallow: /eksempler
Disallow: /admin
Sitemap: https://aksel.nav.no/api/sitemap
+3
View File
@@ -34,6 +34,9 @@ export const grunnleggendeKategorier = [
{ title: "Styling", value: "styling" },
{ title: "Guider", value: "guider" },
{ title: "Stæsj", value: "staesj" },
{ title: "Design", value: "design" },
{ title: "Kode", value: "kode" },
{ title: "Nedlastbart", value: "nedlastbart" },
];
export const monsterKategorier = [];
@@ -15,7 +15,6 @@ export function FeedbackView(props) {
const schema = useSchema();
const schemaType = schema.get("aksel_feedback");
console.log(schemaType);
const client = useClient({ apiVersion: "2021-06-07" });
const { data, error, isValidating } = useSWR(
@@ -68,17 +67,23 @@ export function FeedbackView(props) {
<li key={x._id}>
<Card flex={1}>
<IntentButton
key={x._id}
intent="edit"
mode="bleed"
mode="ghost"
padding={1}
radius={2}
radius={0}
params={{
type: "aksel_feedback",
id: getPublishedId(x._id),
}}
style={{ width: "100%" }}
>
<Preview schemaType={schemaType} value={x} key={x._id} />
<Preview
schemaType={schemaType}
value={x}
key={x._id}
layout="default"
/>
</IntentButton>
</Card>
</li>
@@ -96,12 +101,12 @@ export function FeedbackView(props) {
<li key={x._id}>
<Card flex={1}>
<IntentButton
intent=""
intent="edit"
mode="bleed"
padding={1}
radius={2}
params={{
type: schemaType,
type: "aksel_feedback",
id: getPublishedId(x._id),
}}
style={{ width: "100%" }}
-20
View File
@@ -1,20 +0,0 @@
import { InputWithCounter } from "./schema/custom-components";
export const form = {
form: {
components: {
field: (props) => {
const name = props.schemaType?.name;
if (name === "string" && props.schemaType?.options?.maxLength) {
return <InputWithCounter {...props.inputProps} />;
}
if (name === "text" && props.schemaType?.options?.maxLength) {
return <InputWithCounter {...props.inputProps} size="large" />;
}
return props.renderDefault(props);
},
},
},
};
@@ -1,156 +0,0 @@
import { codeInput } from "@sanity/code-input";
import { colorInput } from "@sanity/color-input";
import { table } from "@sanity/table";
import { visionTool } from "@sanity/vision";
import { createAuthStore, defineConfig } from "sanity";
import { unsplashImageAsset } from "sanity-plugin-asset-source-unsplash";
import { media } from "sanity-plugin-media";
import { deskTool } from "sanity/desk";
import {
defaultDocumentNode,
publicationFlow,
structure,
} from "./custom-plugins";
import { form } from "./form";
import { getTemplates } from "./util";
import { DatabaseIcon, RemoveCircleIcon } from "@sanity/icons";
import { allArticleDocuments } from "./config";
import { schema } from "./schema";
const projectId = "hnbe3yhs";
const sharedConfig = {
projectId,
apiVersion: "2021-10-21",
schema: schema,
...form,
document: {
newDocumentOptions: (prev, { currentUser }) => {
const adminOrDev = currentUser.roles.find((x) =>
["developer", "administrator", "editor"].includes(x.name)
);
if (adminOrDev) {
return [...getTemplates(currentUser.roles), ...prev];
}
return getTemplates(currentUser.roles);
},
},
plugins: [
deskTool({
title: "Desk",
structure,
defaultDocumentNode,
}),
table(),
codeInput(),
media({
projectId,
dataset: "production",
}),
visionTool(),
unsplashImageAsset(),
publicationFlow({
hasQualityControl: [
"komponent_artikkel",
"ds_artikkel",
"aksel_artikkel",
],
hasPublishedAt: allArticleDocuments,
}),
colorInput(),
],
};
export const workspaceConfig = defineConfig([
{
name: "default",
title: "Prod",
icon: DatabaseIcon,
dataset: "production",
basePath: "/admin/prod",
...sharedConfig,
auth: createAuthStore({
redirectOnSingle: false,
mode: "replace",
projectId,
dataset: "production",
providers: [
{
name: "saml",
title: "NAV SSO",
url: "https://api.sanity.io/v2021-10-01/auth/saml/login/f3270b37",
logo: "/images/navlogo.svg",
},
{
name: "github",
title: "GitHub",
url: "https://api.sanity.io/v1/auth/login/github",
},
{
name: "google",
title: "Google",
url: "https://api.sanity.io/v1/auth/login/google",
},
],
}),
},
{
name: "dev",
title: "Dev (testing only)",
icon: RemoveCircleIcon,
dataset: "development",
basePath: "/admin/dev",
...sharedConfig,
auth: createAuthStore({
redirectOnSingle: false,
mode: "replace",
projectId,
dataset: "development",
providers: [
{
name: "saml",
title: "NAV SSO",
url: "https://api.sanity.io/v2021-10-01/auth/saml/login/f3270b37",
logo: "/images/navlogo.svg",
},
{
name: "github",
title: "GitHub",
url: "https://api.sanity.io/v1/auth/login/github",
},
{
name: "google",
title: "Google",
url: "https://api.sanity.io/v1/auth/login/google",
},
],
}),
},
]);
/* interface WorkspaceOptions {
name: string
title: string
projectId: string
dataset: string
plugins?: PluginOptions[]
schema?: SchemaPluginOptions
document?: DocumentPluginOptions
tools?: Tool[] | ComposableOption<Tool[], ConfigContext>
form?: SanityFormConfig
basePath?: string
subtitle?: string
logo?: React.ComponentType
icon?: React.ComponentType
navbar?: {
components?: {
ToolMenu: React.ComponentType<ToolMenuProps>
}
}
theme?: StudioTheme
}
*/
@@ -0,0 +1,125 @@
import { InputWithCounter } from "./schema/custom-components";
import { codeInput } from "@sanity/code-input";
import { colorInput } from "@sanity/color-input";
import { table } from "@sanity/table";
import { visionTool } from "@sanity/vision";
import { createAuthStore, defineConfig } from "sanity";
import { unsplashImageAsset } from "sanity-plugin-asset-source-unsplash";
import { media } from "sanity-plugin-media";
import { deskTool } from "sanity/desk";
import {
defaultDocumentNode,
publicationFlow,
structure,
} from "./custom-plugins";
import { getTemplates } from "./util";
import { DatabaseIcon, RemoveCircleIcon } from "@sanity/icons";
import { allArticleDocuments } from "./config";
import { schema } from "./schema";
const projectId = "hnbe3yhs";
export const workspaceConfig = defineConfig([
{
...defaultConfig(),
title: "Aksel",
name: "default",
dataset: "production",
basePath: "/admin/prod",
icon: DatabaseIcon,
auth: authStore(),
},
{
...defaultConfig(),
title: "Aksel Dev-miljø",
name: "dev",
dataset: "development",
basePath: "/admin/dev",
icon: RemoveCircleIcon,
auth: authStore(),
},
]);
function defaultConfig() {
return {
projectId,
apiVersion: "2021-10-21",
schema,
form: {
components: {
field: (props) => {
const name = props.schemaType?.name;
if (name === "string" && props.schemaType?.options?.maxLength) {
return <InputWithCounter {...props.inputProps} />;
}
if (name === "text" && props.schemaType?.options?.maxLength) {
return <InputWithCounter {...props.inputProps} size="large" />;
}
return props.renderDefault(props);
},
},
},
document: {
newDocumentOptions: (prev, { currentUser }) =>
currentUser.roles.find((x) =>
["developer", "administrator", "editor"].includes(x.name)
)
? [...getTemplates(currentUser.roles), ...prev]
: getTemplates(currentUser.roles),
},
plugins: [
deskTool({
title: "Editor",
structure,
defaultDocumentNode,
}),
publicationFlow({
hasQualityControl: [
"komponent_artikkel",
"ds_artikkel",
"aksel_artikkel",
],
hasPublishedAt: allArticleDocuments,
}),
/* 3rd-party */
table(),
codeInput(),
media(),
visionTool(),
unsplashImageAsset(),
colorInput(),
],
};
}
function authStore() {
return createAuthStore({
redirectOnSingle: false,
mode: "replace",
projectId,
dataset: "production",
providers: [
{
name: "saml",
title: "NAV SSO",
url: "https://api.sanity.io/v2021-10-01/auth/saml/login/f3270b37",
logo: "/images/navlogo.svg",
},
{
name: "github",
title: "GitHub",
url: "https://api.sanity.io/v1/auth/login/github",
},
{
name: "google",
title: "Google",
url: "https://api.sanity.io/v1/auth/login/google",
},
],
});
}
Loaded 100 of 108 files, more files were not shown because too many files have changed in this diff. Show more