mirror of
https://github.com/navikt/aksel.git
synced 2026-10-13 01:02:02 +00:00
Compare commits
29
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2364e689ed | ||
|
|
d57c0b2c2c | ||
|
|
868bb543a5 | ||
|
|
8f9ed7a404 | ||
|
|
5ea6255f6d | ||
|
|
491fccbe80 | ||
|
|
cca2a8263f | ||
|
|
3140ec2765 | ||
|
|
d8fce83ae3 | ||
|
|
f88b00daf6 | ||
|
|
6d0baea5c1 | ||
|
|
880e77b9c2 | ||
|
|
6d16373f85 | ||
|
|
ae078254dc | ||
|
|
638e8260b4 | ||
|
|
d991011690 | ||
|
|
b6086f7b1f | ||
|
|
05462081aa | ||
|
|
63e42de3ee | ||
|
|
2e85178166 | ||
|
|
456d4e2e07 | ||
|
|
b5a0cb8cdf | ||
|
|
a2af9bcc37 | ||
|
|
bd45754adb | ||
|
|
a882a8a389 | ||
|
|
d1606d358a | ||
|
|
bb87837632 | ||
|
|
9c8b2301ed | ||
|
|
54ba483d2d |
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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"
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @navikt/aksel-icons
|
||||
|
||||
## 6.4.0
|
||||
|
||||
## 6.3.6
|
||||
|
||||
## 6.3.5
|
||||
|
||||
## 6.3.4
|
||||
|
||||
## 6.3.3
|
||||
|
||||
## 6.3.2
|
||||
|
||||
## 6.3.1
|
||||
|
||||
@@ -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,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-icons",
|
||||
"version": "6.3.2",
|
||||
"version": "6.4.0",
|
||||
"author": "Aksel | NAV",
|
||||
"homepage": "https://aksel.nav.no/ikoner",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @navikt/aksel-stylelint
|
||||
|
||||
## 6.4.0
|
||||
|
||||
## 6.3.6
|
||||
|
||||
## 6.3.5
|
||||
|
||||
## 6.3.4
|
||||
|
||||
## 6.3.3
|
||||
|
||||
## 6.3.2
|
||||
|
||||
## 6.3.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-stylelint",
|
||||
"version": "6.3.2",
|
||||
"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.2",
|
||||
"@navikt/ds-tokens": "^6.3.2",
|
||||
"@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",
|
||||
|
||||
@@ -1,5 +1,40 @@
|
||||
# @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
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-css@6.3.3
|
||||
|
||||
## 6.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel",
|
||||
"version": "6.3.2",
|
||||
"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.2",
|
||||
"@navikt/ds-css": "6.4.0",
|
||||
"axios": "1.6.0",
|
||||
"chalk": "4.1.0",
|
||||
"clipboardy": "^2.3.0",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @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
|
||||
|
||||
## 6.3.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "6.3.2",
|
||||
"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.2",
|
||||
"@navikt/ds-tokens": "^6.4.0",
|
||||
"cssnano": "6.0.0",
|
||||
"fast-glob": "3.2.11",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -1,5 +1,61 @@
|
||||
# @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
|
||||
|
||||
- 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))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@6.3.3
|
||||
- @navikt/aksel-icons@6.3.3
|
||||
|
||||
## 6.3.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "6.3.2",
|
||||
"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.2",
|
||||
"@navikt/ds-tokens": "^6.3.2",
|
||||
"@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;
|
||||
/**
|
||||
|
||||
@@ -160,6 +160,7 @@ export const UseDatepicker = () => {
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
toDate: new Date("Feb 23 2024"),
|
||||
onDateChange: console.log,
|
||||
onValidate: console.log,
|
||||
});
|
||||
|
||||
return (
|
||||
|
||||
@@ -20,6 +20,7 @@ const DayButton = (props: DayProps) => {
|
||||
name="day"
|
||||
ref={buttonRef}
|
||||
{...dayRender.buttonProps}
|
||||
role={undefined}
|
||||
aria-label={dateTime}
|
||||
aria-hidden={dayRender.activeModifiers.outside}
|
||||
/>
|
||||
|
||||
@@ -272,7 +272,7 @@ export const useDatepicker = (
|
||||
) {
|
||||
updateDate(undefined);
|
||||
updateValidation({
|
||||
isInvalid: isValidDate(day),
|
||||
isInvalid: !isValidDate(day),
|
||||
isWeekend: disableWeekends && isWeekend(day),
|
||||
isDisabled: disabled && isMatch(day, disabled),
|
||||
isValidDate: false,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta } from "@storybook/react";
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "../button";
|
||||
import Dropdown from "./Dropdown";
|
||||
@@ -6,10 +6,15 @@ import Dropdown from "./Dropdown";
|
||||
export default {
|
||||
title: "ds-react/Dropdown",
|
||||
component: Dropdown,
|
||||
} as Meta;
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
} satisfies Meta<typeof Dropdown>;
|
||||
|
||||
export const Default = () => {
|
||||
return (
|
||||
type Story = StoryObj<typeof Dropdown>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => (
|
||||
<Dropdown onSelect={(event) => console.log(event)}>
|
||||
<Dropdown.Toggle>Toggle</Dropdown.Toggle>
|
||||
<Dropdown.Menu strategy="fixed">
|
||||
@@ -33,10 +38,10 @@ export const Default = () => {
|
||||
</Dropdown.Menu.List>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
),
|
||||
};
|
||||
|
||||
export const DefaultOpen = {
|
||||
export const DefaultOpen: Story = {
|
||||
render: () => (
|
||||
<Dropdown onSelect={(event) => console.log(event)} defaultOpen>
|
||||
<Button as={Dropdown.Toggle}>Toggle</Button>
|
||||
@@ -57,20 +62,41 @@ export const DefaultOpen = {
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
),
|
||||
args: { chromatic: { delay: 300 } },
|
||||
};
|
||||
|
||||
export const ControlledOpen = () => {
|
||||
const [openState, setOpenState] = useState(true);
|
||||
return (
|
||||
<Dropdown onSelect={(event) => console.log(event)} open={openState}>
|
||||
<Button as={Dropdown.Toggle} onClick={() => setOpenState(!openState)}>
|
||||
Toggle
|
||||
</Button>
|
||||
<Dropdown.Menu
|
||||
strategy="fixed"
|
||||
onClose={() => console.log("ONCLOSE CONTROLLED")}
|
||||
>
|
||||
export const ControlledOpen: Story = {
|
||||
render: () => {
|
||||
const [openState, setOpenState] = useState(true);
|
||||
return (
|
||||
<Dropdown onSelect={(event) => console.log(event)} open={openState}>
|
||||
<Button as={Dropdown.Toggle} onClick={() => setOpenState(!openState)}>
|
||||
Toggle
|
||||
</Button>
|
||||
<Dropdown.Menu
|
||||
strategy="fixed"
|
||||
onClose={() => console.log("ONCLOSE CONTROLLED")}
|
||||
>
|
||||
<Dropdown.Menu.GroupedList>
|
||||
<Dropdown.Menu.GroupedList.Heading>
|
||||
Systemer og oppslagsverk
|
||||
</Dropdown.Menu.GroupedList.Heading>
|
||||
<Dropdown.Menu.GroupedList.Item
|
||||
onClick={() => console.log("GroupedList.Item-click")}
|
||||
>
|
||||
Gosys
|
||||
</Dropdown.Menu.GroupedList.Item>
|
||||
</Dropdown.Menu.GroupedList>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Chromatic: Story = {
|
||||
render: () => (
|
||||
<Dropdown onSelect={(event) => console.log(event)} open>
|
||||
<Dropdown.Toggle>Toggle</Dropdown.Toggle>
|
||||
<Dropdown.Menu strategy="fixed">
|
||||
<Dropdown.Menu.GroupedList>
|
||||
<Dropdown.Menu.GroupedList.Heading>
|
||||
Systemer og oppslagsverk
|
||||
@@ -81,8 +107,18 @@ export const ControlledOpen = () => {
|
||||
Gosys
|
||||
</Dropdown.Menu.GroupedList.Item>
|
||||
</Dropdown.Menu.GroupedList>
|
||||
<Dropdown.Menu.Divider />
|
||||
<Dropdown.Menu.List>
|
||||
<Dropdown.Menu.List.Item onClick={() => console.log("Item-click")}>
|
||||
Gosys
|
||||
</Dropdown.Menu.List.Item>
|
||||
<Dropdown.Menu.List.Item>Psys</Dropdown.Menu.List.Item>
|
||||
<Dropdown.Menu.List.Item disabled>Infotrygd</Dropdown.Menu.List.Item>
|
||||
</Dropdown.Menu.List>
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
);
|
||||
),
|
||||
parameters: {
|
||||
chromatic: { disable: false, delay: 300 },
|
||||
},
|
||||
};
|
||||
ControlledOpen.args = { chromatic: { delay: 300 } };
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Meta } from "@storybook/react";
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { VStack } from "../../layout/stack";
|
||||
import { ErrorSummary } from "./ErrorSummary";
|
||||
|
||||
export default {
|
||||
@@ -18,9 +19,14 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
} satisfies Meta<typeof ErrorSummary>;
|
||||
|
||||
export const Default = {
|
||||
type Story = StoryObj<typeof ErrorSummary>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: (props) => (
|
||||
<ErrorSummary
|
||||
heading="Feiloppsummering komponent"
|
||||
@@ -35,11 +41,41 @@ export const Default = {
|
||||
),
|
||||
};
|
||||
|
||||
export const Small = () => (
|
||||
<ErrorSummary heading="Feiloppsummering komponent" size="small">
|
||||
<ErrorSummary.Item href="#1">Checkbox må fylles ut</ErrorSummary.Item>
|
||||
<ErrorSummary.Item href="#2">
|
||||
Tekstfeltet må ha en godkjent e-mail
|
||||
</ErrorSummary.Item>
|
||||
</ErrorSummary>
|
||||
);
|
||||
export const Small: Story = {
|
||||
render: () => (
|
||||
<ErrorSummary heading="Feiloppsummering komponent" size="small">
|
||||
<ErrorSummary.Item href="#1">Checkbox må fylles ut</ErrorSummary.Item>
|
||||
<ErrorSummary.Item href="#2">
|
||||
Tekstfeltet må ha en godkjent e-mail
|
||||
</ErrorSummary.Item>
|
||||
</ErrorSummary>
|
||||
),
|
||||
};
|
||||
|
||||
export const Chromatic: Story = {
|
||||
render: () => (
|
||||
<VStack gap="4">
|
||||
<div>
|
||||
<h2>Default</h2>
|
||||
<ErrorSummary heading="Feiloppsummering komponent">
|
||||
<ErrorSummary.Item href="#1">Checkbox må fylles ut</ErrorSummary.Item>
|
||||
<ErrorSummary.Item href="#2">
|
||||
Tekstfeltet må ha en godkjent e-mail
|
||||
</ErrorSummary.Item>
|
||||
</ErrorSummary>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Small</h2>
|
||||
<ErrorSummary heading="Feiloppsummering komponent" size="small">
|
||||
<ErrorSummary.Item href="#1">Checkbox må fylles ut</ErrorSummary.Item>
|
||||
<ErrorSummary.Item href="#2">
|
||||
Tekstfeltet må ha en godkjent e-mail
|
||||
</ErrorSummary.Item>
|
||||
</ErrorSummary>
|
||||
</div>
|
||||
</VStack>
|
||||
),
|
||||
parameters: {
|
||||
chromatic: { disable: false },
|
||||
},
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Meta } from "@storybook/react";
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { VStack } from "../../layout/stack";
|
||||
import TextField from "../textfield/TextField";
|
||||
import { Fieldset } from "./index";
|
||||
|
||||
@@ -26,7 +27,12 @@ export default {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
} satisfies Meta<typeof Fieldset>;
|
||||
|
||||
type Story = StoryObj<typeof Fieldset>;
|
||||
|
||||
const content = (
|
||||
<>
|
||||
@@ -35,88 +41,117 @@ const content = (
|
||||
</>
|
||||
);
|
||||
|
||||
export const Default = {
|
||||
render: (props) => {
|
||||
return (
|
||||
<Fieldset legend="Mollit eiusmod" {...props}>
|
||||
{content}
|
||||
</Fieldset>
|
||||
);
|
||||
},
|
||||
const contentWithError = (
|
||||
<>
|
||||
<TextField label="Textfield label" hideLabel error="Må være fylt ut" />
|
||||
<TextField label="Textfield label" hideLabel />
|
||||
</>
|
||||
);
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
legend: "Mollit eiusmod",
|
||||
description:
|
||||
"Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure.",
|
||||
errorPropagation: true,
|
||||
children: content,
|
||||
},
|
||||
};
|
||||
|
||||
export const Small = () => (
|
||||
<Fieldset
|
||||
size="small"
|
||||
legend="Mollit eiusmod"
|
||||
description="Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure."
|
||||
>
|
||||
{content}
|
||||
</Fieldset>
|
||||
);
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
legend: "Mollit eiusmod",
|
||||
description:
|
||||
"Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure.",
|
||||
errorPropagation: true,
|
||||
children: content,
|
||||
size: "small",
|
||||
},
|
||||
};
|
||||
|
||||
export const Description = () => (
|
||||
<Fieldset
|
||||
legend="Mollit eiusmod"
|
||||
description="Esse cupidatat reprehenderit est culpa consectetur sit dolor esse."
|
||||
>
|
||||
<TextField
|
||||
label="Textfield label"
|
||||
description="Amet quis cillum incididunt "
|
||||
/>
|
||||
<TextField
|
||||
label="Textfield label"
|
||||
description="Enim et occaecat voluptate labore sit do exercitation laborum non "
|
||||
/>
|
||||
</Fieldset>
|
||||
);
|
||||
export const ErrorPropagation: Story = {
|
||||
args: {
|
||||
legend: "Mollit eiusmod",
|
||||
description:
|
||||
"Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure.",
|
||||
errorPropagation: false,
|
||||
children: contentWithError,
|
||||
},
|
||||
};
|
||||
|
||||
export const ErrorPropagation = () => (
|
||||
<Fieldset
|
||||
legend="Mollit eiusmod"
|
||||
error="Fieldsett error"
|
||||
errorPropagation={false}
|
||||
>
|
||||
<TextField label="Textfield label" hideLabel error="Må være fylt ut" />
|
||||
<TextField label="Textfield label" hideLabel />
|
||||
</Fieldset>
|
||||
);
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
legend: "Mollit eiusmod",
|
||||
description:
|
||||
"Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure.",
|
||||
children: content,
|
||||
error: "Laborum officia nisi aliqua esse minim in amet.",
|
||||
},
|
||||
};
|
||||
|
||||
export const Error = () => (
|
||||
<div className="colgap">
|
||||
<Fieldset
|
||||
legend="Mollit eiusmod"
|
||||
error="Laborum officia nisi aliqua esse minim in amet."
|
||||
>
|
||||
{content}
|
||||
</Fieldset>
|
||||
<Fieldset
|
||||
size="small"
|
||||
legend="Mollit eiusmod"
|
||||
error="Laborum officia nisi aliqua esse minim in amet."
|
||||
>
|
||||
{content}
|
||||
</Fieldset>
|
||||
</div>
|
||||
);
|
||||
export const Disabled: Story = {
|
||||
args: {
|
||||
legend: "Mollit eiusmod",
|
||||
description:
|
||||
"Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure.",
|
||||
children: content,
|
||||
disabled: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = () => (
|
||||
<div className="colgap">
|
||||
<Fieldset legend="Mollit eiusmod" disabled>
|
||||
{content}
|
||||
</Fieldset>
|
||||
<Fieldset size="small" legend="Mollit eiusmod" disabled>
|
||||
{content}
|
||||
</Fieldset>
|
||||
</div>
|
||||
);
|
||||
export const HideLegend: Story = {
|
||||
args: {
|
||||
legend: "Mollit eiusmod",
|
||||
description:
|
||||
"Do ullamco amet mollit labore tempor minim cupidatat dolore voluptate velit irure.",
|
||||
children: content,
|
||||
hideLegend: true,
|
||||
},
|
||||
};
|
||||
|
||||
export const HideLegend = () => (
|
||||
<Fieldset legend="Mollit eiusmod" hideLegend>
|
||||
{content}
|
||||
</Fieldset>
|
||||
);
|
||||
export const Chromatic: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<VStack gap="4">
|
||||
<div>
|
||||
<h2>Default</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Fieldset {...Default.args} />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Small</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Fieldset {...Small.args} />
|
||||
</div>
|
||||
<div>
|
||||
<h2>ErrorPropagation</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Fieldset {...ErrorPropagation.args} />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Error</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Fieldset {...Error.args} />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Error small</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Fieldset {...Error.args} size="small" />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Disabled</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Fieldset {...Disabled.args} />
|
||||
</div>
|
||||
<div>
|
||||
<h2>HideLegend</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Fieldset {...HideLegend.args} />
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
},
|
||||
parameters: {
|
||||
chromatic: { disable: false },
|
||||
},
|
||||
};
|
||||
@@ -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,
|
||||
@@ -131,6 +135,7 @@ const Dropzone = forwardRef<HTMLInputElement, FileUploadDropzoneProps>(
|
||||
<Button
|
||||
{...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
|
||||
|
||||
@@ -49,6 +49,7 @@ export const Default = (props) => {
|
||||
</RadioGroup>
|
||||
);
|
||||
};
|
||||
|
||||
Default.args = {
|
||||
controlled: false,
|
||||
error: false,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Meta } from "@storybook/react";
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { VStack } from "../../layout/stack";
|
||||
import Switch from "./Switch";
|
||||
|
||||
export default {
|
||||
@@ -31,70 +32,59 @@ export default {
|
||||
type: "boolean",
|
||||
},
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
export const Default = {
|
||||
render: (props) => {
|
||||
return <Switch {...props}>Label text</Switch>;
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
} satisfies Meta<typeof Switch>;
|
||||
|
||||
type Story = StoryObj<typeof Switch>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
position: "right",
|
||||
children: "Label text",
|
||||
},
|
||||
};
|
||||
|
||||
export const Small = () => {
|
||||
return (
|
||||
<div className="colgap">
|
||||
<Switch size="small">Label text</Switch>
|
||||
<Switch size="small" position="right">
|
||||
Label text
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
export const Small: Story = {
|
||||
args: {
|
||||
children: "Label text",
|
||||
size: "small",
|
||||
},
|
||||
};
|
||||
|
||||
export const Description = () => {
|
||||
return (
|
||||
<div className="colgap">
|
||||
<Switch size="small" description="Cillum sint exercitation ut cillum.">
|
||||
Label text
|
||||
</Switch>
|
||||
<Switch
|
||||
size="small"
|
||||
position="right"
|
||||
description="Cillum sint exercitation ut cillum."
|
||||
>
|
||||
Label text
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
export const Description: Story = {
|
||||
args: {
|
||||
children: "Label text",
|
||||
description: "Cillum sint exercitation ut cillum.",
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading = () => {
|
||||
return (
|
||||
<div className="colgap">
|
||||
export const Loading: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
<div className="colgap">
|
||||
<Switch loading>Label text</Switch>
|
||||
<div className="colgap">
|
||||
<Switch loading>Label text</Switch>
|
||||
|
||||
<Switch checked loading>
|
||||
Label text
|
||||
</Switch>
|
||||
<Switch checked loading>
|
||||
Label text
|
||||
</Switch>
|
||||
</div>
|
||||
<div className="colgap">
|
||||
<Switch loading size="small">
|
||||
Label text
|
||||
</Switch>
|
||||
<Switch checked loading size="small">
|
||||
Label text
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<div className="colgap">
|
||||
<Switch loading size="small">
|
||||
Label text
|
||||
</Switch>
|
||||
<Switch checked loading size="small">
|
||||
Label text
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Disabled = () => {
|
||||
return (
|
||||
export const Disabled: Story = {
|
||||
render: () => (
|
||||
<div className="colgap">
|
||||
<Switch disabled>Label text</Switch>
|
||||
|
||||
@@ -102,11 +92,11 @@ export const Disabled = () => {
|
||||
Label text
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
),
|
||||
};
|
||||
|
||||
export const HideLabel = () => {
|
||||
return (
|
||||
export const HideLabel: Story = {
|
||||
render: () => (
|
||||
<div className="colgap">
|
||||
<Switch hideLabel>Label text</Switch>
|
||||
|
||||
@@ -114,11 +104,11 @@ export const HideLabel = () => {
|
||||
Label text
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
),
|
||||
};
|
||||
|
||||
export const Readonly = () => {
|
||||
return (
|
||||
export const Readonly: Story = {
|
||||
render: () => (
|
||||
<div className="colgap">
|
||||
<Switch description="Slår av alle notifikasjoner" readOnly>
|
||||
Notifikasjoner
|
||||
@@ -128,5 +118,54 @@ export const Readonly = () => {
|
||||
Notifikasjoner
|
||||
</Switch>
|
||||
</div>
|
||||
);
|
||||
),
|
||||
};
|
||||
|
||||
export const Chromatic: Story = {
|
||||
render: () => (
|
||||
<VStack gap="4" align="start">
|
||||
<div>
|
||||
<h2>Default</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Switch {...Default.args} />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Small</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Switch {...Small.args} position="left" />
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Switch {...Small.args} position="right" />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Description</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Switch {...Description.args} position="left" />
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Switch {...Description.args} position="right" />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Loading</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Loading.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Disabled</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Disabled.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>HideLabel</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<HideLabel.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Readonly</h2>
|
||||
{/* @ts-expect-error Args are Partial here */}
|
||||
<Readonly.render />
|
||||
</div>
|
||||
</VStack>
|
||||
),
|
||||
parameters: {
|
||||
chromatic: { disable: false },
|
||||
},
|
||||
};
|
||||
@@ -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 (<480px) and the non-poster variant otherwise.
|
||||
If you exclude the `poster` prop, you will get the poster variant on
|
||||
mobile (<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>
|
||||
),
|
||||
|
||||
@@ -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'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'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";
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @navikt/ds-tailwind
|
||||
|
||||
## 6.4.0
|
||||
|
||||
## 6.3.6
|
||||
|
||||
## 6.3.5
|
||||
|
||||
## 6.3.4
|
||||
|
||||
## 6.3.3
|
||||
|
||||
## 6.3.2
|
||||
|
||||
## 6.3.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tailwind",
|
||||
"version": "6.3.2",
|
||||
"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.2",
|
||||
"@navikt/ds-tokens": "^6.4.0",
|
||||
"color": "4.2.3",
|
||||
"lodash": "^4.17.21",
|
||||
"tailwindcss": "^3.3.3",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @navikt/ds-tokens
|
||||
|
||||
## 6.4.0
|
||||
|
||||
## 6.3.6
|
||||
|
||||
## 6.3.5
|
||||
|
||||
## 6.3.4
|
||||
|
||||
## 6.3.3
|
||||
|
||||
## 6.3.2
|
||||
|
||||
## 6.3.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tokens",
|
||||
"version": "6.3.2",
|
||||
"version": "6.4.0",
|
||||
"description": "Design-tokens for NAV designsystem",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
|
||||
+116
-96
@@ -1,13 +1,33 @@
|
||||
# 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))
|
||||
- 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))
|
||||
|
||||
### @navikt/ds-react
|
||||
|
||||
- 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))
|
||||
@@ -18,16 +38,16 @@
|
||||
|
||||
## 6.2
|
||||
|
||||
### @navikt/ds-react
|
||||
|
||||
- Select: Fjern 'multiple' fra SelectProps ([#2782](https://github.com/navikt/aksel/pull/2782))
|
||||
|
||||
### @navikt/aksel-icons
|
||||
|
||||
- Ikoner: Nytt ikon `archive` ([#2772](https://github.com/navikt/aksel/pull/2772))
|
||||
|
||||
- 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
|
||||
@@ -50,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
|
||||
|
||||
@@ -66,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))
|
||||
@@ -94,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
|
||||
|
||||
@@ -148,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
|
||||
@@ -214,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))
|
||||
@@ -234,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
|
||||
|
||||
@@ -280,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))
|
||||
@@ -298,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))
|
||||
@@ -360,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))
|
||||
@@ -400,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
|
||||
|
||||
@@ -456,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
|
||||
@@ -504,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))
|
||||
@@ -540,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
|
||||
|
||||
@@ -562,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
|
||||
@@ -775,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))
|
||||
@@ -821,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
|
||||
|
||||
@@ -835,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))
|
||||
@@ -869,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
|
||||
@@ -925,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))
|
||||
@@ -943,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
|
||||
@@ -979,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))
|
||||
@@ -991,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
|
||||
|
||||
@@ -1037,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`
|
||||
@@ -1065,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
|
||||
@@ -1161,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))
|
||||
@@ -1177,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))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
"playroom"
|
||||
],
|
||||
"dependencies": {
|
||||
"@navikt/aksel-icons": "^6.3.2",
|
||||
"@navikt/ds-css": "^6.3.2",
|
||||
"@navikt/ds-react": "^6.3.2",
|
||||
"@navikt/ds-tailwind": "^6.3.2",
|
||||
"@navikt/ds-tokens": "^6.3.2"
|
||||
"@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"
|
||||
}
|
||||
}
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -22,7 +22,7 @@ describe("Utils test", () => {
|
||||
expect(capitalize(str4)).toEqual("Teststr");
|
||||
});
|
||||
|
||||
test("dateStr", async () => {
|
||||
test("dateStr", { retry: 10, timeout: 8000 }, async () => {
|
||||
const date = "2022-06-09T11:05:48Z";
|
||||
const date2 = "2021-03-02T12:05:48Z";
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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,
|
||||
};
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
+5
-8
@@ -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>
|
||||
|
||||
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user