Compare commits

...
Author SHA1 Message Date
github-actions[bot]andgithub-actions[bot] 2364e689ed Version Packages (#2831)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-09 09:12:06 +02:00
Halvor Haugan d57c0b2c2c Justering av eksempler på Checkbox (#2833) 2024-04-09 09:06:58 +02:00
Ken 868bb543a5 🐛 Preview brakk for god-praksis
Siden 'userState' ikke hentes fra Sanity, blir den undefined ved lasting av previewdata
2024-04-09 07:33:16 +02:00
Torkjel SkjetneandHalvor Haugan 8f9ed7a404 FileUpload.Item: 💥 Endret API og støtte for å sette ID på knappen (#2824)
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2024-04-08 09:16:53 +02:00
Halvor HauganandKen 5ea6255f6d Justeringer av eksempler på Radio og Checkbox med mer (#2830)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2024-04-04 20:54:12 +02:00
github-actions[bot]andgithub-actions[bot] 491fccbe80 Version Packages (#2828)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-03 11:56:35 +02:00
Ken cca2a8263f 💄 Fjernet Illustrasjon fra bloggsider (#2827) 2024-04-03 11:50:12 +02:00
Ken 3140ec2765 🔥 Fjernet role=gridcell fra datoknapper (#2826)
Siden knappene allerede er i en table og da også td er det unødvendig med gricell
2024-04-03 11:37:05 +02:00
Halvor Haugan d8fce83ae3 Oppgrader til Storybook 8 (#2825) 2024-04-03 10:03:44 +02:00
github-actions[bot]andgithub-actions[bot] f88b00daf6 Version Packages (#2821)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-03 09:45:19 +02:00
Ken 6d0baea5c1 👽 Oppdatert om Aksel lenke i footer
Side er flyttet fra god-praksis til standalone-side url
Er opprettet automatisk redirect slik at eldre lenker ikke brekker
2024-04-03 09:25:54 +02:00
Ken 880e77b9c2 ♻️ Fjernet max-krav for antall tegn i Relaterte lenker-module
Siden lenketeksten ofte refererer til en sidetittel skaper det utfordringer da de ofte er litt lengre
2024-04-03 09:03:57 +02:00
Ken 6d16373f85 👽 Oppdatert skriv for aksel lenke i footer
Side er flyttet fra god-praksis til standalone-side url
Er opprettet automatisk redirect slik at eldre lenker ikke brekker
2024-04-03 08:42:08 +02:00
Halvor Haugan ae078254dc Modal: Fjern tomt element (#2819) 2024-04-02 10:20:00 +02:00
github-actions[bot]andgithub-actions[bot] 638e8260b4 Version Packages (#2811)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2024-04-02 08:45:10 +02:00
Halvor Haugan d991011690 FileUpload.Dropzone: id prop settes på button (#2817) 2024-03-22 11:03:05 +01:00
KenandHalvor Haugan b6086f7b1f 📝 Oppdatert radio-error dokumentasjon (#2800)
* 📝 Oppdatert radio-error dokumentasjon

* Update aksel.nav.no/website/pages/eksempler/radio/error.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update aksel.nav.no/website/pages/eksempler/radio/error.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

* Update aksel.nav.no/website/pages/eksempler/radio/error.tsx

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>

---------

Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2024-03-22 10:42:38 +01:00
Ken 05462081aa useControllableState i Expansioncard og Switch state-bug (#2810)
* ♻️ Bruker controllableState i ExpansionCard

* 🐛 Fikset tilfelle der checked switch var visuelt unchecked

* 📝 Changeset

* 🔥 Fjernet utkommentert kode
2024-03-22 10:24:35 +01:00
Ken 63e42de3ee 🆙 Oppdatert Playroom (#2815) 2024-03-22 09:31:00 +01:00
Halvor Haugan 2e85178166 yarn.lock sync 2024-03-21 16:36:11 +01:00
Ken 456d4e2e07 🐛 Slack var ikke i outbound accessPolicy for feedback 2024-03-21 11:52:48 +01:00
Ken b5a0cb8cdf Oppdatert logging for Aksel.nav.no + bumpet feedback pagination page-size (#2812)
* 🆙 Oppgradert logging

* 🎨 Bedre loggeoutput

* 🎨 Bruker childlogger

* 🧪 Custome error-logg

* ⚡ Større paginering-page for slackusers

* 📝 error -> info
2024-03-21 11:36:43 +01:00
Ken a2af9bcc37 Lagt til mer padding-right på ReadMore (#2806)
* 💄 Oppdatert padding-right på readmore

* 📝 Changeset

* 🐛 Manglet chromatic-disable på readmore story
2024-03-21 10:16:14 +01:00
bd45754adb [aksel.nav.no] Slackbot for feedback (#2785)
* 🎉 Satt opp boilerplate for auth på Aksel

* 🎉 Auth boilerplate

* 🎨 Demo-setup for å teste auth i dev

* 🚚 Flyttet auth til getserversideprops

* 🔥 Fjernet authprovider fra _app

* 🐛 revalidate er ikke valid key for getserversideprops i nextjs

* 🚚 rename auth-dir

* 🔥 Fjernet auth types

* 🎉 Auth for api

* 🎨 Oppdatert workflows med token

* 🔥 Fjernet testing av auth på god-praksis sider

* 🎨 La til sidecar på prod-deploy

* 🐛 only return boolean in Promise<boolean>

* ✨ feedback form

* 🎨 showcase all states of new FeedBackForm

* 🔥 remove prop name (will use auth api)

* 🎨 use getAuthUser()

* 🎨 use useAuth() for login/logout

* 🎉 Slack feedback-api

* 🐛 handle case where token is null

* 🎨 scroll to anchor on login redirect

* ⚡ Utility-funksjoner for slack-api

* 🧪 Tests for slack-utilities

* 🎨 Ryddet opp i henting av slack memebers

* 🚚 Flyttet caching inn i funkson

* 📱 make FeedbackForm responsive

* 🎉 Strengere typet slack-utilities

* 🎨 Fikset typefeil i feedback-api

* ⬆️ Migrert til @navikt/oasis

* 🏷️ Satt typer til strict for å kjøre tester

* 🎨 Slackbot-API with protected auth and actual user

* 🏷️ strengere typer

* ⚡ Implementert paginering av slacbot

* 🎨 Lagt til slackbot-token i workflows

* 🏷️ strict tsconfig

* 🏷️ Typer for slackbot input

* 🔥 Fjernet demo-body

* 🎨 oppdatert medvirkende tekst

* ⬆️ Yarn lock

* 🎨 move state handling down to Feedback

* 🏷️ typing fixes

* 🚧 API usage

* 🏷️ fix type error

* 👷 update AccessPolicy

* 🦺 add validation to feedback form

* 💄 justify-items-stretch on TextArea needed 🤔

* ♻️ avoid prop drilling, use sanity data hook for document id

* 🔧 add auth to webpack config for storybook

* ♻️ Feedback form  (also add error state handling)

* 👷 add stories for Feedback form

* 🔊 log feedback results metadata

* ♻️ remove refs, use formData and event.currentTarget

* 🧪 retry flaky test

* ⚗️ test promises for dynamic import in test

* 🧪 flaky test config

* ⬆️ yarn lock sync

* ⬆️ yarn lock sync

* ♻️ Bruker nå egen userState-type for fortelle komponenter om bruker er logget inn eller ikke

* 🔥 remove unused CSS

* 📝 lagt til todods

* 🔥 remove anonymous feedback submission

* 🔥 remove CSS

* :goals_net: catch fetch clientside, feedback

* ✨ send feedback to private slack channel as well

notify everyone if there are 0 editors

* 💄 + ♿ aria-hiddens and better styling, Feedback

* ♻️ refactor + add more feedback (loading state + alert)

* ⚗️ test encoding uri for anchor (redirect)

* ⚗️ more testing URL redirect

* ⚗️ more testing

* ⚗️ testing

* ✅ stories for Feedback

* 🔒 sanitize user objects (then cache)

* 🏷️ type SanitizeUser in find-user

* 🐛 optionally render the message about zero editors

* ⚗️ more testing for URL redirect / fragment issue

* ⚗️ log the redirect URL

* ⚗️ more testing

* ♻️ Gjennbruker error-return for feedback-api

* 🧪 Tester alternativ løsning for scroll etter login

* 🐛 Query update slapte loop i useEffect

* 🧪 Tester alternativ løsning for scroll etter login

* 🚚 Flyttet autofocus inn i headingseksjon

* 🐛 Wonderwall fjerner & fra query

* 🐛 encodeUrl as defined in wonderwall docs

* 🧪 Reversert til anchor-løsning

* 🧪 Reversert til anchor-løsning med encoding

* 🐛 Fikset edgecase der asPath med allerede eksisterende hash brakk login-redirect

* 🔥 Cleanup etter anchor-testing

* ⬆️ yarn lock sync

* 🎨 Oppdatert til navikt channelid

* 📝 Endret på error-tekst i feedback

* Fjernet ts-expect-error

* ⬆️ yarn lock sync

* 🔥 Fjernet filterering på godkjent mail

* 📝 Oppdatert slackbot-block

---------

Co-authored-by: Julian Nymark <julian.nymark@nav.no>
Co-authored-by: Halvor Haugan <halvor.haugan@nav.no>
2024-03-20 15:48:49 +01:00
111 changed files with 4143 additions and 3777 deletions

No files matched your search

+1
View File
@@ -25,6 +25,7 @@ jobs:
- name: add tokens to .env
run: |
echo "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_WRITE_KEY=${{ secrets.SANITY_FEEDBACK_WRITE_KEY }}" >> aksel.nav.no/website/.env
echo "SANITY_PREVIEW_TOKEN=${{ secrets.SANITY_PREVIEW_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_PRIVATE_NO_DRAFTS=${{ secrets.SANITY_PRIVATE_NO_DRAFTS }}" >> aksel.nav.no/website/.env
+1
View File
@@ -59,6 +59,7 @@ jobs:
- name: add tokens to .env
run: |
echo "SLACK_BOT_TOKEN=${{ secrets.SLACK_BOT_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_WRITE_KEY=${{ secrets.SANITY_FEEDBACK_WRITE_KEY }}" >> aksel.nav.no/website/.env
echo "SANITY_PREVIEW_TOKEN=${{ secrets.SANITY_PREVIEW_TOKEN }}" >> aksel.nav.no/website/.env
echo "SANITY_PRIVATE_NO_DRAFTS=${{ secrets.SANITY_PRIVATE_NO_DRAFTS }}" >> aksel.nav.no/website/.env
+2 -2
View File
@@ -1,4 +1,3 @@
import { mergeConfig } from "vite";
import turbosnap from "vite-plugin-turbosnap";
export default {
@@ -7,7 +6,7 @@ export default {
stories: () => ["../@navikt/**/*.stories.@(js|jsx|ts|tsx|mdx)", "./*.mdx"],
addons: [
"@storybook/addon-a11y",
"@xfinx/storybook-addon-html",
"@whitespace/storybook-addon-html",
"@storybook/addon-interactions",
{
name: "@storybook/addon-storysource",
@@ -42,6 +41,7 @@ export default {
},
async viteFinal(config, { configType }) {
const { mergeConfig } = await import("vite");
return mergeConfig(config, {
plugins:
configType === "PRODUCTION"
+8
View File
@@ -1,5 +1,13 @@
# @navikt/aksel-icons
## 6.4.0
## 6.3.6
## 6.3.5
## 6.3.4
## 6.3.3
## 6.3.2
+4 -4
View File
@@ -3,12 +3,12 @@ import React from "react";
import * as AkselIcons from "@navikt/aksel-icons";
import { StarFillIcon } from "@navikt/aksel-icons";
const meta = {
const meta: Meta<typeof StarFillIcon> = {
title: "aksel-icons/Icons",
} satisfies Meta<typeof StarFillIcon>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof StarFillIcon>;
export const Icons: Story = {
render: () => {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "6.3.3",
"version": "6.4.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
+8
View File
@@ -1,5 +1,13 @@
# @navikt/aksel-stylelint
## 6.4.0
## 6.3.6
## 6.3.5
## 6.3.4
## 6.3.3
## 6.3.2
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "6.3.3",
"version": "6.4.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
"repository": {
@@ -35,8 +35,8 @@
"dev": "yarn watch:lint"
},
"devDependencies": {
"@navikt/ds-css": "^6.3.3",
"@navikt/ds-tokens": "^6.3.3",
"@navikt/ds-css": "^6.4.0",
"@navikt/ds-tokens": "^6.4.0",
"concurrently": "7.2.1",
"postcss-selector-parser": "^6.0.13",
"postcss-value-parser": "^4.2.0",
+28
View File
@@ -1,5 +1,33 @@
# @navikt/aksel
## 6.4.0
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@6.4.0
## 6.3.6
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@6.3.6
## 6.3.5
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@6.3.5
## 6.3.4
### Patch Changes
- Updated dependencies [[`a2af9bcc3`](https://github.com/navikt/aksel/commit/a2af9bcc37eb12cc7cf3a1596d57dc0421898657)]:
- @navikt/ds-css@6.3.4
## 6.3.3
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "6.3.3",
"version": "6.4.0",
"description": "Aksel command line interface. Handles css-imports, codemods and more",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -29,7 +29,7 @@
},
"homepage": "https://aksel.nav.no/grunnleggende/kode/kommandolinje",
"dependencies": {
"@navikt/ds-css": "6.3.3",
"@navikt/ds-css": "6.4.0",
"axios": "1.6.0",
"chalk": "4.1.0",
"clipboardy": "^2.3.0",
+12
View File
@@ -1,5 +1,17 @@
# @navikt/ds-css
## 6.4.0
## 6.3.6
## 6.3.5
## 6.3.4
### Patch Changes
- ReadMore: Padding-right på button er bumpet noen px ([#2806](https://github.com/navikt/aksel/pull/2806))
## 6.3.3
## 6.3.2
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "6.3.3",
"version": "6.4.0",
"description": "CSS for NAV Designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -27,7 +27,7 @@
"css:get-version": "node config/get-version.js"
},
"devDependencies": {
"@navikt/ds-tokens": "^6.3.3",
"@navikt/ds-tokens": "^6.4.0",
"cssnano": "6.0.0",
"fast-glob": "3.2.11",
"lodash": "4.17.21",
+2 -2
View File
@@ -8,12 +8,12 @@
gap: var(--a-spacing-05);
color: var(--ac-read-more-text, var(--a-text-action));
border-radius: var(--a-border-radius-small);
padding: var(--a-spacing-1) var(--a-spacing-1) var(--a-spacing-1) var(--a-spacing-05);
padding: var(--a-spacing-1) var(--a-spacing-2) var(--a-spacing-1) var(--a-spacing-05);
text-align: start;
}
.navds-read-more--small .navds-read-more__button {
padding: var(--a-spacing-05) var(--a-spacing-1) var(--a-spacing-05) var(--a-spacing-05);
padding: var(--a-spacing-05) var(--a-spacing-1-alt) var(--a-spacing-05) var(--a-spacing-05);
}
@media (forced-colors: active) {
+44
View File
@@ -1,5 +1,49 @@
# @navikt/ds-react
## 6.4.0
### Minor Changes
- FileUpload.Item: :boom: Endret API og støtte for å sette ID på knappen ([#2824](https://github.com/navikt/aksel/pull/2824))
### Patch Changes
- Updated dependencies []:
- @navikt/ds-tokens@6.4.0
- @navikt/aksel-icons@6.4.0
## 6.3.6
### Patch Changes
- DatePicker: Fjernet role gridcell fra dato-knapper. ([#2826](https://github.com/navikt/aksel/pull/2826))
- Updated dependencies []:
- @navikt/ds-tokens@6.3.6
- @navikt/aksel-icons@6.3.6
## 6.3.5
### Patch Changes
- Modal: Fjern tomt element ([#2819](https://github.com/navikt/aksel/pull/2819))
- Updated dependencies []:
- @navikt/ds-tokens@6.3.5
- @navikt/aksel-icons@6.3.5
## 6.3.4
### Patch Changes
- Switch: Fikset edgecase der checked Switch var visuelt unchecked ([#2810](https://github.com/navikt/aksel/pull/2810))
- FileUpload.Dropzone: id prop settes på button ([#2817](https://github.com/navikt/aksel/pull/2817))
- Updated dependencies []:
- @navikt/ds-tokens@6.3.4
- @navikt/aksel-icons@6.3.4
## 6.3.3
### Patch Changes
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "6.3.3",
"version": "6.4.0",
"description": "React components from the Norwegian Labour and Welfare Administration.",
"author": "Aksel, a team part of the Norwegian Labour and Welfare Administration.",
"license": "MIT",
@@ -562,8 +562,8 @@
},
"dependencies": {
"@floating-ui/react": "0.25.4",
"@navikt/aksel-icons": "^6.3.3",
"@navikt/ds-tokens": "^6.3.3",
"@navikt/aksel-icons": "^6.4.0",
"@navikt/ds-tokens": "^6.4.0",
"@radix-ui/react-tabs": "1.0.0",
"@radix-ui/react-toggle-group": "1.0.0",
"clsx": "^2.1.0",
@@ -33,6 +33,7 @@ export interface CopyButtonProps
/**
* Text shown when button is clicked.
* Only set if used with `text`-prop.
* @default "Kopiert!"
*/
activeText?: string;
/**
@@ -20,6 +20,7 @@ const DayButton = (props: DayProps) => {
name="day"
ref={buttonRef}
{...dayRender.buttonProps}
role={undefined}
aria-label={dateTime}
aria-hidden={dayRender.activeModifiers.outside}
/>
@@ -1,5 +1,6 @@
import cl from "clsx";
import React, { forwardRef, useRef, useState } from "react";
import React, { forwardRef, useRef } from "react";
import { useControllableState } from "../util/hooks";
import { OverridableComponent } from "../util/types";
import ExpansionCardContent, {
ExpansionCardContentProps,
@@ -116,24 +117,24 @@ export const ExpansionCard = forwardRef<HTMLDivElement, ExpansionCardProps>(
},
ref,
) => {
const [_open, _setOpen] = useState(defaultOpen);
const shouldFade = useRef<boolean>(!(Boolean(open) || defaultOpen));
const handleOpen = () => {
if (open === undefined) {
const newOpen = !_open;
_setOpen(newOpen);
onToggle?.(newOpen);
} else {
onToggle?.(!open);
}
shouldFade.current = true;
};
const [_open, _setOpen] = useControllableState({
value: open,
onChange: (newValue) => {
onToggle?.(newValue);
shouldFade.current = true;
},
defaultValue: defaultOpen,
});
return (
<ExpansionCardContext.Provider
value={{ open: open ?? _open, toggleOpen: handleOpen, size }}
value={{
open: open ?? _open,
toggleOpen: () => _setOpen((x) => !x),
size,
}}
>
<section
{...rest}
@@ -16,7 +16,7 @@ export const CheckboxGroupContext = createContext<CheckboxGroupState | null>(
export interface CheckboxGroupProps
extends Omit<
FieldsetProps,
"onChange" | "errorPropagation" | "defaultValue"
"onChange" | "errorPropagation" | "defaultValue" | "nativeReadOnly"
> {
/**
* Collection of `<Checkbox/>`.
@@ -27,7 +27,7 @@ export interface CheckboxGroupProps
*/
value?: any[];
/**
* Default checked checkboxes on render.
* Default checked checkboxes.
*/
defaultValue?: any[];
/**
@@ -44,10 +44,10 @@ export interface CheckboxGroupProps
*
* @example
* ```jsx
* <CheckboxGroup legend="Hvor vil du sitte?">
* <Checkbox value="Bakerst">Bakerst</Checkbox>
* <Checkbox value="Midterst">Midterst</Checkbox>
* <Checkbox value="Fremst">Fremst</Checkbox>
* <CheckboxGroup legend="Transportmiddel">
* <Checkbox value="car">Bil</Checkbox>
* <Checkbox value="taxi">Drosje</Checkbox>
* <Checkbox value="public">Kollektivt</Checkbox>
* </CheckboxGroup>
* ```
*/
@@ -14,7 +14,7 @@ export interface CheckboxProps
*/
errorId?: string;
/**
* Checkbox label
* Checkbox label.
*/
children: React.ReactNode;
/**
@@ -22,7 +22,7 @@ export interface CheckboxProps
*/
hideLabel?: boolean;
/**
* The value of the HTML element.
* Checkbox value.
*/
value?: any;
/**
@@ -31,7 +31,7 @@ export interface CheckboxProps
*/
indeterminate?: boolean;
/**
* Adds a description to extend labeling of Checkbox.
* Adds a description to extend the labeling.
*/
description?: string;
}
@@ -89,8 +89,8 @@ interface FileUploadComponent
* ```jsx
* <FileUpload.Item file={file} status="uploading" />
* <FileUpload.Item file={file} status="downloading" />
* <FileUpload.Item file={file} itemAction="retry" onRetry={...} />
* <FileUpload.Item file={file} itemAction="delete" onDelete={...} />
* <FileUpload.Item file={file} button={{ action:"retry", onClick:... }} />
* <FileUpload.Item file={file} button={{ action:"delete", onClick:... }} />
* ```
*/
Item: typeof Item;
@@ -57,51 +57,63 @@ export const States: StoryObj<typeof FileUpload.Item> = {
file={fileTxt}
error="Error og uploading"
status="uploading"
onRetry={() => onRetry(fileTxt)}
onDelete={() => onDelete(fileTxt)}
button={{
onClick: () => onDelete(fileTxt),
action: "delete",
}}
/>
<h2>Item Actions</h2>
<h3>status + delete</h3>
<FileUpload.Item
file={fileDocx}
onDelete={() => onDelete(fileDocx)}
onRetry={() => onRetry(fileDocx)}
status="uploading"
itemAction="delete"
button={{
onClick: () => onDelete(fileDocx),
action: "delete",
}}
/>
<h3>status + retry</h3>
<FileUpload.Item
file={fileDocx}
onDelete={() => onDelete(fileDocx)}
onRetry={() => onRetry(fileDocx)}
status="downloading"
itemAction="retry"
button={{
onClick: () => onRetry(fileDocx),
action: "retry",
}}
/>
<h3>retry</h3>
<FileUpload.Item
file={fileCsv}
onRetry={() => onRetry(fileCsv)}
itemAction="retry"
button={{
onClick: () => onRetry(fileCsv),
action: "retry",
}}
/>
<h3>delete</h3>
<FileUpload.Item
file={filePptx}
onDelete={() => onDelete(filePptx)}
itemAction="delete"
button={{
onClick: () => onDelete(filePptx),
action: "delete",
}}
/>
<h3>retry + error</h3>
<FileUpload.Item
file={fileCsv}
error="Error og onRetry"
onRetry={() => onRetry(fileCsv)}
itemAction="retry"
button={{
onClick: () => onRetry(fileCsv),
action: "retry",
}}
/>
<h3>delete + error</h3>
<FileUpload.Item
file={filePptx}
error="Error og onDelete"
onDelete={() => onDelete(filePptx)}
itemAction="delete"
button={{
onClick: () => onDelete(filePptx),
action: "delete",
}}
/>
</div>
),
@@ -25,6 +25,7 @@ const MAX_SIZE = MAX_SIZE_MB * 1024 * 1024;
const CustomItem = ({
index,
onDelete,
...props
}: FileUploadItemProps & {
index: number;
@@ -45,7 +46,10 @@ const CustomItem = ({
<FileUpload.Item
{...props}
status={loading ? "uploading" : "idle"}
itemAction="delete"
button={{
action: "delete",
onClick: onDelete,
}}
as="li"
/>
);
@@ -165,7 +169,10 @@ export const Single: StoryFn = () => {
key={file.file.name}
file={file.file}
error={file.error ? errors[file.reasons[0]] : undefined}
onDelete={removeFile}
button={{
action: "delete",
onClick: removeFile,
}}
/>
))}
</VStack>
@@ -194,12 +201,17 @@ export const Translation = () => (
<FileUpload.Dropzone label="Last opp bilder" onSelect={console.log} />
<FileUpload.Item
file={{ name: "eksempel.png", size: 200000 }}
onDelete={() => null}
button={{
action: "delete",
onClick: () => null,
}}
/>
<FileUpload.Item
file={{ name: "eksempel.png", size: 200000 }}
itemAction="retry"
onRetry={() => null}
button={{
action: "retry",
onClick: () => null,
}}
/>
<FileUpload.Item
file={{ name: "eksempel.png", size: 200000 }}
@@ -30,6 +30,7 @@ const Dropzone = forwardRef<HTMLInputElement, FileUploadDropzoneProps>(
disabled,
translations,
onClick,
id: buttonId,
...rest
} = props;
@@ -46,7 +47,10 @@ const Dropzone = forwardRef<HTMLInputElement, FileUploadDropzoneProps>(
const _disabled = disabled ?? fileLimitReached;
const { inputProps, errorId, showErrorMsg, hasError, inputDescriptionId } =
useFormField({ ...props, disabled: _disabled }, "fileUpload");
useFormField(
{ ...omit(props, ["id"]), disabled: _disabled },
"fileUpload",
);
const {
id: inputId,
"aria-describedby": ariaDescribedby,
@@ -129,8 +133,9 @@ const Dropzone = forwardRef<HTMLInputElement, FileUploadDropzoneProps>(
<BodyShort as="div">{translate("dropzone.or")}</BodyShort>
</div>
<Button
{...omit(rest, ["errorId", "id"])}
{...omit(rest, ["errorId"])}
{...inputPropsRest}
id={buttonId}
aria-describedby={cl(labelId, ariaDescribedby)}
className="navds-dropzone__area-button"
type="button"
@@ -12,12 +12,13 @@ import ItemIcon from "./ItemIcon";
import ItemName from "./ItemName";
import { formatFileSize } from "./utils/format-file-size";
export interface FileItemBaseProps {
export interface FileUploadItemProps
extends React.HTMLAttributes<HTMLDivElement> {
/**
* Overrides html-tag
* @default "div"
*/
as?: "div" | "li";
as?: ("div" | "li") & React.ElementType;
/**
* Either a native File or file metadata.
*/
@@ -43,37 +44,20 @@ export interface FileItemBaseProps {
* @default "idle"
*/
status?: "downloading" | "uploading" | "idle";
/**
* Props for the action button.
*/
button?: {
action: "delete" | "retry";
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
id?: string;
};
/**
* i18n-API for customizing texts and labels
*/
translations?: ComponentTranslation<"FileUpload">["item"];
onRetry?: (event: MouseEvent<HTMLButtonElement>) => void;
onDelete?: (event: MouseEvent<HTMLButtonElement>) => void;
}
type FileItemActionDelete = {
onDelete: (event: MouseEvent<HTMLButtonElement>) => void;
itemAction: "delete";
};
type FileItemActionRetry = {
onRetry: (event: MouseEvent<HTMLButtonElement>) => void;
itemAction: "retry";
};
type FileItemActionNone = {
itemAction?: "none";
};
type FileItemConditionalProps =
| FileItemActionDelete
| FileItemActionRetry
| FileItemActionNone;
export type FileUploadItemProps = FileItemBaseProps &
FileItemConditionalProps &
React.HTMLAttributes<HTMLDivElement>;
export const Item: OverridableComponent<FileUploadItemProps, HTMLDivElement> =
forwardRef(
(
@@ -81,16 +65,14 @@ export const Item: OverridableComponent<FileUploadItemProps, HTMLDivElement> =
as: Component = "div",
file,
status = "idle",
onDelete,
onRetry,
error,
className,
href,
onFileClick,
itemAction = "delete",
button,
translations,
...rest
},
}: FileUploadItemProps,
ref,
) => {
const context = useFileUploadTranslation(false);
@@ -148,14 +130,14 @@ export const Item: OverridableComponent<FileUploadItemProps, HTMLDivElement> =
</div>
</div>
{status === "idle" && (
{status === "idle" && button && (
<ItemButton
file={file}
onRetry={onRetry}
onDelete={onDelete}
action={itemAction}
retryTitle={translate("item.retryButtonTitle")}
deleteTitle={translate("item.deleteButtonTitle")}
{...button}
title={translate(
button.action === "retry"
? "item.retryButtonTitle"
: "item.deleteButtonTitle",
)}
/>
)}
</div>
@@ -1,52 +1,27 @@
import React from "react";
import { ArrowsCirclepathIcon, TrashIcon } from "@navikt/aksel-icons";
import { Button } from "../../../../button";
import { FileItem } from "./Item.types";
interface Props {
file: FileItem;
onRetry?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onDelete?: (event: React.MouseEvent<HTMLButtonElement>) => void;
action: "delete" | "retry" | "none";
retryTitle: string;
deleteTitle: string;
action: "delete" | "retry";
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
id?: string;
title: string;
}
const ItemButton = ({
onRetry,
onDelete,
action,
retryTitle,
deleteTitle,
}: Props) => {
if (action === "none") {
return null;
}
const ItemButton = ({ action, onClick, id, title }: Props) => {
const Icon = action === "delete" ? TrashIcon : ArrowsCirclepathIcon;
if (onRetry && action === "retry") {
return (
<Button
className="navds-file-item__button"
type="button"
variant="tertiary-neutral"
onClick={onRetry}
icon={<ArrowsCirclepathIcon title={retryTitle} />}
/>
);
}
if (onDelete && action === "delete") {
return (
<Button
className="navds-file-item__button"
type="button"
variant="tertiary-neutral"
onClick={onDelete}
icon={<TrashIcon title={deleteTitle} />}
/>
);
}
return null;
return (
<Button
id={id}
className="navds-file-item__button"
type="button"
variant="tertiary-neutral"
onClick={onClick}
icon={<Icon title={title} />}
/>
);
};
export default ItemButton;
@@ -18,7 +18,7 @@ export const RadioGroupContext =
export interface RadioGroupProps
extends Omit<
FieldsetProps,
"onChange" | "errorPropagation" | "defaultValue"
"onChange" | "errorPropagation" | "defaultValue" | "nativeReadOnly"
> {
/**
* Collection of `<Radio />`-elements
+2 -2
View File
@@ -9,11 +9,11 @@ export interface RadioProps
*/
children: React.ReactNode;
/**
* The value of the HTML element.
* Radio value.
*/
value: any;
/**
* Adds a description to extend labeling of Radio.
* Adds a description to extend the labeling.
*/
description?: string;
}
@@ -85,7 +85,7 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
...rest
} = props;
const [checked, setChecked] = useState(
const [_checked, setChecked] = useState(
defaultChecked ?? checkedProp ?? false,
);
@@ -93,6 +93,8 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
checkedProp !== undefined && setChecked(checkedProp);
}, [checkedProp]);
const checked = checkedProp ?? _checked;
return (
<div
className={cl(
+2 -3
View File
@@ -5,8 +5,7 @@ import { FieldsetContext } from "./fieldset/context";
export interface FormFieldProps {
/**
* Adds a description to extend labeling of the field.
* Error message for element.
* Error message.
*/
error?: React.ReactNode;
/**
@@ -24,7 +23,7 @@ export interface FormFieldProps {
*/
disabled?: boolean;
/**
* Adds a description to extend labeling of a field.
* Adds a description to extend the labeling.
*/
description?: React.ReactNode;
/**
@@ -41,16 +41,14 @@ export const PosterVariants = {
render: () => (
<VStack gap="6" align="start">
<GuidePanel>
If you exclude the <code>poster</code> prop, you will get the poster
variant on mobile (&lt;480px) and the non-poster variant otherwise.
If you exclude the `poster` prop, you will get the poster variant on
mobile (&lt;480px) and the non-poster variant otherwise.
</GuidePanel>
<GuidePanel poster>
Use the <code>poster</code> prop to get the poster variant on all
viewports.
Use the `poster` prop to get the poster variant on all viewports.
</GuidePanel>
<GuidePanel poster={false}>
Set <code>poster=false</code> to get the non-poster variant on all
viewports.
Set `poster=false` to get the non-poster variant on all viewports.
</GuidePanel>
</VStack>
),
+5 -1
View File
@@ -238,7 +238,11 @@ export const Modal = forwardRef<HTMLDialogElement, ModalProps>(
level="1"
id={ariaLabelId}
>
<span className="navds-modal__header-icon">{header.icon}</span>
{header.icon && (
<span className="navds-modal__header-icon">
{header.icon}
</span>
)}
{header.heading}
</Heading>
</ModalHeader>
@@ -1,65 +1,91 @@
import { Meta, StoryFn } from "@storybook/react";
import React, { useState } from "react";
import { Meta, StoryObj } from "@storybook/react";
import { fn } from "@storybook/test";
import React from "react";
import { ReadMore } from ".";
import { VStack } from "../layout/stack";
const meta: Meta<typeof ReadMore> = {
export default {
title: "ds-react/ReadMore",
component: ReadMore,
};
export default meta;
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof ReadMore>;
export const Default: StoryFn<{
controlled: boolean;
size: "medium" | "small";
defaultOpen: boolean;
}> = (props) => {
const [state, setState] = useState(false);
type Story = StoryObj<typeof ReadMore>;
return (
<ReadMore
open={props.controlled ? state : undefined}
onClick={() => setState((x) => !x)}
header="Grunnen til at vi spør om dette og i tillegg ber om vedlegg"
size={props.size}
onOpenChange={console.log}
defaultOpen={props.defaultOpen}
>
<div style={{ maxWidth: 300 }}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Mollitia,
tempore corporis exercitationem minus dignissimos eius aspernatur fugiat
iusto.
</div>
</ReadMore>
);
};
Default.args = {
controlled: false,
size: "medium",
defaultOpen: false,
};
Default.argTypes = {
size: {
options: ["medium", "small"],
control: { type: "radio" },
const Content = (
<div style={{ maxWidth: 300 }}>
Lorem ipsum dolor sit amet consectetur adipisicing elit. Mollitia, tempore
corporis exercitationem minus dignissimos eius aspernatur fugiat iusto.
</div>
);
export const Default: Story = {
args: {
size: "medium",
defaultOpen: false,
onOpenChange: fn(),
children: Content,
header: "Grunnen til at vi spør om dette og i tillegg ber om vedlegg",
},
argTypes: {
size: {
options: ["medium", "small"],
control: { type: "radio" },
},
},
};
export const Small = () => (
<ReadMore
header="Grunnen til at vi spør om dette og i tillegg ber om vedlegg"
size="small"
>
Command station, this is ST 321. Code Clearance Blue. We&apos;re starting
our approach. Deactivate the security shield.
</ReadMore>
);
export const Small: Story = {
args: {
...Default.args,
size: "small",
},
};
export const Open = () => (
<ReadMore
header="Grunnen til at vi spør om dette og i tillegg ber om vedlegg"
defaultOpen
>
Command station, this is ST 321. Code Clearance Blue. We&apos;re starting
our approach. Deactivate the security shield.
</ReadMore>
);
export const DefaultOpen: Story = {
args: {
...Default.args,
defaultOpen: true,
},
};
export const Open: Story = {
args: {
...Default.args,
open: true,
},
};
export const Chromatic: Story = {
render: () => {
return (
<VStack gap="4">
<div>
<h2>Default</h2>
{/* @ts-expect-error Args are partial, leading to required prop mismatch */}
<ReadMore {...Default.args} />
</div>
<div>
<h2>Small</h2>
{/* @ts-expect-error Args are partial, leading to required prop mismatch */}
<ReadMore {...Small.args} />
</div>
<div>
<h2>DefaultOpen</h2>
{/* @ts-expect-error Args are partial, leading to required prop mismatch */}
<ReadMore {...DefaultOpen.args} />
</div>
<div>
<h2>Controlled open</h2>
{/* @ts-expect-error Args are partial, leading to required prop mismatch */}
<ReadMore {...Open.args} />
</div>
</VStack>
);
},
parameters: {
chromatic: { disable: false },
},
};
@@ -4,18 +4,17 @@ import React from "react";
import { VStack } from "../../layout/stack";
import BodyLong from "../BodyLong";
const meta = {
const meta: Meta<typeof BodyLong> = {
title: "ds-react/Typography/BodyLong",
component: BodyLong,
decorators: [(story) => <div style={{ maxWidth: "700px" }}>{story()}</div>],
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof BodyLong>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof BodyLong>;
const lorem =
"Hvis du ikke bor sammen med begge foreldrene dine, kan du ha rett til barnebidrag fra en eller begge foreldre mens du fullfører videregående skole eller tilsvarende.";
@@ -4,18 +4,17 @@ import React from "react";
import { VStack } from "../../layout/stack";
import { BodyShort } from "../BodyShort";
const meta = {
const meta: Meta<typeof BodyShort> = {
title: "ds-react/Typography/BodyShort",
component: BodyShort,
decorators: [(story) => <div style={{ maxWidth: "250px" }}>{story()}</div>],
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof BodyShort>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof BodyShort>;
const lorem = "Du må gjøre en filtrering for å se brukere i listen.";
@@ -4,18 +4,17 @@ import React from "react";
import { VStack } from "../../layout/stack";
import Detail from "../Detail";
const meta = {
const meta: Meta<typeof Detail> = {
title: "ds-react/Typography/Detail",
component: Detail,
decorators: [(story) => <div style={{ maxWidth: "200px" }}>{story()}</div>],
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof Detail>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof Detail>;
const lorem = "Du må gjøre en filtrering for å se brukere i listen.";
@@ -4,17 +4,16 @@ import React from "react";
import { VStack } from "../../layout/stack";
import ErrorMessage from "../ErrorMessage";
const meta = {
const meta: Meta<typeof ErrorMessage> = {
title: "ds-react/Typography/ErrorMessage",
component: ErrorMessage,
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof ErrorMessage>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof ErrorMessage>;
const lorem =
"Du må fylle ut: Oppgi årsaken til at du har ventet mer enn 6 måneder med å søke om refusjon";
@@ -4,18 +4,17 @@ import { default as React } from "react";
import { VStack } from "../../layout/stack";
import Heading from "../Heading";
const meta = {
const meta: Meta<typeof Heading> = {
title: "ds-react/Typography/Heading",
component: Heading,
decorators: [(story) => <div style={{ maxWidth: "300px" }}>{story()}</div>],
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof Heading>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof Heading>;
const lorem = "Hva kan vi hjelpe deg med?";
@@ -4,18 +4,17 @@ import React from "react";
import { VStack } from "../../layout/stack";
import Label from "../Label";
const meta = {
const meta: Meta<typeof Label> = {
title: "ds-react/Typography/Label",
component: Label,
decorators: [(story) => <div style={{ maxWidth: "400px" }}>{story()}</div>],
parameters: {
chromatic: { disable: true },
},
} satisfies Meta<typeof Label>;
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof Label>;
const lorem =
"Oppgi årsaken til at du har ventet mer enn 6 måneder med å søke om refusjon";
+8
View File
@@ -1,5 +1,13 @@
# @navikt/ds-tailwind
## 6.4.0
## 6.3.6
## 6.3.5
## 6.3.4
## 6.3.3
## 6.3.2
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tailwind",
"version": "6.3.3",
"version": "6.4.0",
"description": "Tailwind config based on @navikt/ds-tokens",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -25,7 +25,7 @@
"directory": "@navikt/core/tailwind"
},
"devDependencies": {
"@navikt/ds-tokens": "^6.3.3",
"@navikt/ds-tokens": "^6.4.0",
"color": "4.2.3",
"lodash": "^4.17.21",
"tailwindcss": "^3.3.3",
+8
View File
@@ -1,5 +1,13 @@
# @navikt/ds-tokens
## 6.4.0
## 6.3.6
## 6.3.5
## 6.3.4
## 6.3.3
## 6.3.2
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-tokens",
"version": "6.3.3",
"version": "6.4.0",
"description": "Design-tokens for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
+123 -107
View File
@@ -1,30 +1,42 @@
# Changelog
## 6.4
### @navikt/ds-react
- FileUpload.Item: :boom: Endret API og støtte for å sette ID på knappen ([#2824](https://github.com/navikt/aksel/pull/2824))
## 6.3
### @navikt/ds-css
- FileUpload: Ny komponent `FileUpload` for å håndtere opplasting av filer, Dropzone og fil-visning ([#2504](https://github.com/navikt/aksel/pull/2504))
### @navikt/ds-react
- FileUpload.Dropzone: Fiks feil med duplisert id ([#2805](https://github.com/navikt/aksel/pull/2805))
- useDatePicker: `isInvalid`-key i onValidate var hadde flipper boolean i noen tilfeller ([#2809](https://github.com/navikt/aksel/pull/2809))
* FileUpload: Fiks feil ved import av json ([#2803](https://github.com/navikt/aksel/pull/2803))
- FileUpload: Disabled dropzone stopper opplasting ([#2798](https://github.com/navikt/aksel/pull/2798))
* Allow Combobox options as objects to support separate display text and value ([#2716](https://github.com/navikt/aksel/pull/2716))
- ReadMore: Padding-right på button er bumpet noen px ([#2806](https://github.com/navikt/aksel/pull/2806))
* FileUpload: Ny komponent `FileUpload` for å håndtere opplasting av filer, Dropzone og fil-visning ([#2504](https://github.com/navikt/aksel/pull/2504))
## 6.2
### @navikt/ds-react
- Select: Fjern 'multiple' fra SelectProps ([#2782](https://github.com/navikt/aksel/pull/2782))
- DatePicker: Fjernet role gridcell fra dato-knapper. ([#2826](https://github.com/navikt/aksel/pull/2826))
* Modal: Fjern tomt element ([#2819](https://github.com/navikt/aksel/pull/2819))
- Switch: Fikset edgecase der checked Switch var visuelt unchecked ([#2810](https://github.com/navikt/aksel/pull/2810))
- FileUpload.Dropzone: id prop settes på button ([#2817](https://github.com/navikt/aksel/pull/2817))
* FileUpload.Dropzone: Fiks feil med duplisert id ([#2805](https://github.com/navikt/aksel/pull/2805))
* useDatePicker: `isInvalid`-key i onValidate var hadde flipper boolean i noen tilfeller ([#2809](https://github.com/navikt/aksel/pull/2809))
- FileUpload: Fiks feil ved import av json ([#2803](https://github.com/navikt/aksel/pull/2803))
* FileUpload: Disabled dropzone stopper opplasting ([#2798](https://github.com/navikt/aksel/pull/2798))
- Allow Combobox options as objects to support separate display text and value ([#2716](https://github.com/navikt/aksel/pull/2716))
- FileUpload: Ny komponent `FileUpload` for å håndtere opplasting av filer, Dropzone og fil-visning ([#2504](https://github.com/navikt/aksel/pull/2504))
## 6.2
### @navikt/aksel-icons
@@ -32,6 +44,10 @@
- Ikoner: Nytt ikon `opensource` ([#2787](https://github.com/navikt/aksel/pull/2787))
### @navikt/ds-react
- Select: Fjern 'multiple' fra SelectProps ([#2782](https://github.com/navikt/aksel/pull/2782))
## 6.1
### @navikt/ds-css
@@ -54,9 +70,11 @@
## 6.0
### @navikt/ds-tailwind
### @navikt/aksel-icons
- Tokens: La til nytt brekkpunkt `2xl` for `1440px`. Alle primitives er oppdatert for å reflektere oppdateringen. Brukere av Tailwind vil måtte sjekke at overskriving av `screen: "2xl"` ikke brekker app. ([#2623](https://github.com/navikt/aksel/pull/2623))
- ESM: ds-react og aksel-icons støtter nå native ESM 🎉 ([#2623](https://github.com/navikt/aksel/pull/2623))
* SSR: ds-react og aksel-icons støtter nå `use client` og kan brukes i nextjs app-router uten lokal re-eksportering. ([#2623](https://github.com/navikt/aksel/pull/2623))
### @navikt/ds-css
@@ -70,6 +88,10 @@
- Textarea: Fjernet div med klassen navds-textarea\_\_wrapper ([#2623](https://github.com/navikt/aksel/pull/2623))
### @navikt/ds-tailwind
- Tokens: La til nytt brekkpunkt `2xl` for `1440px`. Alle primitives er oppdatert for å reflektere oppdateringen. Brukere av Tailwind vil måtte sjekke at overskriving av `screen: "2xl"` ikke brekker app. ([#2623](https://github.com/navikt/aksel/pull/2623))
### @navikt/ds-tokens
- Tokens: La til nytt brekkpunkt `2xl` for `1440px`. Alle primitives er oppdatert for å reflektere oppdateringen. Brukere av Tailwind vil måtte sjekke at overskriving av `screen: "2xl"` ikke brekker app. ([#2623](https://github.com/navikt/aksel/pull/2623))
@@ -98,19 +120,13 @@
* SSR: ds-react og aksel-icons støtter nå `use client` og kan brukes i nextjs app-router uten lokal re-eksportering. ([#2623](https://github.com/navikt/aksel/pull/2623))
### @navikt/aksel-stylelint
- Textarea: Fjernet div med klassen navds-textarea\_\_wrapper ([#2623](https://github.com/navikt/aksel/pull/2623))
### @navikt/aksel
- Chat: Fjernet deprecated props `backgroundColor` og `avatarBgColor`. Bruk prop `variant` som erstatning. ([#2623](https://github.com/navikt/aksel/pull/2623))
### @navikt/aksel-icons
### @navikt/aksel-stylelint
- ESM: ds-react og aksel-icons støtter nå native ESM 🎉 ([#2623](https://github.com/navikt/aksel/pull/2623))
* SSR: ds-react og aksel-icons støtter nå `use client` og kan brukes i nextjs app-router uten lokal re-eksportering. ([#2623](https://github.com/navikt/aksel/pull/2623))
- Textarea: Fjernet div med klassen navds-textarea\_\_wrapper ([#2623](https://github.com/navikt/aksel/pull/2623))
## 5.18
@@ -152,32 +168,32 @@
## 5.16
### @navikt/ds-tailwind
### @navikt/aksel-icons
- Tailwind: La til ny `maxWidth` for text ([#2652](https://github.com/navikt/aksel/pull/2652))
- Ikoner: Nytt ikon `LocationPin` ✨ ([#2672](https://github.com/navikt/aksel/pull/2672))
### @navikt/ds-css
- :sparkles: Combobox: Mulighet for å begrense hvor mange valg bruker kan ta ([#2260](https://github.com/navikt/aksel/pull/2260))
### @navikt/ds-tailwind
- Tailwind: La til ny `maxWidth` for text ([#2652](https://github.com/navikt/aksel/pull/2652))
### @navikt/ds-react
- :sparkles: Combobox: Mulighet for å begrense hvor mange valg bruker kan ta ([#2260](https://github.com/navikt/aksel/pull/2260))
### @navikt/aksel-icons
- Ikoner: Nytt ikon `LocationPin` ✨ ([#2672](https://github.com/navikt/aksel/pull/2672))
## 5.15
### @navikt/ds-react
- HelpText: HelpText-state ble ikke riktig oppdatert ved klikk ([#2643](https://github.com/navikt/aksel/pull/2643))
### @navikt/aksel-icons
- Ikoner: Synket ikoner med Figma. Lagt til nytt ikon `FigureChild` ([#2629](https://github.com/navikt/aksel/pull/2629))
### @navikt/ds-react
- HelpText: HelpText-state ble ikke riktig oppdatert ved klikk ([#2643](https://github.com/navikt/aksel/pull/2643))
## 5.14
### @navikt/ds-css
@@ -218,6 +234,10 @@
## 5.12
### @navikt/aksel-icons
- Ikoner: Oppdatert ikonpakke med diverse bugdfixes og oppdateringer av stroke ([#2538](https://github.com/navikt/aksel/pull/2538))
### @navikt/ds-css
- ExpansionCard: Divider mellom header og innhold ([#2566](https://github.com/navikt/aksel/pull/2566))
@@ -238,11 +258,11 @@
- Stylelint: Støtter nå stylelint v16 ([#2591](https://github.com/navikt/aksel/pull/2591))
## 5.11
### @navikt/aksel-icons
- Ikoner: Oppdatert ikonpakke med diverse bugdfixes og oppdateringer av stroke ([#2538](https://github.com/navikt/aksel/pull/2538))
## 5.11
- Ikoner: Nytt ikon `PushPin` ([#2503](https://github.com/navikt/aksel/pull/2503))
### @navikt/ds-css
@@ -284,16 +304,8 @@
- :wheelchair: Textarea: Forsinkelse ved live-opplesning av gjenstående tegn ([#2502](https://github.com/navikt/aksel/pull/2502))
### @navikt/aksel-icons
- Ikoner: Nytt ikon `PushPin` ([#2503](https://github.com/navikt/aksel/pull/2503))
## 5.10
### @navikt/ds-tailwind
- typescript types for tailwind preset ([#2486](https://github.com/navikt/aksel/pull/2486))
### @navikt/ds-css
- :bug: Textarea: Teller flyttet ut av tekstfeltet for å unngå overlapp og misforståelser ([#2483](https://github.com/navikt/aksel/pull/2483))
@@ -302,6 +314,10 @@
- Primitives: Ny komponent `Page` og `Page.Block` ([#2456](https://github.com/navikt/aksel/pull/2456))
### @navikt/ds-tailwind
- typescript types for tailwind preset ([#2486](https://github.com/navikt/aksel/pull/2486))
### @navikt/ds-react
- Gjør lukknapp i Alert til type button ([#2497](https://github.com/navikt/aksel/pull/2497))
@@ -364,6 +380,10 @@
## 5.7
### @navikt/aksel-icons
- Ikoner: Nye ikoner `ChevronLeftFirstCircle` og `ChevronRightLastCircle` ([#2365](https://github.com/navikt/aksel/pull/2365))
### @navikt/ds-css
- :lipstick: Chips.Toggle: Ikon i uvalgt tilstand ([#2395](https://github.com/navikt/aksel/pull/2395))
@@ -404,11 +424,15 @@
- :lipstick: CopyButton: Justert padding, gap og animasjon ([#2355](https://github.com/navikt/aksel/pull/2355))
## 5.6
### @navikt/aksel-icons
- Ikoner: Nye ikoner `ChevronLeftFirstCircle` og `ChevronRightLastCircle` ([#2365](https://github.com/navikt/aksel/pull/2365))
- Ikoner: SVG export setter nå `height="1em"`, `width="1em"` og `fill="currentColor"`. ([#2300](https://github.com/navikt/aksel/pull/2300))
## 5.6
- Ikoner: Nye ikoner `book` og `books` ([#2313](https://github.com/navikt/aksel/pull/2313))
* Ikoner: Nye ikoner `ClipboardCheckmark` og `ClipboardLink` ([#2307](https://github.com/navikt/aksel/pull/2307))
### @navikt/ds-css
@@ -460,14 +484,6 @@
- Remove tabIndex="-1" on Popover to fix VoiceOver navigation issue inside Popover ([#2303](https://github.com/navikt/aksel/pull/2303))
### @navikt/aksel-icons
- Ikoner: SVG export setter nå `height="1em"`, `width="1em"` og `fill="currentColor"`. ([#2300](https://github.com/navikt/aksel/pull/2300))
- Ikoner: Nye ikoner `book` og `books` ([#2313](https://github.com/navikt/aksel/pull/2313))
* Ikoner: Nye ikoner `ClipboardCheckmark` og `ClipboardLink` ([#2307](https://github.com/navikt/aksel/pull/2307))
## 5.5
### @navikt/ds-css
@@ -508,6 +524,10 @@
## 5.3
### @navikt/aksel-icons
- :bug: SVG import map er nå riktig path ([#2250](https://github.com/navikt/aksel/pull/2250))
### @navikt/ds-css
- Alert: 12px -> 8px gap mellom tekst og ikon ved `size="small"` ([#2240](https://github.com/navikt/aksel/pull/2240))
@@ -544,11 +564,11 @@
* :wheelchair: Textarea: Skjermleser-spesifikk tekst leses opp sammenhengende ([#2216](https://github.com/navikt/aksel/pull/2216))
## 5.2
### @navikt/aksel-icons
- :bug: SVG import map er nå riktig path ([#2250](https://github.com/navikt/aksel/pull/2250))
## 5.2
- Ikoner: Oppdatert ikonpakke ([#2197](https://github.com/navikt/aksel/pull/2197))
### @navikt/ds-css
@@ -566,10 +586,6 @@
- Hovering over combobox dropdown will move selection/focus in the list, so we don't end up with a split focus, and reversely when moving focus while hovering ([#2193](https://github.com/navikt/aksel/pull/2193))
### @navikt/aksel-icons
- Ikoner: Oppdatert ikonpakke ([#2197](https://github.com/navikt/aksel/pull/2197))
## 5.1
### @navikt/ds-css
@@ -779,18 +795,18 @@
## 4.4
### @navikt/ds-tailwind
- Shadow-tokens er oppdatert ([PR](https://github.com/navikt/aksel/pull/2041))
- Oppdatert text-subtle og icon-subtle tokens til 700-skala (tidligere 600). ([PR](https://github.com/navikt/aksel/pull/2036))
### @navikt/ds-css
- :lipstick: Chat: fjernet border, satt avatar svg til 24x24px, byttet om "subtle" og "neutral" ([#2077](https://github.com/navikt/aksel/pull/2077))
* Shadow-tokens er oppdatert: Datepicker har nå ikke border, Modal bruker shadow-xlarge, LinkCard bruker shadow-xsmall ([PR](https://github.com/navikt/aksel/pull/2041))
### @navikt/ds-tailwind
- Shadow-tokens er oppdatert ([PR](https://github.com/navikt/aksel/pull/2041))
- Oppdatert text-subtle og icon-subtle tokens til 700-skala (tidligere 600). ([PR](https://github.com/navikt/aksel/pull/2036))
### @navikt/ds-tokens
- Shadow-tokens er oppdatert til mer tydeligere varianter ([PR](https://github.com/navikt/aksel/pull/2041))
@@ -825,9 +841,9 @@
## 4.1
### @navikt/ds-tailwind
### @navikt/aksel-icons
- :tada: Fargetokens for datavisualisering. ([PR](https://github.com/navikt/aksel/pull/2032))
- Nye ikoner `ChevronRightLast` og `ChevronLeftLast` ([PR](https://github.com/navikt/aksel/pull/2054))
### @navikt/ds-css
@@ -839,6 +855,10 @@
- Alle description-felter på fieldsets har nå `text-subtle` som farge. ([PR](https://github.com/navikt/aksel/pull/2036))
### @navikt/ds-tailwind
- :tada: Fargetokens for datavisualisering. ([PR](https://github.com/navikt/aksel/pull/2032))
### @navikt/ds-tokens
- :tada: Fargetokens for datavisualisering. ([PR](https://github.com/navikt/aksel/pull/2032))
@@ -873,10 +893,6 @@
- Deprecated klassenavn `navds-chips--icon-left` ([PR](https://github.com/navikt/aksel/pull/2035))
### @navikt/aksel-icons
- Nye ikoner `ChevronRightLast` og `ChevronLeftLast` ([PR](https://github.com/navikt/aksel/pull/2054))
## 4.0
### @navikt/ds-css
@@ -929,6 +945,10 @@
## 3.2
### @navikt/aksel-icons
- Nye ikoner `FileParagraph` og `FilePlus` ([PR](https://github.com/navikt/aksel/pull/1998))
### @navikt/ds-css
- Datepicker: Fikset small-variant av datepicker-input. ([PR](https://github.com/navikt/aksel/pull/1993))
@@ -947,10 +967,6 @@
* Ny komponent CopyButton! Erstatter `CopyToClipboard` som nå er tagget som deprecated ([PR](https://github.com/navikt/aksel/pull/1982))
### @navikt/aksel-icons
- Nye ikoner `FileParagraph` og `FilePlus` ([PR](https://github.com/navikt/aksel/pull/1998))
## 3.1
### @navikt/ds-css
@@ -983,6 +999,16 @@
## 2.9
### @navikt/aksel-icons
- Oppdatert Statusikoner til å være tydeligere og mer konsistente. ([PR](https://github.com/navikt/aksel/pull/1959))
* Fikset `CheckmarkIcon`-bug
- Fikset `CheckmarkCircleIcon`-bug ([PR](https://github.com/navikt/aksel/pull/1956))
* Nye ikoner `BulletList` og `NumberList` ([PR](https://github.com/navikt/aksel/pull/1950))
### @navikt/ds-css
- CSS nå tilgjengelig som separate filer: Kan lastest fra CDN, minified-versjoner tilgjengelig ([PR](https://github.com/navikt/aksel/pull/1941))
@@ -995,17 +1021,19 @@
- :truck: Flyttet Codemods fra `@navikt/ds-codemods` -> `@navikt/aksel`. `@navikt/ds-codemods` regnes nå som deprecated ([PR](https://github.com/navikt/aksel/pull/1952))
## 2.8
### @navikt/aksel-icons
- Oppdatert Statusikoner til å være tydeligere og mer konsistente. ([PR](https://github.com/navikt/aksel/pull/1959))
- :tada: Nye ikoner `HandKnot` og `HandKnotFilled` ([PR](https://github.com/navikt/aksel/pull/1928))
* Fikset `CheckmarkIcon`-bug
* Oppdatert `InformationSquare`-ikon ([PR](https://github.com/navikt/aksel/pull/1921))
- Fikset `CheckmarkCircleIcon`-bug ([PR](https://github.com/navikt/aksel/pull/1956))
- Oppdatert `VideoSlack`, `Questionmark` og `Information`-ikoner ([PR](https://github.com/navikt/aksel/pull/1909))
* Nye ikoner `BulletList` og `NumberList` ([PR](https://github.com/navikt/aksel/pull/1950))
* Nye ikoner `VideoSlash` og `VideoSlashFill`
## 2.8
- Fikset Sourcemap-bug
### @navikt/ds-css
@@ -1041,26 +1069,8 @@
* Ny komponent ExpansionCard! ([PR](https://github.com/navikt/aksel/pull/1820))
### @navikt/aksel-icons
- :tada: Nye ikoner `HandKnot` og `HandKnotFilled` ([PR](https://github.com/navikt/aksel/pull/1928))
* Oppdatert `InformationSquare`-ikon ([PR](https://github.com/navikt/aksel/pull/1921))
- Oppdatert `VideoSlack`, `Questionmark` og `Information`-ikoner ([PR](https://github.com/navikt/aksel/pull/1909))
* Nye ikoner `VideoSlash` og `VideoSlashFill`
- Fikset Sourcemap-bug
## 2.7
### @navikt/ds-css
- Tabs: La til focus-markering for Tabs.Panel ([PR](https://github.com/navikt/aksel/pull/1863))
* ToggleGroup: Fikset token-bug
### @navikt/aksel-icons
- Nye ikoner `CaretUpDownFilledDown`, `CaretUpDownFilledUp`, `CloudDown`, `CloudDownFill`, `CloudUp`, `CloudUpFill`, `HddDown`, `HddDownFill`, `HddUp`, `HddUpFill`, `Inbox`, `InboxDown`, `InboxDownFill`, `InboxFill`, `InboxUp`, `InboxUpFill`, `ShoppingBasket`, `ShoppingBasketFill`
@@ -1069,6 +1079,12 @@
- Ny ikonpakke med for core icons 3! `@navikt/aksel-icons` ([PR](https://github.com/navikt/aksel/pull/1847))
### @navikt/ds-css
- Tabs: La til focus-markering for Tabs.Panel ([PR](https://github.com/navikt/aksel/pull/1863))
* ToggleGroup: Fikset token-bug
## 2.6
### @navikt/ds-react
@@ -1165,10 +1181,6 @@
## 2.0
### @navikt/ds-tailwind
- Alle token er oppdatert til nytt format. ([Guide](https://aksel.nav.no/grunnleggende/kode/migrering#h76f47744d112))
### @navikt/ds-css
- Radio: Fikset default visuell error-state ([PR](https://github.com/navikt/aksel/pull/1737))
@@ -1181,6 +1193,10 @@
* Tokens: Alle komponenter bruker nå semantiske tokens for som standard, med innebygd støtte for komponent-spesifikke tokens.
### @navikt/ds-tailwind
- Alle token er oppdatert til nytt format. ([Guide](https://aksel.nav.no/grunnleggende/kode/migrering#h76f47744d112))
### @navikt/ds-tokens
- Alle token er oppdatert til nytt format. ([Guide](https://aksel.nav.no/grunnleggende/kode/migrering#h76f47744d112))
+2 -1
View File
@@ -4,8 +4,9 @@ ENV NODE_ENV production
WORKDIR /app
COPY aksel.nav.no/website/.next/standalone ./
COPY aksel.nav.no/website/config/next-logger.config.js ./
COPY aksel.nav.no/website/next-logger.config.js ./
ENV NODE_ENV=production
ENV NODE_OPTIONS '-r next-logger'
EXPOSE 3000
+3
View File
@@ -21,6 +21,9 @@ spec:
outbound:
external:
- host: api.sanity.io
- host: api.slack.com
- host: slack.com
- host: wss-primary.slack.com
- host: hnbe3yhs.api.sanity.io
- host: apicdn.sanity.io
- host: hnbe3yhs.apicdn.sanity.io
+3
View File
@@ -25,6 +25,9 @@ spec:
outbound:
external:
- host: api.sanity.io
- host: api.slack.com
- host: slack.com
- host: wss-primary.slack.com
- host: hnbe3yhs.api.sanity.io
- host: apicdn.sanity.io
- host: hnbe3yhs.apicdn.sanity.io
+5 -5
View File
@@ -15,10 +15,10 @@
"playroom"
],
"dependencies": {
"@navikt/aksel-icons": "^6.3.3",
"@navikt/ds-css": "^6.3.3",
"@navikt/ds-react": "^6.3.3",
"@navikt/ds-tailwind": "^6.3.3",
"@navikt/ds-tokens": "^6.3.3"
"@navikt/aksel-icons": "^6.4.0",
"@navikt/ds-css": "^6.4.0",
"@navikt/ds-react": "^6.4.0",
"@navikt/ds-tailwind": "^6.4.0",
"@navikt/ds-tokens": "^6.4.0"
}
}
+1 -1
View File
@@ -11,7 +11,7 @@
"babel-loader": "^9.1.3",
"babel-plugin-preval": "^5.1.0",
"css-loader": "^6.8.1",
"playroom": "^0.32.1",
"playroom": "^0.37.0",
"style-loader": "^3.3.3"
},
"dependencies": {
+1 -1
View File
@@ -13,7 +13,7 @@ module.exports = {
title: "Aksel Sandbox",
snippets: path.resolve("./src/snippets.ts"),
widths: [320, 480, 768, 1024, 1280, 1440],
defaultWidths: [320, 1280], // will be supported if this PR is merged: https://github.com/seek-oss/playroom/pull/302
defaultVisibleWidths: [320, 1280],
port: 9000,
frameComponent: path.resolve("./src/FrameComponent.tsx"),
useScope: path.resolve("./src/useScope.tsx"),
+13 -5
View File
@@ -12,10 +12,10 @@ function getAbsolutePath(value: string): any {
}
const config: StorybookConfig = {
storyIndexers: (indexers) => {
experimental_indexers: (indexers) => {
const csfIndexer = async (fileName: string, opts) => {
const code = readFileSync(fileName, "utf-8").toString();
return loadCsf(code, { ...opts, fileName }).parse();
return loadCsf(code, { ...opts, fileName }).parse().indexInputs;
};
const exampleIndexer = async (fileName: string, opts) => {
@@ -32,18 +32,18 @@ const config: StorybookConfig = {
.split("pages/eksempler/")[1]
.replace(".tsx", "")}" };\n`;
return loadCsf(code, { ...opts, fileName }).parse();
return loadCsf(code, { ...opts, fileName }).parse().indexInputs;
};
return [
...(indexers || []),
{
test: /(stories|story)\.[tj]sx?$/,
indexer: csfIndexer,
createIndex: csfIndexer,
},
{
test: /pages\/eksempler\/|(".+")|(.+.tsx)$/,
indexer: exampleIndexer,
createIndex: exampleIndexer,
},
];
},
@@ -99,6 +99,14 @@ const config: StorybookConfig = {
__dirname,
"../components/layout/",
);
sbConfig.resolve.alias["@/auth"] = resolve(
__dirname,
"../components/auth/",
);
sbConfig.resolve.alias["@/slack"] = resolve(
__dirname,
"../components/utils/slack/index.ts",
);
sbConfig.resolve.alias["@/assets"] = resolve(
__dirname,
"../components/assets/",
@@ -0,0 +1,32 @@
import { NextApiRequest, NextApiResponse } from "next/types";
export type ApiHandler = (
req: NextApiRequest,
res: NextApiResponse,
) => Promise<unknown> | unknown;
export type AuthUser = {
name: "name";
email: "email";
};
export const AuthApiErrorReturn = {
"Access denied": "Access denied",
} as const;
export type AuthApiErrorReturn = {
ok: false;
error: (typeof AuthApiErrorReturn)[keyof typeof AuthApiErrorReturn];
};
type LoggedInT = {
signedIn: true;
user: AuthUser;
};
type NotLoggedInT = {
signedIn: false;
user: null;
};
export type UserStateT = LoggedInT | NotLoggedInT;
@@ -1,11 +1,6 @@
import { NextApiRequest, NextApiResponse } from "next/types";
import { logger } from "@navikt/next-logger";
import { ApiHandler } from "@/auth/auth.types";
import { validateWonderwallToken } from "@/auth/validateWonderwall";
import { logger } from "../../config/logger";
type ApiHandler = (
req: NextApiRequest,
res: NextApiResponse,
) => Promise<unknown> | unknown;
/**
* Used to authenticate Next.JS pages. Assumes application is behind
@@ -1,15 +1,13 @@
import { NextApiRequest } from "next/types";
import { logger } from "@navikt/next-logger";
import { AuthUser } from "@/auth/auth.types";
export function getAuthUser(headers: NextApiRequest["headers"]): {
name: string | null;
email: string | null;
} {
export function getAuthUser(
headers: NextApiRequest["headers"],
): AuthUser | null {
const token = getToken(headers);
if (!token) {
return {
name: null,
email: null,
};
return null;
}
const jwt = JSON.parse(
@@ -26,7 +24,12 @@ export const fakeToken =
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiIsImtpZCI6IjlHbW55RlBraGMzaE91UjIybXZTdmduTG83WSJ9.eyJhdWQiOiJmYWtlLWF1ZCIsImlzcyI6Imh0dHBzOi8vbG9naW4ubWljcm9zb2Z0b25saW5lLmNvbS85NjZhYzU3Mi1mNWI3LTRiYmUtYWE4OC1jNzY0MTljMGY4NTEvdjIuMCIsImlhdCI6MTY5ODQxMTU0NiwibmJmIjoxNjk4NDExNTQ2LCJleHAiOjE2OTg0MTY1MjYsImFpbyI6ImZha2UtYWlvIiwiYXpwIjoiZmFrZS1henAiLCJhenBhY3IiOiIyIiwiZ3JvdXBzIjpbImZha2UtZ3JvdXAiXSwibmFtZSI6IkZha2UgVG9rZW5lbnNzb24iLCJvaWQiOiJmYWtlLW9pZCIsInByZWZlcnJlZF91c2VybmFtZSI6ImZha2UtbG9uZy1lbWFpbC1hdEBleGFtcGxlLm5vIiwicmgiOiJmYWtlLXJoLiIsInNjcCI6ImRlZmF1bHRhY2Nlc3MiLCJzdWIiOiJmYWtlLXN1YiIsInRpZCI6ImZha2UtdGlkIiwidXRpIjoiekY4R0VGemhfRTZya3gtYzNnNFNBQSIsInZlciI6IjIuMCIsIk5BVmlkZW50IjoiWjk5MjM4OSIsImF6cF9uYW1lIjoiZGV2LWdjcDpmYWtlLXRlYW06aGVsc2VzamVray1ib3QifQ.OOP_Q0ZcUI6jQzM1UON9mA8d8BU_GhioQ5OLuepwebg";
export function getToken(headers: NextApiRequest["headers"]) {
if (process.env.NODE_ENV !== "production") return fakeToken;
if (process.env.NODE_ENV !== "production") {
logger.info(
"Is running locally or in demo, using local fake token for Authenticated user-requests",
);
return fakeToken;
}
return headers.authorization?.replace("Bearer ", "") ?? null;
}
@@ -0,0 +1,35 @@
import { NextApiRequest } from "next/types";
import { logger } from "@navikt/next-logger";
import { UserStateT } from "@/auth/auth.types";
import { getAuthUser } from "@/auth/getAuthUser";
import { validateWonderwallToken } from "@/auth/validateWonderwall";
export async function getAuthUserState(
headers: NextApiRequest["headers"],
): Promise<UserStateT> {
const isSignedIn = await validateWonderwallToken(headers);
if (!isSignedIn) {
return {
signedIn: false,
user: null,
};
}
const user = getAuthUser(headers);
if (!user) {
logger.error(
"User is signed in, but no user was found in the token. This should be impossible!",
);
return {
signedIn: false,
user: null,
};
}
return {
signedIn: true,
user,
};
}
@@ -1,16 +1,22 @@
import { useRouter } from "next/router";
export const useAuth = () => {
const router = useRouter();
export const useAuth = (addRedirect?: boolean) => {
const { push, asPath } = useRouter();
const login = async () => {
router.push(`/oauth2/login?redirect=${router.asPath}`, undefined, {
shallow: true,
});
push(
`/oauth2/login?redirect=${asPath.split("#")[0]}${
addRedirect ? encodeURIComponent("#scrollToFeedback") : ""
}`,
undefined,
{
shallow: true,
},
);
};
const logout = async () => {
router.push(`/oauth2/logout?redirect=${router.asPath}`, undefined, {
push(`/oauth2/logout?redirect=${asPath}`, undefined, {
shallow: true,
});
};
@@ -1,6 +1,6 @@
import { NextApiRequest } from "next/types";
import { logger } from "@navikt/next-logger";
import { getToken, validateAzureToken } from "@navikt/oasis";
import { logger } from "../../config/logger";
/**
* Validates the wonderwall token according to nais.io. Should only actually redirect if the token has expired.
@@ -29,6 +29,10 @@ export async function validateWonderwallToken(
{ cause: validationResult.error },
),
);
} else {
logger.error(
`JWT token has expired (cause: ${validationResult.errorType} ${validationResult.error}`,
);
}
return false;
}
@@ -32,7 +32,7 @@ const Footer = () => {
heading="Snarveier"
links={[
{
url: "/god-praksis/artikler/skriv-for-aksel",
url: "/side/skriv-for-aksel",
text: "Skriv for Aksel",
},
{
@@ -50,7 +50,7 @@ const Footer = () => {
heading="Om nettstedet"
links={[
{
url: "/god-praksis/artikler/om-aksel",
url: "/side/om-aksel",
text: "Hva er Aksel?",
},
{
@@ -6,4 +6,5 @@ export enum AmplitudeEvents {
"feedbackinteraksjon" = "feedbackinteraksjon",
"søk" = "søk",
"link" = "link",
"slackfeedback" = "slackfeedback",
}
@@ -61,7 +61,7 @@ const useAmplitudeInit = () => {
.forEach(([event, eventData]) => amplitude.track(event, eventData)),
)
.catch(async () => {
const { logger } = await import("../../config/logger");
const { logger } = await import("@navikt/next-logger");
logger.error("Failed logging batched events");
});
};
@@ -15,7 +15,7 @@ const BloggCard = ({ blog }: BloggCardProps) => {
return (
<li
key={blog._id}
className="flex h-full flex-col place-content-start border-b border-b-border-subtle pb-8"
className="flex h-full flex-col place-content-start border-b border-b-border-subtle pb-10"
>
<Heading size="medium" as="div">
<NextLink href={`/${blog.slug}`} passHref legacyBehavior>
@@ -32,7 +32,7 @@ const BloggCard = ({ blog }: BloggCardProps) => {
</Link>
</NextLink>
</Heading>
<BodyLong className="my-2">{blog?.ingress}</BodyLong>
<BodyLong className="my-2 line-clamp-2">{blog?.ingress}</BodyLong>
{getAuthors(blog).length > 0 && (
<BodyShort
size="small"
@@ -2,7 +2,11 @@ export const dateStr = async (date: string) => {
if (!date) {
return "";
}
const { format } = await import("date-fns");
const { nb } = await import("date-fns/locale");
const [{ format }, { nb }] = await Promise.all([
import("date-fns"),
import("date-fns/locale"),
]);
return format(new Date(date), "d. MMMM yyy", { locale: nb });
};
@@ -0,0 +1,42 @@
import { expect, test } from "vitest";
import { findUserByEmail } from "../find-user";
test("findUserByEmail returns user when user with given email exists", () => {
const members = [
{ id: "test_id", email: "test1@example.com" },
{ id: "test_id", email: "test2@example.com" },
{ id: "test_id", email: "test3@example.com" },
];
const result = findUserByEmail("test2@example.com", members);
expect(result).toEqual(members[1]);
});
test("findUserByEmail returns undefined when user with given email does not exist", () => {
const members = [
{ id: "test_id", email: "test1@example.com" },
{ id: "test_id", email: "test2@example.com" },
{ id: "test_id", email: "test3@example.com" },
];
const result = findUserByEmail("nonexistent@example.com", members);
expect(result).toBeUndefined();
});
test("findUserByEmail returns undefined member contains no email-key", () => {
const members = [{}, {}, {}];
const result = findUserByEmail("nonexistent@example.com", members);
expect(result).toBeUndefined();
});
test("findUserByEmail is case insensitive", () => {
const members = [
{ id: "test_id", email: "test1@example.com" },
{ id: "test_id", email: "test2@example.com" },
{ id: "test_id", email: "test3@example.com" },
];
const result = findUserByEmail("TEST2@EXAMPLE.COM", members);
expect(result).toEqual(members[1]);
});
@@ -0,0 +1,94 @@
import { UsersListResponse, WebClient } from "@slack/web-api";
import NodeCache from "node-cache";
import {
FetchSlackMembersError,
FetchSlackMembersSuccess,
SanitizedUser,
} from "./slack.types";
const cache = new NodeCache();
const CACHE_KEY = "fetchslackMembers";
export async function fetchSlackMembers(): Promise<
FetchSlackMembersSuccess | FetchSlackMembersError
> {
const slackMembers: FetchSlackMembersSuccess["members"] | undefined =
cache.get(CACHE_KEY);
if (slackMembers) {
return {
ok: true,
members: slackMembers,
};
}
const client = new WebClient(process.env.SLACK_BOT_TOKEN);
const pagination: {
limit: number;
cursor: string | undefined;
continue: boolean;
} = {
limit: 900,
cursor: undefined,
continue: true,
};
let members: Exclude<UsersListResponse["members"], undefined> = [];
let error: string | undefined = undefined;
while (pagination.continue) {
const users = await client.users.list({
cursor: pagination.cursor,
limit: pagination.limit,
});
if (users.ok && users.members) {
members = members.concat(users.members);
} else {
pagination.continue = false;
error = users.error;
break;
}
if (
users.response_metadata?.next_cursor &&
users.response_metadata?.next_cursor !== ""
) {
pagination.cursor = users.response_metadata.next_cursor;
} else {
pagination.continue = false;
}
}
if (error) {
return {
ok: false,
error: error ?? "Error when paginating slack-users",
};
}
/**
* Lets filter out som unwanted users to simplify the list
*/
const subset_members = members
.filter((m) => !m.is_bot)
.filter((m) => !m.deleted)
.filter((m) => m.profile?.email)
.filter((m) => m.id)
.map((m) => ({
id: m.id,
email: m.profile?.email,
})) as SanitizedUser[]; // TODO: perhaps not needed with newer TS version
if (subset_members.length === 0) {
return { ok: false, error: "No members found" };
}
cache.set(CACHE_KEY, subset_members, 60 * 60 * 24);
return {
ok: true,
members: subset_members,
};
}
@@ -0,0 +1,8 @@
import { SanitizedUser } from "./slack.types";
export function findUserByEmail(
email: string,
members: Partial<SanitizedUser>[],
) {
return members?.find((m) => m.email?.toLowerCase() === email.toLowerCase());
}
@@ -0,0 +1,7 @@
export { fetchSlackMembers } from "./fetch-members";
export { findUserByEmail } from "./find-user";
export {
SlackFeedbackError,
type SlackFeedbackResponse,
type SlackFeedbackErrorT,
} from "./slack.types";
@@ -0,0 +1,57 @@
import { AuthApiErrorReturn } from "@/auth/auth.types";
export type SlackFeedbackInput = {
/**
* Feedback message
*/
feedback: string;
/**
* If user wants to be anonymous
*/
anon: boolean;
/**
* Sanity-document id for article feedback is about
*/
document_id: string;
};
type SlackFeedbackOkResponse = {
ok: true;
};
export const SlackFeedbackError = {
InvalidBody: "InvalidBody",
InvalidUser: "InvalidUser",
InvalidId: "InvalidId",
NoEditors: "NoEditors",
NoSlackUsers: "NoSlackUsers",
PostMessageError: "PostMessageError",
} as const;
export type SlackFeedbackErrorT =
(typeof SlackFeedbackError)[keyof typeof SlackFeedbackError];
type SlackFeedbackErrorResponse = {
ok: false;
error: SlackFeedbackErrorT;
};
export type SlackFeedbackResponse =
| SlackFeedbackOkResponse
| SlackFeedbackErrorResponse
| AuthApiErrorReturn;
export type SanitizedUser = {
id: string;
email: string;
};
export type FetchSlackMembersSuccess = {
ok: true;
members: SanitizedUser[];
};
export type FetchSlackMembersError = {
ok: false;
error: string;
};
@@ -0,0 +1,22 @@
.responsive {
font-size: 4rem;
}
.responsive_square {
position: absolute;
transform: translate(1rem, -1.1rem);
}
.responsive_inbox {
position: absolute;
}
@media (max-width: 768px) {
.responsive {
font-size: 3rem;
}
.responsive_square {
transform: translate(0.95rem, -1rem);
}
}
@@ -0,0 +1,53 @@
import type { Meta, StoryObj } from "@storybook/react";
import { userEvent, within } from "@storybook/test";
import { AkselTheme } from "@/sb-util";
import { Feedback, FeedbackForm } from "./Feedback";
const meta = {
title: "Layout/god-praksis/Feedback",
component: Feedback,
tags: ["autodocs"],
decorators: [AkselTheme],
} satisfies Meta<typeof Feedback>;
export default meta;
type Story = StoryObj<typeof meta>;
export const WithOutName: Story = {
args: { userState: { signedIn: false, user: null } },
};
export const WithName: Story = {
args: {
userState: {
signedIn: true,
user: {
email: "email",
name: "name",
},
},
},
};
export const InteractionStates = {
render: () => {
return <FeedbackForm user={{ email: "email", name: "name" }} />;
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByLabelText("Innspill");
await userEvent.click(input);
await userEvent.type(input, "e".repeat(500) + "!");
const submit = canvas.getByText("Send inn");
await userEvent.click(submit);
await new Promise((resolve) => setTimeout(resolve, 1000));
await userEvent.click(input);
await userEvent.keyboard("{backspace}");
await userEvent.click(submit);
},
};
@@ -0,0 +1,262 @@
import React, { useState } from "react";
import { InboxDownIcon, PersonIcon } from "@navikt/aksel-icons";
import {
Alert,
BodyLong,
BodyShort,
Box,
Button,
HGrid,
HStack,
Heading,
Link,
Textarea,
VStack,
} from "@navikt/ds-react";
import { AuthUser, UserStateT } from "@/auth/auth.types";
import { useAuth } from "@/auth/useAuth";
import { useSanityData } from "@/hooks/useSanityData";
import { AmplitudeEvents, amplitude } from "@/logging";
import { SlackFeedbackResponse } from "@/slack";
import styles from "./Feedback.module.css";
type States = "feedbackSent" | "loggedIn" | "error" | "submittingForm";
export const FeedbackForm = ({ user }: { user: AuthUser }) => {
const [state, setState] = useState<States>("loggedIn");
const [APIError, setAPIError] = useState<null | string>(null);
const sanityDocumentId = useSanityData()?.id;
const { logout } = useAuth();
const [formError, setFormError] = useState<string | null>(null);
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
setState("loggedIn");
const formData = new FormData(event.currentTarget);
const feedback = (formData.get("feedback") as string) || "";
if (!feedback) {
setFormError("Feltet kan ikke være tomt.");
return;
}
if (feedback.length > 500) {
setFormError("Tilbakemeldingen må være under 500 tegn.");
return;
}
const body = JSON.stringify({
feedback: feedback.slice(0, 500) || "",
document_id: sanityDocumentId,
});
setState("submittingForm");
fetch("/api/slack/feedback/v1", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body,
})
.then((res) => res.json())
.then((res: SlackFeedbackResponse) => {
const feedbackMetadata = {
side: window.location.pathname,
length: feedback.length,
};
if (!res.ok) {
setAPIError(res.error);
setState("error");
amplitude.track(AmplitudeEvents.slackfeedback, {
result: "error",
...feedbackMetadata,
});
} else {
setFormError(null);
setState("feedbackSent");
amplitude.track(AmplitudeEvents.slackfeedback, {
result: "success",
...feedbackMetadata,
});
}
})
.catch(() => {
setAPIError("unknownError");
setState("error");
amplitude.track(AmplitudeEvents.slackfeedback, {
result: "error",
side: window.location.pathname,
length: feedback.length,
});
});
};
if (state === "feedbackSent") {
return (
<div>
<IntroSection
heading="Innspill sendt"
description="Ditt innspill er viktig for å holde kvaliteten oppe og innholdet
relevant. Takk skal du ha!"
/>
<Button
onClick={() => setState("loggedIn")}
className="bg-deepblue-600 hover:bg-deepblue-700 active:bg-deepblue-700"
>
Nytt innspill
</Button>
</div>
);
}
return (
<form onSubmit={handleSubmit}>
<IntroSection
heading="Innspill til artikkelen"
description="Har du innspill til artikkelen? Meldingen blir sendt med Slack til team Aksel og folka som har lagd artikkelen 🙌"
/>
<VStack gap="4">
<HStack gap="2">
<PersonIcon aria-hidden fontSize="1.5rem" />
<BodyShort>{user.name}</BodyShort>
<BodyShort>
(
<Link
onClick={() => {
logout();
}}
href="#"
>
logg ut
</Link>
)
</BodyShort>
</HStack>
<Textarea
name="feedback"
label="Innspill"
minRows={4}
maxLength={500}
error={formError}
onInput={(element) => {
if (element.currentTarget.value.length > 500) {
return;
}
setFormError(null);
}}
/>
</VStack>
<Button
type="submit"
loading={state === "submittingForm"}
className="mt-4 bg-deepblue-600 hover:bg-deepblue-700 active:bg-deepblue-700"
>
Send inn
</Button>
<>
{state === "error" && (
<Alert variant="error" className="mt-4" role="alert">
Noe gikk galt. Hvis feilen oppstår flere ganger eller du har lyst
til å sende tilbakemeldingen direkte finner du oss under{" "}
<Link inlineText href="https://nav-it.slack.com/archives/C7NE7A8UF">
#aksel-designsystemet
</Link>{" "}
på slack.
<BodyShort
textColor="subtle"
size="small"
className="mt-3"
>{`Feil-id: ${APIError}`}</BodyShort>
</Alert>
)}
</>
</form>
);
};
function IntroSection({
heading,
description,
}: {
heading: string;
description: string;
}) {
return (
<>
<Heading
level="2"
tabIndex={-1}
id="scrollToFeedback"
size="small"
className="mb-1 focus:outline-none"
>
{heading}
</Heading>
<BodyLong className="mb-4">{description}</BodyLong>
</>
);
}
type Props = {
userState: UserStateT;
};
export const Feedback = ({ userState }: Props) => {
const { login } = useAuth(true);
return (
<Box
borderRadius="large"
background="surface-neutral-subtle"
className="mt-20"
padding="6"
>
<HGrid columns={{ xs: "1fr 3.5rem", md: "1fr 4.5rem" }} gap="2">
{userState.signedIn ? (
<FeedbackForm user={userState.user} />
) : (
<div>
<IntroSection
heading="Innspill til artikkelen"
description="Logg inn med NAV SSO for å gi innspill til artikkelen"
/>
<Button
onClick={() => login()}
className="bg-deepblue-600 hover:bg-deepblue-700 active:bg-deepblue-700"
>
Logg inn med NAV SSO
</Button>
</div>
)}
<div className="responsive-svg relative translate-x-[-0.2rem] translate-y-[0.7rem]">
<svg
aria-hidden
width="1em"
height="1em"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={`${styles.responsive} ${styles.responsive_square}`}
>
<rect
x="0"
y="0"
width="14"
height="14"
rx="0.75"
transform="translate(12.2 2.5) rotate(45 0 0)"
fill="#D6C3EE"
/>
</svg>
<InboxDownIcon
className={`${styles.responsive} ${styles.responsive_inbox} absolute`}
aria-hidden
fontSize="4rem"
/>
</div>
</HGrid>
</Box>
);
};
-7
View File
@@ -1,7 +0,0 @@
import pino from "pino";
import { logger as sharedPinoConfig } from "./next-logger.config";
/**
* This logger is not isomorphic, and should only be used on the server
*/
export const logger: pino.Logger = sharedPinoConfig();
@@ -1,31 +0,0 @@
const pino = require("pino");
const logger = (defaultConfig = {}) =>
pino({
...defaultConfig,
timestamp: false,
formatters: {
level: (label) => {
return { level: label };
},
log: (object) => {
if (object.err) {
// massage the error object so logs.adeo.no shows stack traces
const err =
object.err instanceof Error
? pino.stdSerializers.err(object.err)
: object.err;
object.stack_trace = err.stack;
object.type = err.type;
object.message = err.message;
delete object.err;
}
return object;
},
},
});
module.exports = {
logger,
};
@@ -0,0 +1,5 @@
const { backendLogger } = require("@navikt/next-logger");
module.exports = {
logger: backendLogger,
};
+6 -2
View File
@@ -32,6 +32,7 @@
},
"dependencies": {
"@amplitude/analytics-browser": "^2.2.3",
"@navikt/next-logger": "1.23.0",
"@navikt/oasis": "3.2.1",
"@portabletext/react": "^3.0.11",
"@sanity/client": "^6.13.3",
@@ -41,6 +42,7 @@
"@sanity/locale-nb-no": "^1.1.0",
"@sanity/table": "1.1.2",
"@sanity/vision": "3.34.0",
"@slack/web-api": "^7.0.2",
"boring-avatars": "1.10.1",
"clsx": "^2.1.0",
"codesandbox": "^2.2.3",
@@ -50,8 +52,9 @@
"lodash": "^4.17.21",
"lz-string": "^1.5.0",
"next": "14.1.1-canary.57",
"next-logger": "3.0.2",
"next-logger": "^3.0.2",
"next-sanity": "5.5.8",
"node-cache": "^5.1.2",
"pino": "^8.14.1",
"prism-react-renderer": "^2.0.0",
"react": "^18.0.0",
@@ -67,7 +70,8 @@
"styled-components": "^6.0.0",
"swr": "^1.1.2",
"tailwindcss": "^3.3.3",
"tinycolor2": "^1.6.0"
"tinycolor2": "^1.6.0",
"zod": "^3.22.4"
},
"devDependencies": {
"@axe-core/playwright": "^4.5.2",
+1
View File
@@ -0,0 +1 @@
export { pinoLoggingRoute as default } from "@navikt/next-logger";
@@ -0,0 +1,326 @@
import { SectionBlock, UsersListResponse, WebClient } from "@slack/web-api";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { createChildLogger } from "@navikt/next-logger";
import { authProtectedApi } from "@/auth/authProtectedApi";
import { getAuthUser } from "@/auth/getAuthUser";
import { getClient } from "@/sanity/client.server";
import {
SlackFeedbackError,
SlackFeedbackErrorT,
SlackFeedbackResponse,
fetchSlackMembers,
findUserByEmail,
} from "@/slack";
const maxFeedbackLength = 500;
const requestBodySchema = z.object({
body: z.object({
feedback: z
.string({
required_error: "Message-string is required",
})
.min(1, "Message-string cannot be empty")
.max(
maxFeedbackLength,
`Message-string cannot be longer than ${maxFeedbackLength} characters`,
),
document_id: z.string({
required_error: "Document-id is required",
}),
}),
});
const client = new WebClient(process.env.SLACK_BOT_TOKEN);
export default authProtectedApi(sendSlackbotFeedback);
async function sendSlackbotFeedback(
request: NextApiRequest,
response: NextApiResponse,
) {
const logger = createChildLogger("slackbot-feedback");
logger.info("Received slackbot feedback request");
const user = getAuthUser(request.headers);
/**
* If authenticated user is not found, we return 400.
* This should in theory no be possible since api is behind `authProtectedApi`
*/
if (!user) {
return errorResponse(response, SlackFeedbackError.InvalidUser);
}
/**
* Validate the request with zod before we continue flow
*/
const validation = requestBodySchema.safeParse({
body: request.body,
});
if (validation.success === false) {
return errorResponse(response, SlackFeedbackError.InvalidBody);
}
/**
* Sanity caches requests with cdn, so no need to cache locally
*/
const document = await getClient().fetch(
`*[_id == $id][0]{
"id": _id,
"title": heading,
"editors": contributors[]->email,
"slug": slug.current
}`,
{
id: validation.data.body.document_id,
},
);
/**
* If no document is found, sanity will return `null`
*/
if (!document) {
return errorResponse(response, SlackFeedbackError.InvalidId);
}
const slackMembers = await fetchSlackMembers();
if (slackMembers.ok === false) {
return errorResponse(response, SlackFeedbackError.NoSlackUsers);
}
/**
* We find the sender in list of slack members
* Since everyone in NAV has access to login,
* but might not use slack we can have some cases where no user is found
*/
const senderSlackUser = findUserByEmail(user.email, slackMembers.members);
/**
* We use contributors found on article and find their matching slack profiles
*/
const slackProfileForEditors = document.editors
.filter(Boolean)
.map((email: string) => findUserByEmail(email, slackMembers.members))
.filter(Boolean) as Exclude<UsersListResponse["members"], undefined>;
let postMessageError = false;
await client.chat
.postMessage({
/* ChannelId for #aksel-feedback-bot */
channel: "C06PY34NNJH",
text: `Tilbakemelding: ${validation.data.body.feedback}`,
blocks: [
...slackBlock({
article: {
id: document.id,
slug: document.slug,
title: document.title,
},
feedback: validation.data.body.feedback,
recievers: slackProfileForEditors
.map((x) => x.id)
.filter(Boolean) as string[],
sender: {
email: user.email,
slackId: senderSlackUser?.id,
},
}),
...(slackProfileForEditors.length === 0
? [
{
type: "section",
text: {
type: "mrkdwn",
text: ":exclamation: Denne artikkelen har ingen redaktører. @here",
},
} satisfies SectionBlock,
]
: []),
],
})
.catch((e) => {
postMessageError = true;
logger.error(
`Error when sending slackbot feedback to private channel: ${e}`,
);
});
for (const editor of slackProfileForEditors) {
if (!editor.id) {
continue;
}
await client.chat
.postMessage({
channel: editor.id,
text: `Tilbakemelding: ${validation.data.body.feedback}`,
blocks: slackBlock({
article: {
id: document.id,
slug: document.slug,
title: document.title,
},
feedback: validation.data.body.feedback,
recievers: slackProfileForEditors
.map((x) => x.id)
.filter(Boolean) as string[],
sender: {
email: user.email,
slackId: senderSlackUser?.id,
},
}),
})
.catch((e) => {
postMessageError = true;
logger.error(`Error when sending slackbot feedback: ${e}`);
});
}
if (postMessageError) {
return errorResponse(response, SlackFeedbackError.PostMessageError);
}
response.status(200).json(responseJson(true));
return;
}
const ErrorMap = {
[SlackFeedbackError.InvalidUser]: `Error with getAuthUser in slackbot feedback. This should not happend since we are using authProtectedApi`,
[SlackFeedbackError.InvalidBody]: `Error with validation of body in slackbot feedback`,
[SlackFeedbackError.InvalidId]: `Error when fetching sanity document for slackbot feedback`,
[SlackFeedbackError.NoSlackUsers]: `Error extracting members from slack in slackbot feedback`,
[SlackFeedbackError.PostMessageError]: `Error when sending slackbot feedback to private channel or user in slackbot feedback`,
};
function errorResponse(
response: NextApiResponse,
error: keyof typeof ErrorMap,
) {
const logger = createChildLogger("slackbot-feedback");
logger.error(ErrorMap[error] ?? "unknown error");
response.status(400).json(responseJson(false, error));
}
function responseJson(
valid: boolean,
message?: SlackFeedbackErrorT,
): SlackFeedbackResponse {
if (!valid && message) {
return { ok: false, error: message };
}
return { ok: true };
}
type SlackBlockT = {
feedback: string;
article: { slug: string; title: string; id: string };
recievers: string[];
sender: { email: string; slackId?: string };
};
function slackBlock({ feedback, article, recievers, sender }: SlackBlockT) {
return [
{
type: "header",
text: {
type: "plain_text",
/* TODO: Endre melding basert på om du er editor eller temakontakt */
text: "Innspill til Aksel-artikkel du har medvirket til",
emoji: true,
},
},
{
type: "rich_text",
elements: [
{
type: "rich_text_quote",
elements: [
{
type: "text",
text: `\n${feedback}\n\n`,
},
],
},
],
},
{
type: "divider",
},
{
type: "rich_text",
elements: [
{
type: "rich_text_section",
elements: [
{
type: "text",
text: `Avsender:\n`,
style: {
bold: true,
},
},
{
...(sender.slackId
? { type: "user", user_id: sender.slackId }
: { type: "text", text: sender.email }),
},
{
type: "text",
text: "\n\nArtikkel:\n",
style: {
bold: true,
},
},
{
type: "link",
url: `https://aksel.nav.no/${article.slug}`,
text: `${article.title}\n\n`,
},
{
type: "text",
text: "\n\nMedvirkende (disse fikk tilbakemeldingen):\n",
style: {
bold: true,
},
},
],
},
{
type: "rich_text_list",
style: "bullet",
indent: 0,
border: 0,
elements: recievers.map((reciever) => ({
type: "rich_text_section",
elements: [{ type: "user", user_id: reciever }],
})),
},
],
},
{
type: "divider",
},
{
type: "section",
text: {
type: "mrkdwn",
text: "Rediger artikkel ",
},
accessory: {
type: "button",
text: {
type: "plain_text",
text: ":pencil2: Rediger",
emoji: true,
},
url: `https://aksel.nav.no/admin/prod/intent/edit/id=${article.id}`,
},
},
{
type: "divider",
},
];
}
@@ -30,4 +30,5 @@ export const Demo = {
export const args = {
index: 20,
desc: "Vi fraråder bruk av disabled state da elementet blir gjemt fra hjelpeteknologier og ofte vil være vanskelig å oppfatte visuelt.",
};
@@ -2,17 +2,17 @@ import { Checkbox, CheckboxGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any[]) => console.log(val);
const handleChange = (val: string[]) => console.log(val);
return (
<CheckboxGroup
legend="Hvor vil du sitte?"
onChange={(val: any[]) => handleChange(val)}
>
<Checkbox value="Bakerst">Bakerst</Checkbox>
<Checkbox value="Midterst">Midterst</Checkbox>
<Checkbox value="Fremst" description="Tilgjengelig med rullestol">
Fremst
<CheckboxGroup legend="Transportmiddel" onChange={handleChange}>
<Checkbox value="car">Bil</Checkbox>
<Checkbox value="taxi">Drosje</Checkbox>
<Checkbox
value="public"
description="Buss, båt, fly, trikk, T-bane eller tog."
>
Kollektivt
</Checkbox>
</CheckboxGroup>
);
@@ -3,17 +3,13 @@ import { Checkbox, CheckboxGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const [state, setState] = useState(["Midterst"]);
const [state, setState] = useState(["taxi"]);
return (
<CheckboxGroup
legend="Hvor vil du sitte?"
onChange={(v) => setState(v)}
value={state}
>
<Checkbox value="Bakerst">Bakerst</Checkbox>
<Checkbox value="Midterst">Midterst</Checkbox>
<Checkbox value="Fremst">Fremst</Checkbox>
<CheckboxGroup legend="Transportmiddel" onChange={setState} value={state}>
<Checkbox value="car">Bil</Checkbox>
<Checkbox value="taxi">Drosje</Checkbox>
<Checkbox value="public">Kollektivt</Checkbox>
</CheckboxGroup>
);
};
@@ -2,17 +2,17 @@ import { Checkbox, CheckboxGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any[]) => console.log(val);
const handleChange = (val: string[]) => console.log(val);
return (
<CheckboxGroup
legend="Hvor vil du sitte?"
description="Fremste rad er nærmest nødutgangene"
onChange={(val: any[]) => handleChange(val)}
legend="Transportmiddel"
description="Oppgi alle transportmidlene du brukte for å komme til reisemålet."
onChange={handleChange}
>
<Checkbox value="Bakerst">Bakerst</Checkbox>
<Checkbox value="Midterst">Midterst</Checkbox>
<Checkbox value="Fremst">Fremst</Checkbox>
<Checkbox value="car">Bil</Checkbox>
<Checkbox value="taxi">Drosje</Checkbox>
<Checkbox value="public">Kollektivt</Checkbox>
</CheckboxGroup>
);
};
@@ -2,17 +2,18 @@ import { Checkbox, CheckboxGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any[]) => console.log(val);
const handleChange = (val: string[]) => console.log(val);
return (
<CheckboxGroup
legend="Hvor vil du sitte?"
onChange={(val: any[]) => handleChange(val)}
legend="Transportmiddel"
onChange={handleChange}
value={["taxi"]}
disabled
>
<Checkbox value="Bakerst">Bakerst</Checkbox>
<Checkbox value="Midterst">Midterst</Checkbox>
<Checkbox value="Fremst">Fremst</Checkbox>
<Checkbox value="car">Bil</Checkbox>
<Checkbox value="taxi">Drosje</Checkbox>
<Checkbox value="public">Kollektivt</Checkbox>
</CheckboxGroup>
);
};
@@ -27,4 +28,5 @@ export const Demo = {
export const args = {
index: 99,
desc: "Vi fraråder bruk av disabled state. Vurder om du trenger å vise feltet i det hele tatt, om du heller kan bruke 'readOnly', eller bare kan skrive det ut i ren tekst.",
};
@@ -2,17 +2,17 @@ import { Checkbox, CheckboxGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any[]) => console.log(val);
const handleChange = (val: string[]) => console.log(val);
return (
<CheckboxGroup
legend="Hvor vil du sitte?"
onChange={(val: any[]) => handleChange(val)}
error="Feilmelding"
legend="Transportmiddel"
onChange={handleChange}
error="Du må velge minst ett transportmiddel."
>
<Checkbox value="Bakerst">Bakerst</Checkbox>
<Checkbox value="Midterst">Midterst</Checkbox>
<Checkbox value="Fremst">Fremst</Checkbox>
<Checkbox value="car">Bil</Checkbox>
<Checkbox value="taxi">Drosje</Checkbox>
<Checkbox value="public">Kollektivt</Checkbox>
</CheckboxGroup>
);
};
@@ -3,8 +3,8 @@ import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
return (
<Checkbox hideLabel value="Midterst">
Midterst
<Checkbox hideLabel value="car">
Bil
</Checkbox>
);
};
@@ -19,4 +19,5 @@ export const Demo = {
export const args = {
index: 3,
desc: "Label må være meningsfull selv om den skjules, siden den fortsatt leses av skjermlesere.",
};
@@ -2,17 +2,13 @@ import { Checkbox, CheckboxGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any[]) => console.log(val);
const handleChange = (val: string[]) => console.log(val);
return (
<CheckboxGroup
legend="Hvor vil du sitte?"
hideLegend
onChange={(val: any[]) => handleChange(val)}
>
<Checkbox value="Bakerst">Bakerst</Checkbox>
<Checkbox value="Midterst">Midterst</Checkbox>
<Checkbox value="Fremst">Fremst</Checkbox>
<CheckboxGroup legend="Transportmiddel" hideLegend onChange={handleChange}>
<Checkbox value="car">Bil</Checkbox>
<Checkbox value="taxi">Drosje</Checkbox>
<Checkbox value="public">Kollektivt</Checkbox>
</CheckboxGroup>
);
};
@@ -27,4 +23,5 @@ export const Demo = {
export const args = {
index: 4,
desc: "Legend må være meningsfull selv om den skjules, siden den fortsatt leses av skjermlesere.",
};
@@ -1,23 +1,105 @@
import { Checkbox, CheckboxGroup } from "@navikt/ds-react";
import { useState } from "react";
import { Checkbox, Table } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any[]) => console.log(val);
const [selectedRows, setSelectedRows] = useState<string[]>(["18124441438"]);
const toggleSelectedRow = (value) =>
setSelectedRows((list) =>
list.includes(value)
? list.filter((id) => id !== value)
: [...list, value],
);
return (
<CheckboxGroup
legend="Hvor vil du sitte?"
onChange={(val: any[]) => handleChange(val)}
>
<Checkbox value="Bakerst">Bakerst</Checkbox>
<Checkbox value="Midterst" indeterminate>
Midterst
</Checkbox>
<Checkbox value="Fremst">Fremst</Checkbox>
</CheckboxGroup>
<Table>
<Table.Header>
<Table.Row>
<Table.DataCell>
<Checkbox
checked={selectedRows.length === data.length}
indeterminate={
selectedRows.length > 0 && selectedRows.length !== data.length
}
onChange={() => {
selectedRows.length
? setSelectedRows([])
: setSelectedRows(data.map(({ fnr }) => fnr));
}}
hideLabel
>
Velg alle rader
</Checkbox>
</Table.DataCell>
<Table.HeaderCell scope="col">Navn</Table.HeaderCell>
<Table.HeaderCell scope="col">Fødselsnr.</Table.HeaderCell>
<Table.HeaderCell scope="col">Start</Table.HeaderCell>
</Table.Row>
</Table.Header>
<Table.Body>
{data.map(({ name, fnr, start }, i) => {
return (
<Table.Row key={i + fnr} selected={selectedRows.includes(fnr)}>
<Table.DataCell>
<Checkbox
hideLabel
checked={selectedRows.includes(fnr)}
onChange={() => toggleSelectedRow(fnr)}
aria-labelledby={`id-${fnr}`}
>
{" "}
</Checkbox>
</Table.DataCell>
<Table.HeaderCell scope="row">
<span id={`id-${fnr}`}>{name}</span>
</Table.HeaderCell>
<Table.DataCell>{fnr}</Table.DataCell>
<Table.DataCell>{format(new Date(start))}</Table.DataCell>
</Table.Row>
);
})}
</Table.Body>
</Table>
);
};
const format = (date: Date) => {
const y = date.getFullYear();
const m = (date.getMonth() + 1).toString().padStart(2, "0");
const d = date.getDate().toString().padStart(2, "0");
return `${d}.${m}.${y}`;
};
const data = [
{
name: "Jakobsen, Markus",
fnr: "03129265463",
start: "2020-04-28T19:12:14.358Z",
},
{
name: "Halvorsen, Mari",
fnr: "16063634134",
start: "2022-01-29T09:51:19.833Z",
},
{
name: "Christiansen, Mathias",
fnr: "18124441438",
start: "2021-06-04T20:57:29.159Z",
},
{
name: "Fredriksen, Leah",
fnr: "24089080180",
start: "2015-08-31T15:47:36.293Z",
},
{
name: "Evensen, Jonas",
fnr: "18106248460",
start: "2010-07-17T11:13:26.116Z",
},
];
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
export default withDsExample(Example);
@@ -2,16 +2,13 @@ import { Checkbox, CheckboxGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any[]) => console.log(val);
const handleChange = (val: string[]) => console.log(val);
return (
<CheckboxGroup
legend="Hvor vil du sitte?"
onChange={(val: any[]) => handleChange(val)}
>
<Checkbox value="Bakerst">Bakerst</Checkbox>
<Checkbox value="Midterst">Midterst</Checkbox>
<Checkbox value="Fremst">Fremst</Checkbox>
<CheckboxGroup legend="Transportmiddel" onChange={handleChange}>
<Checkbox value="car">Bil</Checkbox>
<Checkbox value="taxi">Drosje</Checkbox>
<Checkbox value="public">Kollektivt</Checkbox>
</CheckboxGroup>
);
};
@@ -2,18 +2,18 @@ import { Checkbox, CheckboxGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any[]) => console.log(val);
const handleChange = (val: string[]) => console.log(val);
return (
<CheckboxGroup
legend="Hvor vil du sitte?"
onChange={(val: any[]) => handleChange(val)}
value={["Midterst"]}
legend="Transportmiddel"
onChange={handleChange}
value={["taxi"]}
readOnly
>
<Checkbox value="Bakerst">Bakerst</Checkbox>
<Checkbox value="Midterst">Midterst</Checkbox>
<Checkbox value="Fremst">Fremst</Checkbox>
<Checkbox value="car">Bil</Checkbox>
<Checkbox value="taxi">Drosje</Checkbox>
<Checkbox value="public">Kollektivt</Checkbox>
</CheckboxGroup>
);
};
@@ -28,4 +28,5 @@ export const Demo = {
export const args = {
index: 98,
desc: "Readonly-attributtet gjør at valgene ikke kan endres, men brukere vil fortsatt kunne markere og kopiere teksten. Til forskjell fra disabled vil brukere også kunne tabbe til det, og feltet vil inkluderes når skjemaet sendes inn.",
};
@@ -1,31 +0,0 @@
import { Checkbox, CheckboxGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any[]) => console.log(val);
return (
<CheckboxGroup
legend="Hvor vil du sitte?"
onChange={(val: any[]) => handleChange(val)}
>
<Checkbox value="Bakerst">Bakerst</Checkbox>
<Checkbox error value="Midterst">
Midterst
</Checkbox>
<Checkbox value="Fremst">Fremst</Checkbox>
</CheckboxGroup>
);
};
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
export default withDsExample(Example);
/* Storybook story */
export const Demo = {
render: Example,
};
export const args = {
index: 7,
};
@@ -2,17 +2,17 @@ import { Checkbox, CheckboxGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any[]) => console.log(val);
const handleChange = (val: string[]) => console.log(val);
return (
<CheckboxGroup
legend="Hvor vil du sitte?"
onChange={(val: any[]) => handleChange(val)}
legend="Transportmiddel"
onChange={handleChange}
size="small"
>
<Checkbox value="Bakerst">Bakerst</Checkbox>
<Checkbox value="Midterst">Midterst</Checkbox>
<Checkbox value="Fremst">Fremst</Checkbox>
<Checkbox value="car">Bil</Checkbox>
<Checkbox value="taxi">Drosje</Checkbox>
<Checkbox value="public">Kollektivt</Checkbox>
</CheckboxGroup>
);
};
@@ -27,4 +27,5 @@ export const Demo = {
export const args = {
index: 1,
desc: "Small brukes på interne flater der det er behov for et mer komprimert grensesnitt.",
};
@@ -41,7 +41,10 @@ const Example = () => {
as="li"
key={index}
file={file.file}
onDelete={() => removeFile(file)}
button={{
action: "delete",
onClick: () => removeFile(file),
}}
/>
))}
</VStack>
@@ -59,7 +62,10 @@ const Example = () => {
key={index}
file={rejected.file}
error={errors[rejected.reasons[0]]}
onDelete={() => removeFile(rejected)}
button={{
action: "delete",
onClick: () => removeFile(rejected),
}}
/>
))}
</VStack>
@@ -7,8 +7,10 @@ const Example = () => (
<Heading size="small">retry</Heading>
<UNSAFE_FileUpload.Item
file={new File(["abc".repeat(100000)], "eksempel1.pdf")}
onRetry={() => null}
itemAction="retry"
button={{
action: "retry",
onClick: () => null,
}}
/>
</div>
<div>
@@ -16,8 +18,10 @@ const Example = () => (
<UNSAFE_FileUpload.Item
file={{ name: "eksempel2.pdf", size: 200000 }}
href="#"
onDelete={() => null}
itemAction="delete"
button={{
action: "delete",
onClick: () => null,
}}
/>
</div>
</VStack>
@@ -17,7 +17,10 @@ const Example = () => {
<UNSAFE_FileUpload.Item
key={file.file.name}
file={file.file}
onDelete={() => setFiles([])}
button={{
action: "delete",
onClick: () => setFiles([]),
}}
/>
))}
</VStack>
@@ -2,12 +2,12 @@ import { Radio, RadioGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any) => console.log(val);
const handleChange = (val: string) => console.log(val);
return (
<RadioGroup
legend="Velg din aldersgruppe."
onChange={(val: any) => handleChange(val)}
onChange={handleChange}
description="Informasjonen blir brukt for å gi deg bedre søketreff."
>
<Radio value="10">10-20 år</Radio>
@@ -2,12 +2,12 @@ import { Radio, RadioGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any) => console.log(val);
const handleChange = (val: string) => console.log(val);
return (
<RadioGroup
legend="Velg din aldersgruppe."
onChange={(val: any) => handleChange(val)}
onChange={handleChange}
disabled
>
<Radio value="10">10-20 år</Radio>
@@ -27,4 +27,5 @@ export const Demo = {
export const args = {
index: 99,
desc: "Vi fraråder bruk av disabled state. Vurder om du trenger å vise feltet i det hele tatt, om du heller kan bruke 'readOnly', eller bare kan skrive det ut i ren tekst.",
};
@@ -1,14 +1,21 @@
import { useState } from "react";
import { Radio, RadioGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any) => console.log(val);
const [error, setError] = useState(
"Du må velge en aldersgruppe før du kan gå videre.",
);
const handleChange = (val: string) => {
console.log(val);
setError("");
};
return (
<RadioGroup
legend="Velg din aldersgruppe."
onChange={(val: any) => handleChange(val)}
error="Vi fant ingen resultater på din aldersgruppe."
onChange={handleChange}
error={error}
>
<Radio value="10">10-20 år</Radio>
<Radio value="20">21-45 år</Radio>
@@ -2,12 +2,12 @@ import { Radio, RadioGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any) => console.log(val);
const handleChange = (val: string) => console.log(val);
return (
<RadioGroup
legend="Velg din aldersgruppe."
onChange={(val: any) => handleChange(val)}
onChange={handleChange}
hideLegend
>
<Radio value="10">10-20 år</Radio>
@@ -27,4 +27,5 @@ export const Demo = {
export const args = {
index: 4,
desc: "Legend må være meningsfull selv om den skjules, siden den fortsatt leses av skjermlesere.",
};
@@ -2,13 +2,10 @@ import { Radio, RadioGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any) => console.log(val);
const handleChange = (val: string) => console.log(val);
return (
<RadioGroup
legend="Velg din aldersgruppe."
onChange={(val: any) => handleChange(val)}
>
<RadioGroup legend="Velg din aldersgruppe." onChange={handleChange}>
<Radio value="10">10-20 år</Radio>
<Radio value="20">21-45 år</Radio>
<Radio value="40">46-80 år</Radio>
@@ -2,13 +2,10 @@ import { Radio, RadioGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any) => console.log(val);
const handleChange = (val: string) => console.log(val);
return (
<RadioGroup
legend="Velg din aldersgruppe."
onChange={(val: any) => handleChange(val)}
>
<RadioGroup legend="Velg din aldersgruppe." onChange={handleChange}>
<Radio value="10">10-20 år</Radio>
<Radio value="20" description="Gjelder fra året man blir 21">
21-45 år
@@ -2,12 +2,12 @@ import { Radio, RadioGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any) => console.log(val);
const handleChange = (val: string) => console.log(val);
return (
<RadioGroup
legend="Velg din aldersgruppe."
onChange={(val: any) => handleChange(val)}
onChange={handleChange}
value="20"
readOnly
>
@@ -28,4 +28,5 @@ export const Demo = {
export const args = {
index: 98,
desc: "Readonly-attributtet gjør at valget ikke kan endres, men brukere vil fortsatt kunne markere og kopiere teksten. Til forskjell fra disabled vil brukere også kunne tabbe til det, og feltet vil inkluderes når skjemaet sendes inn.",
};
@@ -2,12 +2,12 @@ import { Radio, RadioGroup } from "@navikt/ds-react";
import { withDsExample } from "@/web/examples/withDsExample";
const Example = () => {
const handleChange = (val: any) => console.log(val);
const handleChange = (val: string) => console.log(val);
return (
<RadioGroup
legend="Velg din aldersgruppe."
onChange={(val: any) => handleChange(val)}
onChange={handleChange}
size="small"
>
<Radio value="10">10-20 år</Radio>
@@ -27,4 +27,5 @@ export const Demo = {
export const args = {
index: 1,
desc: "Small brukes på interne flater der det er behov for et mer komprimert grensesnitt.",
};
Loaded 100 of 111 files, more files were not shown because too many files have changed in this diff. Show more