mirror of
https://github.com/navikt/aksel.git
synced 2026-10-14 01:32:02 +00:00
Compare commits
35
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9bbc3e6813 | ||
|
|
5250b47ca4 | ||
|
|
fc7709ae2a | ||
|
|
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 | ||
|
|
85f5458c06 | ||
|
|
19abec80f7 | ||
|
|
b2ddce5279 |
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,19 @@
|
||||
# @navikt/aksel-icons
|
||||
|
||||
## 6.4.1
|
||||
|
||||
## 6.4.0
|
||||
|
||||
## 6.3.6
|
||||
|
||||
## 6.3.5
|
||||
|
||||
## 6.3.4
|
||||
|
||||
## 6.3.3
|
||||
|
||||
## 6.3.2
|
||||
|
||||
## 6.3.1
|
||||
|
||||
## 6.3.0
|
||||
|
||||
@@ -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.1",
|
||||
"version": "6.4.1",
|
||||
"author": "Aksel | NAV",
|
||||
"homepage": "https://aksel.nav.no/ikoner",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @navikt/aksel-stylelint
|
||||
|
||||
## 6.4.1
|
||||
|
||||
## 6.4.0
|
||||
|
||||
## 6.3.6
|
||||
|
||||
## 6.3.5
|
||||
|
||||
## 6.3.4
|
||||
|
||||
## 6.3.3
|
||||
|
||||
## 6.3.2
|
||||
|
||||
## 6.3.1
|
||||
|
||||
## 6.3.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-stylelint",
|
||||
"version": "6.3.1",
|
||||
"version": "6.4.1",
|
||||
"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.1",
|
||||
"@navikt/ds-tokens": "^6.3.1",
|
||||
"@navikt/ds-css": "^6.4.1",
|
||||
"@navikt/ds-tokens": "^6.4.1",
|
||||
"concurrently": "7.2.1",
|
||||
"postcss-selector-parser": "^6.0.13",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
|
||||
@@ -1,5 +1,54 @@
|
||||
# @navikt/aksel
|
||||
|
||||
## 6.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`5250b47ca`](https://github.com/navikt/aksel/commit/5250b47ca410f9cd57c5350bacc77b1b2ec9b2c0)]:
|
||||
- @navikt/ds-css@6.4.1
|
||||
|
||||
## 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
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-css@6.3.2
|
||||
|
||||
## 6.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel",
|
||||
"version": "6.3.1",
|
||||
"version": "6.4.1",
|
||||
"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.1",
|
||||
"@navikt/ds-css": "6.4.1",
|
||||
"axios": "1.6.0",
|
||||
"chalk": "4.1.0",
|
||||
"clipboardy": "^2.3.0",
|
||||
|
||||
@@ -1,5 +1,27 @@
|
||||
# @navikt/ds-css
|
||||
|
||||
## 6.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- FileUpload: Item-illustrasjon er nå radius-full ([#2834](https://github.com/navikt/aksel/pull/2834))
|
||||
|
||||
## 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
|
||||
|
||||
## 6.3.0
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
.navds-file-item__icon {
|
||||
background-color: var(--a-surface-neutral-subtle);
|
||||
color: var(--a-icon-default);
|
||||
border-radius: var(--a-border-radius-xlarge);
|
||||
border-radius: var(--a-border-radius-full);
|
||||
min-height: 3rem;
|
||||
min-width: 3rem;
|
||||
display: grid;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "6.3.1",
|
||||
"version": "6.4.1",
|
||||
"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.1",
|
||||
"@navikt/ds-tokens": "^6.4.1",
|
||||
"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,79 @@
|
||||
# @navikt/ds-react
|
||||
|
||||
## 6.4.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@6.4.1
|
||||
- @navikt/aksel-icons@6.4.1
|
||||
|
||||
## 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
|
||||
|
||||
- FileUpload: Fiks feil ved import av json ([#2803](https://github.com/navikt/aksel/pull/2803))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@6.3.2
|
||||
- @navikt/aksel-icons@6.3.2
|
||||
|
||||
## 6.3.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "6.3.1",
|
||||
"version": "6.4.1",
|
||||
"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.1",
|
||||
"@navikt/ds-tokens": "^6.3.1",
|
||||
"@navikt/aksel-icons": "^6.4.1",
|
||||
"@navikt/ds-tokens": "^6.4.1",
|
||||
"@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 labling 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 }}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
ComponentTranslation,
|
||||
TranslationDictionary,
|
||||
} from "./i18n.types";
|
||||
import nb from "./locales/nb.json";
|
||||
import nb from "./locales/nb";
|
||||
|
||||
/**
|
||||
* https://regex101.com/r/LYKWi3/1
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import nb from "./locales/nb.json";
|
||||
import nb from "./locales/nb";
|
||||
|
||||
export interface TranslationObject {
|
||||
[key: string]: string | TranslationObject | undefined;
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"FileUpload": {
|
||||
"dropzone": {
|
||||
"button": "Velg fil",
|
||||
"buttonMultiple": "Velg filer",
|
||||
"dragAndDrop": "Dra og slipp filen her",
|
||||
"dragAndDropMultiple": "Dra og slipp filer her",
|
||||
"drop": "Slipp",
|
||||
"or": "eller",
|
||||
"disabled": "Filopplasting er deaktivert",
|
||||
"disabledFilelimit": "Du kan ikke laste opp flere filer"
|
||||
},
|
||||
"item": {
|
||||
"retryButtonTitle": "Prøv å laste opp filen på nytt",
|
||||
"deleteButtonTitle": "Slett filen",
|
||||
"uploading": "Laster opp…",
|
||||
"downloading": "Laster ned…"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
export default {
|
||||
FileUpload: {
|
||||
dropzone: {
|
||||
button: "Velg fil",
|
||||
buttonMultiple: "Velg filer",
|
||||
dragAndDrop: "Dra og slipp filen her",
|
||||
dragAndDropMultiple: "Dra og slipp filer her",
|
||||
drop: "Slipp",
|
||||
or: "eller",
|
||||
disabled: "Filopplasting er deaktivert",
|
||||
disabledFilelimit: "Du kan ikke laste opp flere filer",
|
||||
},
|
||||
item: {
|
||||
retryButtonTitle: "Prøv å laste opp filen på nytt",
|
||||
deleteButtonTitle: "Slett filen",
|
||||
uploading: "Laster opp…",
|
||||
downloading: "Laster ned…",
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -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,24 +12,27 @@ 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.
|
||||
*/
|
||||
file: FileItem;
|
||||
/**
|
||||
* onClick on the file name.
|
||||
* @note If this and `href` is not set and the `file` prop is a native file, onClick will download the file.
|
||||
*
|
||||
* If neither this nor `href` is set, and the `file` prop is a native file, onClick will download the file.
|
||||
*/
|
||||
onFileClick?: (event: MouseEvent<HTMLAnchorElement>) => void;
|
||||
/**
|
||||
* href on the file name.
|
||||
* @note If this and `onFileClick` is not set and the `file` prop is a native file, onClick will download the file.
|
||||
*
|
||||
* If neither this nor `onFileClick` is set, and the `file` prop is a native file, onClick will download the file.
|
||||
*/
|
||||
href?: string;
|
||||
/**
|
||||
@@ -41,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(
|
||||
(
|
||||
@@ -79,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);
|
||||
@@ -146,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,
|
||||
|
||||
@@ -5,15 +5,15 @@ export interface RadioProps
|
||||
extends Omit<FormFieldProps, "error" | "errorId" | "readOnly">,
|
||||
Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "value"> {
|
||||
/**
|
||||
* Radio label
|
||||
* Radio label.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* The value of the HTML element
|
||||
* Radio value.
|
||||
*/
|
||||
value: any;
|
||||
/**
|
||||
* Adds a description to extend labling of Radio
|
||||
* Adds a description to extend the labeling.
|
||||
*/
|
||||
description?: string;
|
||||
}
|
||||
@@ -28,47 +28,51 @@ export interface SearchProps
|
||||
/**
|
||||
* Search label.
|
||||
*
|
||||
* Will be hidden by default, is required for accessibility reasons.
|
||||
* Will be hidden by default and is required for WCAG compliance.
|
||||
*/
|
||||
label: React.ReactNode;
|
||||
/**
|
||||
* Shows label and description for screenreaders-only
|
||||
* Shows label and description for screenreaders only.
|
||||
* @default true
|
||||
*/
|
||||
hideLabel?: boolean;
|
||||
/**
|
||||
* Callback for value-change in input
|
||||
* Callback for value-change in input.
|
||||
*/
|
||||
onChange?: (value: string) => void;
|
||||
/**
|
||||
* Callback for click on clear-button or Escape keydown
|
||||
* Callback for click on clear-button or Escape keydown.
|
||||
*/
|
||||
onClear?: (e: SearchClearEvent) => void;
|
||||
/**
|
||||
* Callback for Search-button submit
|
||||
* Callback for Search-button submit.
|
||||
*/
|
||||
onSearchClick?: (value: string) => void;
|
||||
/**
|
||||
* aria-label on clear button
|
||||
* Sets the `aria-label` for the clear button.
|
||||
* @default "Tøm"
|
||||
*/
|
||||
clearButtonLabel?: string;
|
||||
/**
|
||||
* If false, removes clear-button option from input.
|
||||
* Removes clear-button if `false`.
|
||||
* @default true
|
||||
*/
|
||||
clearButton?: boolean;
|
||||
/**
|
||||
* Changes button-variant, "simple" removes button
|
||||
* Changes button-variant.
|
||||
*
|
||||
* - "primary": When this is the main function of the page.
|
||||
* - "secondary": This is probably the one you want if in doubt.
|
||||
* - "simple": Removes the search button.
|
||||
* @default "primary"
|
||||
*/
|
||||
variant?: "primary" | "secondary" | "simple";
|
||||
/**
|
||||
* Exposes the HTML size attribute
|
||||
* Exposes the HTML size attribute. Specifies the width of the element, in characters.
|
||||
*/
|
||||
htmlSize?: number | string;
|
||||
/*
|
||||
* Exposes role attribute
|
||||
* Exposes role attribute.
|
||||
*/
|
||||
role?: string;
|
||||
}
|
||||
@@ -88,7 +92,7 @@ interface SearchComponent
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <form>
|
||||
* <form role="search">
|
||||
* <Search label="Søk alle NAV sine sider" variant="primary" />
|
||||
* </form>
|
||||
* ```
|
||||
|
||||
@@ -53,7 +53,7 @@ export interface SwitchProps
|
||||
*/
|
||||
position?: "left" | "right";
|
||||
/**
|
||||
* Adds a description to extend labling of Switch.
|
||||
* Adds a description to extend labeling of Switch.
|
||||
*/
|
||||
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 labling 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 labling 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,19 @@
|
||||
# @navikt/ds-tailwind
|
||||
|
||||
## 6.4.1
|
||||
|
||||
## 6.4.0
|
||||
|
||||
## 6.3.6
|
||||
|
||||
## 6.3.5
|
||||
|
||||
## 6.3.4
|
||||
|
||||
## 6.3.3
|
||||
|
||||
## 6.3.2
|
||||
|
||||
## 6.3.1
|
||||
|
||||
## 6.3.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tailwind",
|
||||
"version": "6.3.1",
|
||||
"version": "6.4.1",
|
||||
"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.1",
|
||||
"@navikt/ds-tokens": "^6.4.1",
|
||||
"color": "4.2.3",
|
||||
"lodash": "^4.17.21",
|
||||
"tailwindcss": "^3.3.3",
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @navikt/ds-tokens
|
||||
|
||||
## 6.4.1
|
||||
|
||||
## 6.4.0
|
||||
|
||||
## 6.3.6
|
||||
|
||||
## 6.3.5
|
||||
|
||||
## 6.3.4
|
||||
|
||||
## 6.3.3
|
||||
|
||||
## 6.3.2
|
||||
|
||||
## 6.3.1
|
||||
|
||||
## 6.3.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tokens",
|
||||
"version": "6.3.1",
|
||||
"version": "6.4.1",
|
||||
"description": "Design-tokens for NAV designsystem",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
|
||||
+127
-101
@@ -1,24 +1,46 @@
|
||||
# Changelog
|
||||
|
||||
## 6.4
|
||||
|
||||
### @navikt/ds-css
|
||||
|
||||
- FileUpload: Item-illustrasjon er nå radius-full ([#2834](https://github.com/navikt/aksel/pull/2834))
|
||||
|
||||
### @navikt/ds-react
|
||||
|
||||
- FileUpload.Item: :boom: Endret API og støtte for å sette ID på knappen ([#2824](https://github.com/navikt/aksel/pull/2824))
|
||||
|
||||
## 6.3
|
||||
|
||||
### @navikt/ds-css
|
||||
|
||||
- FileUpload: Ny komponent `FileUpload` for å håndtere opplasting av filer, Dropzone og fil-visning ([#2504](https://github.com/navikt/aksel/pull/2504))
|
||||
|
||||
### @navikt/ds-react
|
||||
|
||||
- FileUpload: Disabled dropzone stopper opplasting ([#2798](https://github.com/navikt/aksel/pull/2798))
|
||||
|
||||
* Allow Combobox options as objects to support separate display text and value ([#2716](https://github.com/navikt/aksel/pull/2716))
|
||||
- ReadMore: Padding-right på button er bumpet noen px ([#2806](https://github.com/navikt/aksel/pull/2806))
|
||||
|
||||
* FileUpload: Ny komponent `FileUpload` for å håndtere opplasting av filer, Dropzone og fil-visning ([#2504](https://github.com/navikt/aksel/pull/2504))
|
||||
|
||||
## 6.2
|
||||
|
||||
### @navikt/ds-react
|
||||
|
||||
- Select: Fjern 'multiple' fra SelectProps ([#2782](https://github.com/navikt/aksel/pull/2782))
|
||||
- DatePicker: Fjernet role gridcell fra dato-knapper. ([#2826](https://github.com/navikt/aksel/pull/2826))
|
||||
|
||||
* Modal: Fjern tomt element ([#2819](https://github.com/navikt/aksel/pull/2819))
|
||||
|
||||
- Switch: Fikset edgecase der checked Switch var visuelt unchecked ([#2810](https://github.com/navikt/aksel/pull/2810))
|
||||
|
||||
- FileUpload.Dropzone: id prop settes på button ([#2817](https://github.com/navikt/aksel/pull/2817))
|
||||
|
||||
* FileUpload.Dropzone: Fiks feil med duplisert id ([#2805](https://github.com/navikt/aksel/pull/2805))
|
||||
|
||||
* useDatePicker: `isInvalid`-key i onValidate var hadde flipper boolean i noen tilfeller ([#2809](https://github.com/navikt/aksel/pull/2809))
|
||||
|
||||
- FileUpload: Fiks feil ved import av json ([#2803](https://github.com/navikt/aksel/pull/2803))
|
||||
|
||||
* FileUpload: Disabled dropzone stopper opplasting ([#2798](https://github.com/navikt/aksel/pull/2798))
|
||||
|
||||
- Allow Combobox options as objects to support separate display text and value ([#2716](https://github.com/navikt/aksel/pull/2716))
|
||||
|
||||
- FileUpload: Ny komponent `FileUpload` for å håndtere opplasting av filer, Dropzone og fil-visning ([#2504](https://github.com/navikt/aksel/pull/2504))
|
||||
|
||||
## 6.2
|
||||
|
||||
### @navikt/aksel-icons
|
||||
|
||||
@@ -26,6 +48,10 @@
|
||||
|
||||
- Ikoner: Nytt ikon `opensource` ([#2787](https://github.com/navikt/aksel/pull/2787))
|
||||
|
||||
### @navikt/ds-react
|
||||
|
||||
- Select: Fjern 'multiple' fra SelectProps ([#2782](https://github.com/navikt/aksel/pull/2782))
|
||||
|
||||
## 6.1
|
||||
|
||||
### @navikt/ds-css
|
||||
@@ -48,9 +74,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
|
||||
|
||||
@@ -64,6 +92,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))
|
||||
@@ -92,19 +124,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
|
||||
|
||||
@@ -146,32 +172,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
|
||||
@@ -212,6 +238,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))
|
||||
@@ -232,11 +262,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
|
||||
|
||||
@@ -278,16 +308,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))
|
||||
@@ -296,6 +318,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))
|
||||
@@ -358,6 +384,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))
|
||||
@@ -398,11 +428,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
|
||||
|
||||
@@ -454,14 +488,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
|
||||
@@ -502,6 +528,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))
|
||||
@@ -538,11 +568,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
|
||||
|
||||
@@ -560,10 +590,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
|
||||
@@ -773,18 +799,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))
|
||||
@@ -819,9 +845,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
|
||||
|
||||
@@ -833,6 +859,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))
|
||||
@@ -867,10 +897,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
|
||||
@@ -923,6 +949,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))
|
||||
@@ -941,10 +971,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
|
||||
@@ -977,6 +1003,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))
|
||||
@@ -989,17 +1025,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
|
||||
|
||||
@@ -1035,26 +1073,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`
|
||||
@@ -1063,6 +1083,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
|
||||
@@ -1159,10 +1185,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))
|
||||
@@ -1175,6 +1197,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.1",
|
||||
"@navikt/ds-css": "^6.3.1",
|
||||
"@navikt/ds-react": "^6.3.1",
|
||||
"@navikt/ds-tailwind": "^6.3.1",
|
||||
"@navikt/ds-tokens": "^6.3.1"
|
||||
"@navikt/aksel-icons": "^6.4.1",
|
||||
"@navikt/ds-css": "^6.4.1",
|
||||
"@navikt/ds-react": "^6.4.1",
|
||||
"@navikt/ds-tailwind": "^6.4.1",
|
||||
"@navikt/ds-tokens": "^6.4.1"
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -9,7 +9,7 @@ type PropTableProps = {
|
||||
|
||||
const PropTable = ({ komponent }: PropTableProps) => {
|
||||
return (
|
||||
<div>
|
||||
<div lang="en">
|
||||
<Heading
|
||||
size="xsmall"
|
||||
level="3"
|
||||
@@ -26,7 +26,7 @@ const PropTable = ({ komponent }: PropTableProps) => {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<dl lang="en">
|
||||
<dl>
|
||||
{komponent?.overridable && (
|
||||
<div className="border border-t-0 border-gray-300 p-2">
|
||||
<dt className="px-2 py-2">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -17,7 +17,9 @@ export function createSearchResult(
|
||||
groupedHits,
|
||||
topResults:
|
||||
result?.length > 4
|
||||
? result.slice(0, 4).filter((x) => x.score && x.score < 0.1)
|
||||
? result
|
||||
.filter((x) => x.score !== undefined && x.score < 0.1)
|
||||
.slice(0, 4)
|
||||
: [],
|
||||
totalHits: result?.length ?? 0,
|
||||
hits: {
|
||||
|
||||
@@ -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.",
|
||||
};
|
||||
Loaded 100 of 137 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user