mirror of
https://github.com/navikt/aksel.git
synced 2026-10-13 09:12:03 +00:00
Compare commits
99
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8c3fc4ec5 | ||
|
|
834ba0cbae | ||
|
|
be653cc67a | ||
|
|
054835d1dd | ||
|
|
7f99d407c0 | ||
|
|
1202e0a6f7 | ||
|
|
0c06fa12d0 | ||
|
|
4797ccae9d | ||
|
|
ac06d882a7 | ||
|
|
8f5c956bad | ||
|
|
6b40a0ddd8 | ||
|
|
60a6cba329 | ||
|
|
7fd62f3b79 | ||
|
|
0944bbc256 | ||
|
|
16087a458a | ||
|
|
c47c372d0f | ||
|
|
0c479831df | ||
|
|
41e9fbd2a5 | ||
|
|
7b25f30e60 | ||
|
|
db9c858153 | ||
|
|
b21015bf54 | ||
|
|
bac48d9057 | ||
|
|
e1a85dfbeb | ||
|
|
3f65eee9e9 | ||
|
|
cd77198f1c | ||
|
|
82827ec712 | ||
|
|
710ed83661 | ||
|
|
3b44a97725 | ||
|
|
edcbd6e198 | ||
|
|
bd3f9355eb | ||
|
|
f11bce1f8c | ||
|
|
277525f39f | ||
|
|
210496c2cc | ||
|
|
8f317878fc | ||
|
|
c3064a01f7 | ||
|
|
02f0a911b0 | ||
|
|
ee8524229f | ||
|
|
d9932e203a | ||
|
|
b14fc23ecb | ||
|
|
c16f75d6f8 | ||
|
|
a01be91608 | ||
|
|
8826a79021 | ||
|
|
c2f3da84cc | ||
|
|
2c1476022d | ||
|
|
dacf648c47 | ||
|
|
5ec986771b | ||
|
|
ca6af8a5f7 | ||
|
|
9410840f74 | ||
|
|
d88884f785 | ||
|
|
0c8239ee00 | ||
|
|
987118fd47 | ||
|
|
8d13f11689 | ||
|
|
158e18454e | ||
|
|
be26bf22c9 | ||
|
|
c2968a0e6d | ||
|
|
3f92773273 | ||
|
|
0c3ab1d0e7 | ||
|
|
3a73a029f6 | ||
|
|
6fd0f501f5 | ||
|
|
a11b8ddf4e | ||
|
|
171db6a813 | ||
|
|
085cc532ee | ||
|
|
deae4ecba5 | ||
|
|
cd2adcb655 | ||
|
|
0d4385a545 | ||
|
|
6f1b9751f3 | ||
|
|
7fb97808f0 | ||
|
|
d8ac958de2 | ||
|
|
b417c74374 | ||
|
|
ff1d3534a6 | ||
|
|
57d015209d | ||
|
|
5969859024 | ||
|
|
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 |
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,25 @@
|
||||
# @navikt/aksel-icons
|
||||
|
||||
## 6.7.1
|
||||
|
||||
## 6.7.0
|
||||
|
||||
## 6.6.1
|
||||
|
||||
## 6.6.0
|
||||
|
||||
## 6.5.0
|
||||
|
||||
## 6.4.1
|
||||
|
||||
## 6.4.0
|
||||
|
||||
## 6.3.6
|
||||
|
||||
## 6.3.5
|
||||
|
||||
## 6.3.4
|
||||
|
||||
## 6.3.3
|
||||
|
||||
## 6.3.2
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-url": "^10.1.3",
|
||||
"typescript": "^5.1.6",
|
||||
"vite": "^5.0.12",
|
||||
"vite": "^5.1.7",
|
||||
"vite-plugin-singlefile": "^1.0.0",
|
||||
"vite-plugin-svgr": "^4.2.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.3",
|
||||
"version": "6.7.1",
|
||||
"author": "Aksel | NAV",
|
||||
"homepage": "https://aksel.nav.no/ikoner",
|
||||
"repository": {
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
# @navikt/aksel-stylelint
|
||||
|
||||
## 6.7.1
|
||||
|
||||
## 6.7.0
|
||||
|
||||
## 6.6.1
|
||||
|
||||
## 6.6.0
|
||||
|
||||
## 6.5.0
|
||||
|
||||
## 6.4.1
|
||||
|
||||
## 6.4.0
|
||||
|
||||
## 6.3.6
|
||||
|
||||
## 6.3.5
|
||||
|
||||
## 6.3.4
|
||||
|
||||
## 6.3.3
|
||||
|
||||
## 6.3.2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-stylelint",
|
||||
"version": "6.3.3",
|
||||
"version": "6.7.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.3",
|
||||
"@navikt/ds-tokens": "^6.3.3",
|
||||
"@navikt/ds-css": "^6.7.1",
|
||||
"@navikt/ds-tokens": "^6.7.1",
|
||||
"concurrently": "7.2.1",
|
||||
"postcss-selector-parser": "^6.0.13",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
|
||||
@@ -1,5 +1,77 @@
|
||||
# @navikt/aksel
|
||||
|
||||
## 6.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`0c06fa12d`](https://github.com/navikt/aksel/commit/0c06fa12d0de4f3015be045d4e65e149ebd2c141)]:
|
||||
- @navikt/ds-css@6.7.1
|
||||
|
||||
## 6.7.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`db9c85815`](https://github.com/navikt/aksel/commit/db9c85815392528e59ef5082397ec14b1aa36e7a), [`277525f39`](https://github.com/navikt/aksel/commit/277525f39ff94c7ded1accf6031957efa3bdeb8b), [`b21015bf5`](https://github.com/navikt/aksel/commit/b21015bf54b4e19673b2849756c608ae323a9f70)]:
|
||||
- @navikt/ds-css@6.7.0
|
||||
|
||||
## 6.6.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-css@6.6.1
|
||||
|
||||
## 6.6.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Aksel CLI: Fjernet deprecated komponenter fra css oversikt. ([#2860](https://github.com/navikt/aksel/pull/2860))
|
||||
|
||||
- Updated dependencies [[`3a73a029f`](https://github.com/navikt/aksel/commit/3a73a029f6272f76e10cb6baefbaa2040feaef6b)]:
|
||||
- @navikt/ds-css@6.6.0
|
||||
|
||||
## 6.5.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-css@6.5.0
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel",
|
||||
"version": "6.3.3",
|
||||
"version": "6.7.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.3",
|
||||
"@navikt/ds-css": "6.7.1",
|
||||
"axios": "1.6.0",
|
||||
"chalk": "4.1.0",
|
||||
"clipboardy": "^2.3.0",
|
||||
|
||||
@@ -1,5 +1,51 @@
|
||||
# @navikt/ds-css
|
||||
|
||||
## 6.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Progressbar: Tweak API, examples, stories and css ([#2892](https://github.com/navikt/aksel/pull/2892))
|
||||
|
||||
## 6.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- :sparkles: Ny komponent: ProgressBar ([#2845](https://github.com/navikt/aksel/pull/2845))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Checkbox, Radio: Erstatter `:focus-visible` med `:focus` for bedre UX brukt sammen med ErrorSummary. ([#2893](https://github.com/navikt/aksel/pull/2893))
|
||||
|
||||
- :lipstick: tweak CSS for timeline pins. ([#2889](https://github.com/navikt/aksel/pull/2889))
|
||||
|
||||
## 6.6.1
|
||||
|
||||
## 6.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- :sparkles: Ny komponent FormSummary ([#2802](https://github.com/navikt/aksel/pull/2802))
|
||||
|
||||
## 6.5.0
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/* https://web.dev/prefers-reduced-motion/ */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*:not(.navds-loader *):not(.navds-loader),
|
||||
*:not(.navds-loader *):not(.navds-loader):not(.navds-progress-bar *):not(.navds-progress-bar),
|
||||
::before,
|
||||
::after {
|
||||
animation-delay: -1ms !important;
|
||||
|
||||
@@ -97,10 +97,6 @@ const StyleMappings = {
|
||||
main: formCss,
|
||||
dependencies: [typoCss],
|
||||
},
|
||||
{
|
||||
component: "ContentContainer",
|
||||
main: "content-container.css",
|
||||
},
|
||||
{
|
||||
component: "CopyButton",
|
||||
main: "copybutton.css",
|
||||
@@ -125,7 +121,6 @@ const StyleMappings = {
|
||||
main: formCss,
|
||||
dependencies: [typoCss, "button.css", "loader.css", "link.css"],
|
||||
},
|
||||
{ component: "Grid", main: "grid.css", dependencies: [typoCss] },
|
||||
{
|
||||
component: "GuidePanel",
|
||||
main: "guide-panel.css",
|
||||
@@ -164,6 +159,7 @@ const StyleMappings = {
|
||||
},
|
||||
{ component: "Panel", main: "panel.css" },
|
||||
{ component: "Popover", main: "popover.css" },
|
||||
{ component: "ProgressBar", main: "progress-bar.css" },
|
||||
{ component: "Provider", main: "" },
|
||||
{ component: "Radio", main: formCss, dependencies: [typoCss] },
|
||||
{ component: "RadioGroup", main: formCss, dependencies: [typoCss] },
|
||||
@@ -178,6 +174,11 @@ const StyleMappings = {
|
||||
{ component: "Tag", main: "tag.css", dependencies: [typoCss] },
|
||||
{ component: "TextField", main: formCss, dependencies: [typoCss] },
|
||||
{ component: "Textarea", main: formCss, dependencies: [typoCss] },
|
||||
{
|
||||
component: "FormSummary",
|
||||
main: formCss,
|
||||
dependencies: [typoCss, "link.css"],
|
||||
},
|
||||
{
|
||||
component: "ToggleGroup",
|
||||
main: "toggle-group.css",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
.navds-form-summary {
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--a-border-subtle);
|
||||
border-radius: var(--a-border-radius-large);
|
||||
}
|
||||
|
||||
.navds-form-summary__header {
|
||||
background-color: var(--a-surface-subtle);
|
||||
padding: var(--a-spacing-4) var(--a-spacing-6);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0 var(--a-spacing-2);
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.navds-form-summary__header {
|
||||
padding: var(--a-spacing-3) var(--a-spacing-4);
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.navds-form-summary__edit {
|
||||
flex-shrink: 0;
|
||||
margin-top: var(--a-spacing-1);
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.navds-form-summary > .navds-form-summary__answers {
|
||||
border-top: 1px solid var(--a-border-subtle);
|
||||
}
|
||||
|
||||
.navds-form-summary__answers {
|
||||
padding: var(--a-spacing-5) var(--a-spacing-6) var(--a-spacing-6);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.navds-form-summary__answers {
|
||||
padding: var(--a-spacing-4);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-form-summary__answer:not(:last-child) {
|
||||
margin-bottom: var(--a-spacing-4);
|
||||
padding-bottom: var(--a-spacing-4);
|
||||
border-bottom: 1px solid var(--a-border-divider);
|
||||
}
|
||||
|
||||
.navds-form-summary__answer:has(.navds-form-summary__answer) {
|
||||
padding-bottom: var(--a-spacing-6);
|
||||
}
|
||||
|
||||
.navds-form-summary__answer:has(.navds-form-summary__answer):last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 479px) {
|
||||
.navds-form-summary__answer:not(:last-child) {
|
||||
margin-bottom: var(--a-spacing-3);
|
||||
padding-bottom: var(--a-spacing-3);
|
||||
}
|
||||
|
||||
.navds-form-summary__answer:has(.navds-form-summary__answer) {
|
||||
padding-bottom: var(--a-spacing-5);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-form-summary__value:first-of-type {
|
||||
margin-top: var(--a-spacing-1);
|
||||
}
|
||||
|
||||
.navds-form-summary__value :where(.navds-form-summary__answers) {
|
||||
margin-top: var(--a-spacing-2);
|
||||
padding: var(--a-spacing-4);
|
||||
background: var(--a-surface-selected);
|
||||
border-radius: var(--a-border-radius-large);
|
||||
}
|
||||
@@ -13,3 +13,4 @@
|
||||
@import "textarea.css";
|
||||
@import "search.css";
|
||||
@import "combobox.css";
|
||||
@import "form-summary.css";
|
||||
@@ -70,8 +70,8 @@
|
||||
height: calc(1.25rem - 0.25rem);
|
||||
}
|
||||
|
||||
.navds-checkbox__input:focus-visible + .navds-checkbox__label::before,
|
||||
.navds-radio__input:focus-visible + .navds-radio__label::before {
|
||||
.navds-checkbox__input:focus + .navds-checkbox__label::before,
|
||||
.navds-radio__input:focus + .navds-radio__label::before {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-shadow:
|
||||
@@ -80,33 +80,13 @@
|
||||
0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-checkbox__input:focus + .navds-checkbox__label::before,
|
||||
.navds-radio__input:focus + .navds-radio__label::before {
|
||||
outline: 2px solid transparent;
|
||||
outline-offset: 2px;
|
||||
box-shadow:
|
||||
0 0 0 2px var(--ac-radio-checkbox-border, var(--a-border-default)),
|
||||
0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-checkbox__input:hover:focus-visible + .navds-checkbox__label::before,
|
||||
.navds-radio__input:hover:focus-visible + .navds-radio__label::before {
|
||||
.navds-checkbox__input:hover:focus + .navds-checkbox__label::before,
|
||||
.navds-radio__input:hover:focus + .navds-radio__label::before {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action-hover)),
|
||||
0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-checkbox__input:hover:focus + .navds-checkbox__label::before,
|
||||
.navds-radio__input:hover:focus + .navds-radio__label::before {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action-hover)),
|
||||
0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-checkbox__input:indeterminate + .navds-checkbox__label::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
@@ -163,24 +143,14 @@
|
||||
background-position: 0.25rem center;
|
||||
}
|
||||
|
||||
.navds-checkbox__input:indeterminate:focus-visible + .navds-checkbox__label::before,
|
||||
.navds-checkbox__input:checked:focus-visible + .navds-checkbox__label::before {
|
||||
.navds-checkbox__input:indeterminate:focus + .navds-checkbox__label::before,
|
||||
.navds-checkbox__input:checked:focus + .navds-checkbox__label::before {
|
||||
box-shadow:
|
||||
0 0 0 1px var(--ac-radio-checkbox-action, var(--a-surface-action-selected)),
|
||||
0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)),
|
||||
0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-checkbox__input:indeterminate:focus + .navds-checkbox__label::before,
|
||||
.navds-checkbox__input:checked:focus + .navds-checkbox__label::before {
|
||||
box-shadow:
|
||||
0 0 0 1px var(--ac-radio-checkbox-action, var(--a-surface-action-selected)),
|
||||
0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)),
|
||||
0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-radio__input:checked + .navds-radio__label::before {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action-selected)),
|
||||
@@ -188,40 +158,23 @@
|
||||
background-color: var(--ac-radio-checkbox-action, var(--a-surface-action-selected));
|
||||
}
|
||||
|
||||
.navds-radio__input:checked:focus-visible + .navds-radio__label::before {
|
||||
.navds-radio__input:checked:focus + .navds-radio__label::before {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action-selected)),
|
||||
inset 0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)),
|
||||
0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-radio__input:checked:focus + .navds-radio__label::before {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action)),
|
||||
inset 0 0 0 2px var(--ac-radio-checkbox-bg, var(--a-surface-default)),
|
||||
0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-checkbox__input:hover:not(:disabled) + .navds-checkbox__label,
|
||||
.navds-radio__input:hover:not(:disabled) + .navds-radio__label {
|
||||
color: var(--ac-radio-checkbox-action, var(--a-surface-action-hover));
|
||||
}
|
||||
|
||||
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus-visible)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio__input:hover:not(:disabled):not(:checked):not(:focus-visible) + .navds-radio__label::before {
|
||||
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus) + .navds-checkbox__label::before,
|
||||
.navds-radio__input:hover:not(:disabled):not(:checked):not(:focus) + .navds-radio__label::before {
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action-hover));
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus) + .navds-checkbox__label::before,
|
||||
.navds-radio__input:hover:not(:disabled):not(:checked):not(:focus) + .navds-radio__label::before {
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-action, var(--a-surface-action-hover));
|
||||
}
|
||||
}
|
||||
|
||||
.navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate) + .navds-checkbox__label::before,
|
||||
.navds-radio__input:hover:not(:disabled):not(:checked) + .navds-radio__label::before {
|
||||
background-color: var(--ac-radio-checkbox-action-hover-bg, var(--a-surface-action-subtle));
|
||||
@@ -235,63 +188,32 @@
|
||||
}
|
||||
|
||||
.navds-checkbox--error
|
||||
> .navds-checkbox__input:focus-visible:not(:hover):not(:disabled):not(:checked):not(:indeterminate)
|
||||
> .navds-checkbox__input:focus:not(:hover):not(:disabled):not(:checked):not(:indeterminate)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--error > .navds-radio__input:focus-visible:not(:hover):not(:disabled):not(:checked) + .navds-radio__label::before {
|
||||
.navds-radio--error > .navds-radio__input:focus:not(:hover):not(:disabled):not(:checked) + .navds-radio__label::before {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)),
|
||||
0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-checkbox--error
|
||||
> .navds-checkbox__input:focus:not(:hover):not(:disabled):not(:checked):not(:indeterminate)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--error > .navds-radio__input:focus:not(:hover):not(:disabled):not(:checked) + .navds-radio__label::before {
|
||||
box-shadow:
|
||||
0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)),
|
||||
0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-checkbox--error
|
||||
> .navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus-visible)
|
||||
> .navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--error > .navds-radio__input:hover:not(:disabled):not(:checked):not(:focus-visible) + .navds-radio__label::before {
|
||||
.navds-radio--error > .navds-radio__input:hover:not(:disabled):not(:checked):not(:focus) + .navds-radio__label::before {
|
||||
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
|
||||
}
|
||||
|
||||
.navds-checkbox--error
|
||||
> .navds-checkbox__input:focus-visible:hover:not(:disabled):not(:checked):not(:indeterminate)
|
||||
> .navds-checkbox__input:focus:hover:not(:disabled):not(:checked):not(:indeterminate)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--error > .navds-radio__input:focus-visible:hover:not(:disabled):not(:checked) + .navds-radio__label::before {
|
||||
.navds-radio--error > .navds-radio__input:focus:hover:not(:disabled):not(:checked) + .navds-radio__label::before {
|
||||
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
|
||||
box-shadow:
|
||||
0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)),
|
||||
0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
|
||||
@supports not selector(:focus-visible) {
|
||||
.navds-checkbox--error
|
||||
> .navds-checkbox__input:hover:not(:disabled):not(:checked):not(:indeterminate):not(:focus)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--error > .navds-radio__input:hover:not(:disabled):not(:checked):not(:focus) + .navds-radio__label::before {
|
||||
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
|
||||
box-shadow: 0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger));
|
||||
}
|
||||
|
||||
.navds-checkbox--error
|
||||
> .navds-checkbox__input:focus:hover:not(:disabled):not(:checked):not(:indeterminate)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--error > .navds-radio__input:focus:hover:not(:disabled):not(:checked) + .navds-radio__label::before {
|
||||
background-color: var(--ac-radio-checkbox-error-hover-bg, var(--a-surface-danger-subtle));
|
||||
box-shadow:
|
||||
0 0 0 2px var(--ac-radio-checkbox-error-border, var(--a-border-danger)),
|
||||
0 0 0 4px var(--a-border-focus);
|
||||
}
|
||||
}
|
||||
|
||||
.navds-checkbox--disabled,
|
||||
.navds-radio--disabled {
|
||||
opacity: 0.3;
|
||||
@@ -323,16 +245,16 @@
|
||||
> .navds-checkbox__input:not(:disabled):not(:checked):not(:indeterminate)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-checkbox--readonly
|
||||
> .navds-checkbox__input:hover:not(:checked):not(:indeterminate):not(:focus-visible)
|
||||
> .navds-checkbox__input:hover:not(:checked):not(:indeterminate):not(:focus)
|
||||
+ .navds-checkbox__label::before,
|
||||
.navds-radio--readonly > .navds-radio__input:not(:disabled):not(:checked) + .navds-radio__label::before,
|
||||
.navds-radio--readonly > .navds-radio__input:hover:not(:checked):not(:focus-visible) + .navds-radio__label::before {
|
||||
.navds-radio--readonly > .navds-radio__input:hover:not(:checked):not(:focus) + .navds-radio__label::before {
|
||||
background-color: var(--__ac-radio-checkbox-readonly-bg);
|
||||
box-shadow: 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
|
||||
}
|
||||
|
||||
.navds-checkbox--readonly > .navds-checkbox__input:focus-visible + .navds-checkbox__label::before,
|
||||
.navds-radio--readonly > .navds-radio__input:focus-visible + .navds-radio__label::before {
|
||||
.navds-checkbox--readonly > .navds-checkbox__input:focus + .navds-checkbox__label::before,
|
||||
.navds-radio--readonly > .navds-radio__input:focus + .navds-radio__label::before {
|
||||
--__ac-radio-checkbox-readonly-border: var(--a-border-subtle), var(--a-shadow-focus);
|
||||
}
|
||||
|
||||
@@ -362,8 +284,8 @@
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.navds-checkbox__input:focus-visible + .navds-checkbox__label::before,
|
||||
.navds-radio__input:focus-visible + .navds-radio__label::before {
|
||||
.navds-checkbox__input:focus + .navds-checkbox__label::before,
|
||||
.navds-radio__input:focus + .navds-radio__label::before {
|
||||
outline-color: highlight;
|
||||
}
|
||||
|
||||
@@ -395,7 +317,7 @@
|
||||
background-color: var(--__ac-radio-checkbox-high-contrast-text);
|
||||
}
|
||||
|
||||
.navds-radio__input:checked:focus-visible + .navds-radio__label::before {
|
||||
.navds-radio__input:checked:focus + .navds-radio__label::before {
|
||||
border: 1px solid var(--__ac-radio-checkbox-high-contrast-bg);
|
||||
outline: 2px solid highlight;
|
||||
outline-offset: 2px;
|
||||
@@ -406,7 +328,7 @@
|
||||
color: highlight;
|
||||
}
|
||||
|
||||
.navds-checkbox__input:focus-visible + .navds-checkbox__label::before {
|
||||
.navds-checkbox__input:focus + .navds-checkbox__label::before {
|
||||
outline: 2px solid var(--__ac-radio-checkbox-high-contrast-highlight);
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
@import "panel.css";
|
||||
@import "link-panel.css";
|
||||
@import "read-more.css";
|
||||
@import "progress-bar.css";
|
||||
@import "skeleton.css";
|
||||
@import "stepper.css";
|
||||
@import "table.css";
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "6.3.3",
|
||||
"version": "6.7.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.3",
|
||||
"@navikt/ds-tokens": "^6.7.1",
|
||||
"cssnano": "6.0.0",
|
||||
"fast-glob": "3.2.11",
|
||||
"lodash": "4.17.21",
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
.navds-progress-bar {
|
||||
background: var(--a-surface-neutral-subtle);
|
||||
position: relative;
|
||||
border-radius: var(--a-border-radius-full);
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
inset 0 1px 3px 0 rgba(0 0 0 / 0.15),
|
||||
inset 0 0 1px 0 rgba(0 0 0 / 0.2);
|
||||
}
|
||||
|
||||
.navds-progress-bar--small {
|
||||
height: 12px;
|
||||
min-width: 12px;
|
||||
}
|
||||
|
||||
.navds-progress-bar--medium {
|
||||
height: 16px;
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
.navds-progress-bar--large {
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.navds-progress-bar__foreground {
|
||||
--__ac-progress-bar-translate: initial;
|
||||
|
||||
transform: translateX(var(--__ac-progress-bar-translate));
|
||||
background: var(--ac-progress-bar-fg, var(--a-surface-alt-3));
|
||||
transform-origin: left;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
border-radius: inherit;
|
||||
transition: transform 0.2s ease;
|
||||
}
|
||||
|
||||
.navds-progress-bar__foreground--indeterminate {
|
||||
--__ac-progress-bar-simulated: initial;
|
||||
|
||||
animation-name: navds-progress-bar-indeterminate-grow, navds-progress-bar-indeterminate;
|
||||
animation-timing-function: ease-in-out, ease-in-out;
|
||||
animation-duration: var(--__ac-progress-bar-simulated), 2500ms;
|
||||
animation-fill-mode: forwards, none;
|
||||
animation-iteration-count: 1, infinite;
|
||||
animation-delay: 0s, var(--__ac-progress-bar-simulated);
|
||||
}
|
||||
|
||||
/* navds-progress-bar-indeterminate wave animation */
|
||||
@keyframes navds-progress-bar-indeterminate {
|
||||
0% {
|
||||
left: -50%;
|
||||
width: 50%;
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
50% {
|
||||
animation-direction: reverse;
|
||||
left: 100%;
|
||||
transform: translateX(0%);
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
100% {
|
||||
left: -50%;
|
||||
transform: translateX(0%);
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes navds-progress-bar-indeterminate-grow {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
|
||||
20% {
|
||||
transform: translateX(-80%);
|
||||
}
|
||||
|
||||
30% {
|
||||
transform: translateX(-40%);
|
||||
}
|
||||
|
||||
50% {
|
||||
transform: translateX(-20%);
|
||||
}
|
||||
|
||||
75% {
|
||||
transform: translateX(-10%);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translateX(-10%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.navds-progress-bar__foreground {
|
||||
background: Highlight;
|
||||
}
|
||||
|
||||
.navds-progress-bar {
|
||||
outline: 1px solid transparent;
|
||||
}
|
||||
}
|
||||
@@ -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) {
|
||||
@@ -79,7 +79,8 @@
|
||||
}
|
||||
|
||||
.navds-read-more__button:hover > .navds-read-more__expand-icon {
|
||||
transform: translateY(1px);
|
||||
position: relative;
|
||||
top: 1px;
|
||||
}
|
||||
|
||||
.navds-read-more--open > .navds-read-more__button > .navds-read-more__expand-icon {
|
||||
@@ -87,5 +88,5 @@
|
||||
}
|
||||
|
||||
.navds-read-more--open > .navds-read-more__button:hover > .navds-read-more__expand-icon {
|
||||
transform: translateY(-1px) rotate(-180deg);
|
||||
top: -1px;
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
.navds-tabs__tablist {
|
||||
display: flex;
|
||||
max-width: 100%;
|
||||
width: auto;
|
||||
width: 100%;
|
||||
scroll-behavior: smooth;
|
||||
-ms-overflow-style: none; /* for Internet Explorer, Edge */
|
||||
scrollbar-width: none; /* for Firefox */
|
||||
@@ -83,6 +83,10 @@
|
||||
gap: var(--a-spacing-1);
|
||||
}
|
||||
|
||||
.navds-tabs__tab-inner > * {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.navds-tabs__tab-inner svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
@@ -117,6 +121,10 @@
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.navds-tabs__tab--fill {
|
||||
flex: 1 1 100%;
|
||||
}
|
||||
|
||||
.navds-tabs__tabpanel:focus-visible {
|
||||
outline: 2px solid transparent;
|
||||
box-shadow: inset 0 0 0 2px var(--a-border-focus);
|
||||
|
||||
@@ -245,7 +245,7 @@
|
||||
.navds-timeline__pin-wrapper::before {
|
||||
content: "";
|
||||
top: var(--navdsc-timeline-pin-size);
|
||||
height: calc(88% - var(--navdsc-timeline-pin-size));
|
||||
height: calc(100% - var(--navdsc-timeline-pin-size) * 2);
|
||||
width: 1px;
|
||||
margin: 0 auto;
|
||||
background: var(--a-surface-inverted);
|
||||
|
||||
@@ -4,6 +4,10 @@
|
||||
gap: var(--a-spacing-2);
|
||||
}
|
||||
|
||||
.navds-toggle-group__wrapper--fill {
|
||||
justify-items: initial;
|
||||
}
|
||||
|
||||
.navds-toggle-group {
|
||||
border-radius: var(--a-border-radius-medium);
|
||||
background-color: var(--ac-toggle-group-bg, var(--a-surface-transparent));
|
||||
|
||||
@@ -217,6 +217,9 @@
|
||||
"--ac-popover-bg": "--a-surface-default",
|
||||
"--ac-popover-border": "--a-border-default"
|
||||
},
|
||||
"progress-bar": {
|
||||
"--ac-progress-bar-fg": "--a-surface-alt-3"
|
||||
},
|
||||
"readmore": {
|
||||
"--ac-read-more-text": "--a-text-action",
|
||||
"--ac-read-more-hover-bg": "--a-surface-hover",
|
||||
|
||||
@@ -1,5 +1,131 @@
|
||||
# @navikt/ds-react
|
||||
|
||||
## 6.7.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Progressbar: Tweak API, examples, stories and css ([#2892](https://github.com/navikt/aksel/pull/2892))
|
||||
|
||||
- Combobox: Remove 'Ingen søketreff' when custom options allowed ([#2895](https://github.com/navikt/aksel/pull/2895))
|
||||
|
||||
- Datepicker, MonthPicker: Rekkefølgen på årstall i Select er reversert slik at siste år er øverst. Dette er endret for å være bedre tilpasset ekspertsystemer der de mest relevante årene ble vist lengst unna musepeker ved klikk. ([#2882](https://github.com/navikt/aksel/pull/2882))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@6.7.1
|
||||
- @navikt/aksel-icons@6.7.1
|
||||
|
||||
## 6.7.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- :sparkles: Ny komponent: ProgressBar ([#2845](https://github.com/navikt/aksel/pull/2845))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@6.7.0
|
||||
- @navikt/aksel-icons@6.7.0
|
||||
|
||||
## 6.6.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Spacer: Er nå `span` (tidligere `div`). ([#2874](https://github.com/navikt/aksel/pull/2874))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@6.6.1
|
||||
- @navikt/aksel-icons@6.6.1
|
||||
|
||||
## 6.6.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- :sparkles: Ny komponent FormSummary ([#2802](https://github.com/navikt/aksel/pull/2802))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Combobox: Prevents "Enter" while Combobox is focused from submitting form. ([#2861](https://github.com/navikt/aksel/pull/2861))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@6.6.0
|
||||
- @navikt/aksel-icons@6.6.0
|
||||
|
||||
## 6.5.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Tabs: Ny prop `lazy` som rendrer innhold i TabPanel selv når panel er skjult (har fortsatt display:none) ([#2621](https://github.com/navikt/aksel/pull/2621))
|
||||
|
||||
- ToggleGroup: Erstattet bruk av dependency `@radix-ui/react-toggle-group` med egen implementasjon. ([#2620](https://github.com/navikt/aksel/pull/2620))
|
||||
|
||||
- Tabs: La til ny prop `fill` som lar Tabs.Tab-elementer strekke seg over tilgjengelig bredde. ([#2621](https://github.com/navikt/aksel/pull/2621))
|
||||
|
||||
- Tabs: Erstattet bruk av dependency `@radix-ui/react-tabs` med egen implementasjon. ([#2621](https://github.com/navikt/aksel/pull/2621))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- DatePicker/MonthPicker: Valgte datoer får nå `aria-pressed` for å bedre indikere valg for skjermleser. ([#2838](https://github.com/navikt/aksel/pull/2838))
|
||||
|
||||
- DatePicker/MonthPicker: `required`-prop stoppet ikke de-select av allerede valgt dato. ([#2838](https://github.com/navikt/aksel/pull/2838))
|
||||
|
||||
- ToggleGroup: La til ny prop `fill` som strekker ToggleGroup til å ta opp all tilgjengelig bredde. ([#2620](https://github.com/navikt/aksel/pull/2620))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@6.5.0
|
||||
- @navikt/aksel-icons@6.5.0
|
||||
|
||||
## 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
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "6.3.3",
|
||||
"version": "6.7.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",
|
||||
@@ -24,7 +24,8 @@
|
||||
"/cjs",
|
||||
"/esm",
|
||||
"/src",
|
||||
"!**/*.tsbuildinfo"
|
||||
"!**/*.tsbuildinfo",
|
||||
"!**/*.stories.*"
|
||||
],
|
||||
"main": "./cjs/index.js",
|
||||
"module": "./esm/index.js",
|
||||
@@ -309,6 +310,16 @@
|
||||
"default": "./cjs/popover/index.js"
|
||||
}
|
||||
},
|
||||
"./ProgressBar": {
|
||||
"import": {
|
||||
"types": "./esm/progress-bar/index.d.ts",
|
||||
"default": "./esm/progress-bar/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./cjs/progress-bar/index.d.ts",
|
||||
"default": "./cjs/progress-bar/index.js"
|
||||
}
|
||||
},
|
||||
"./Provider": {
|
||||
"import": {
|
||||
"types": "./esm/provider/index.d.ts",
|
||||
@@ -549,6 +560,16 @@
|
||||
"default": "./cjs/form/file-upload/index.js"
|
||||
}
|
||||
},
|
||||
"./FormSummary": {
|
||||
"import": {
|
||||
"types": "./esm/form/form-summary/index.d.ts",
|
||||
"default": "./esm/form/form-summary/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./cjs/form/form-summary/index.d.ts",
|
||||
"default": "./cjs/form/form-summary/index.js"
|
||||
}
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
@@ -562,18 +583,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "0.25.4",
|
||||
"@navikt/aksel-icons": "^6.3.3",
|
||||
"@navikt/ds-tokens": "^6.3.3",
|
||||
"@radix-ui/react-tabs": "1.0.0",
|
||||
"@radix-ui/react-toggle-group": "1.0.0",
|
||||
"@floating-ui/react-dom": "^2.0.9",
|
||||
"@navikt/aksel-icons": "^6.7.1",
|
||||
"@navikt/ds-tokens": "^6.7.1",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.0.0",
|
||||
"react-day-picker": "8.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "8.13.0",
|
||||
"@testing-library/dom": "9.3.4",
|
||||
"@testing-library/jest-dom": "^5.16.0",
|
||||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@testing-library/user-event": "^14.2.0",
|
||||
"@types/faker": "5.5.8",
|
||||
"concurrently": "7.2.1",
|
||||
|
||||
@@ -183,3 +183,10 @@ export const Chromatic: Story = {
|
||||
chromatic: { disable: false },
|
||||
},
|
||||
};
|
||||
|
||||
export const ChromaticForcedColors: Story = {
|
||||
render: Chromatic.render,
|
||||
parameters: {
|
||||
chromatic: { disable: false, forcedColors: "active" },
|
||||
},
|
||||
};
|
||||
@@ -3,6 +3,7 @@ import React, { HTMLAttributes, forwardRef } from "react";
|
||||
import { BodyLong } from "../typography";
|
||||
import Bubble from "./Bubble";
|
||||
|
||||
export const VARIANTS = ["subtle", "info", "neutral"] as const;
|
||||
export const POSITIONS = ["left", "right"] as const;
|
||||
export const SIZES = ["medium", "small"] as const;
|
||||
|
||||
@@ -28,7 +29,7 @@ export interface ChatProps extends HTMLAttributes<HTMLDivElement> {
|
||||
* Avoid using the same background as the surface behind Chat.
|
||||
* @default "neutral"
|
||||
*/
|
||||
variant?: "subtle" | "info" | "neutral";
|
||||
variant?: (typeof VARIANTS)[number];
|
||||
/**
|
||||
* Positions avatar and bubbles.
|
||||
* @default "left"
|
||||
|
||||
@@ -1,25 +1,11 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { VStack } from "../layout/stack";
|
||||
import Chat, { POSITIONS, SIZES } from "./Chat";
|
||||
import Chat, { POSITIONS, SIZES, VARIANTS } from "./Chat";
|
||||
|
||||
export default {
|
||||
title: "ds-react/Chat",
|
||||
component: Chat,
|
||||
argTypes: {
|
||||
position: {
|
||||
control: { type: "radio" },
|
||||
options: POSITIONS,
|
||||
},
|
||||
toptextPosition: {
|
||||
control: { type: "radio" },
|
||||
options: POSITIONS,
|
||||
},
|
||||
size: {
|
||||
control: { type: "radio" },
|
||||
options: SIZES,
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
@@ -48,43 +34,49 @@ export const Controls: Story = {
|
||||
timestamp: "01.01.21 14:00",
|
||||
avatar: "ON",
|
||||
},
|
||||
argTypes: {
|
||||
position: {
|
||||
control: { type: "radio" },
|
||||
options: [...POSITIONS], // TypeScript does not like using readonly typed arrays here
|
||||
},
|
||||
toptextPosition: {
|
||||
control: { type: "radio" },
|
||||
options: [...POSITIONS],
|
||||
},
|
||||
size: {
|
||||
control: { type: "radio" },
|
||||
options: [...SIZES],
|
||||
},
|
||||
variant: {
|
||||
control: { type: "radio" },
|
||||
options: [...VARIANTS],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const Size: Story = {
|
||||
render: () => (
|
||||
<VStack gap="4">
|
||||
<h3>Medium</h3>
|
||||
<Chat
|
||||
avatar="ON"
|
||||
name="Ola Normann"
|
||||
timestamp="01.01.21 14:00"
|
||||
size="medium"
|
||||
>
|
||||
<Chat.Bubble>
|
||||
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
|
||||
</Chat.Bubble>
|
||||
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
|
||||
<Chat.Bubble>
|
||||
Adipisicing laborum est eu laborum est sit in commodo enim sint
|
||||
laboris labore nisi ut.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
<h3>Small</h3>
|
||||
<Chat
|
||||
avatar="ON"
|
||||
name="Ola Normann"
|
||||
timestamp="01.01.21 14:00"
|
||||
size="small"
|
||||
>
|
||||
<Chat.Bubble>
|
||||
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
|
||||
</Chat.Bubble>
|
||||
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
|
||||
<Chat.Bubble>
|
||||
Adipisicing laborum est eu laborum est sit in commodo enim sint
|
||||
laboris labore nisi ut.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
{SIZES.map((size) => (
|
||||
<>
|
||||
<h3>{size}</h3>
|
||||
<Chat
|
||||
avatar="ON"
|
||||
name="Ola Normann"
|
||||
timestamp="01.01.21 14:00"
|
||||
size={size}
|
||||
>
|
||||
<Chat.Bubble>
|
||||
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
|
||||
</Chat.Bubble>
|
||||
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
|
||||
<Chat.Bubble>
|
||||
Adipisicing laborum est eu laborum est sit in commodo enim sint
|
||||
laboris labore nisi ut.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
</>
|
||||
))}
|
||||
</VStack>
|
||||
),
|
||||
};
|
||||
@@ -92,35 +84,21 @@ export const Size: Story = {
|
||||
export const Variants: Story = {
|
||||
render: () => (
|
||||
<VStack gap="4">
|
||||
<h3>Info</h3>
|
||||
<Chat avatar="NAV" name="NAV" timestamp="01.01.21 14:00" variant="info">
|
||||
<Chat.Bubble>
|
||||
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
<h3>Neutral</h3>
|
||||
<Chat
|
||||
avatar="KN"
|
||||
name="Kari Nordmann"
|
||||
timestamp="01.01.21 14:03"
|
||||
variant="neutral"
|
||||
position="right"
|
||||
>
|
||||
<Chat.Bubble>Tempor fugiat amet eu sint in in ullamco.</Chat.Bubble>
|
||||
</Chat>
|
||||
<h3>Subtle</h3>
|
||||
<Chat
|
||||
avatar="ON"
|
||||
name="Ola Nordmann"
|
||||
timestamp="01.01.21 14:07"
|
||||
variant="subtle"
|
||||
position="right"
|
||||
>
|
||||
<Chat.Bubble>
|
||||
Adipisicing laborum est eu laborum est sit in commodo enim sint
|
||||
laboris labore nisi ut.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
{VARIANTS.map((variant) => (
|
||||
<>
|
||||
<h3>{variant}</h3>
|
||||
<Chat
|
||||
avatar="NAV"
|
||||
name="NAV"
|
||||
timestamp="01.01.21 14:00"
|
||||
variant={variant}
|
||||
>
|
||||
<Chat.Bubble>
|
||||
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
</>
|
||||
))}
|
||||
</VStack>
|
||||
),
|
||||
};
|
||||
@@ -131,8 +109,7 @@ export const Position: Story = {
|
||||
<h3>Default</h3>
|
||||
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 14:00">
|
||||
<Chat.Bubble>
|
||||
Adipisicing laborum est eu laborum est sit in commodo enim sint
|
||||
laboris labore nisi ut.
|
||||
Reprehenderit pariatur officia exercitation laboris.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
<h3>Right</h3>
|
||||
@@ -159,10 +136,7 @@ export const Position: Story = {
|
||||
timestamp="01.01.21 15:00"
|
||||
>
|
||||
<Chat.Bubble>
|
||||
Exercitation irure Lorem cupidatat culpa anim cillum esse ullamco qui
|
||||
dolore laborum et Lorem. Labore sunt duis id Lorem voluptate commodo
|
||||
ea esse. Dolore esse aliqua proident ea ad commodo ut dolore voluptate
|
||||
labore sunt aute.
|
||||
Reprehenderit pariatur officia exercitation laboris.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
</VStack>
|
||||
@@ -171,8 +145,8 @@ export const Position: Story = {
|
||||
|
||||
export const ToptextPosition: Story = {
|
||||
render: () => (
|
||||
<VStack gap="4">
|
||||
<h3>Right</h3>
|
||||
<VStack gap="4" style={{ width: 500 }}>
|
||||
<h3>All right</h3>
|
||||
<Chat
|
||||
avatar="ON"
|
||||
name="Ola Normann"
|
||||
@@ -180,20 +154,31 @@ export const ToptextPosition: Story = {
|
||||
toptextPosition="right"
|
||||
>
|
||||
<Chat.Bubble>
|
||||
Adipisicing laborum est eu laborum est sit in commodo enim sint
|
||||
laboris labore nisi ut.
|
||||
Reprehenderit pariatur officia exercitation laboris.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
<h3>Left</h3>
|
||||
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 15:00">
|
||||
<Chat.Bubble toptextPosition="left">
|
||||
Exercitation irure Lorem cupidatat culpa anim cillum esse ullamco qui
|
||||
dolore laborum et Lorem. Labore sunt duis id Lorem voluptate commodo
|
||||
ea esse. Dolore esse aliqua proident ea ad commodo ut dolore voluptate
|
||||
labore sunt aute.
|
||||
<h3>Single bubble right</h3>
|
||||
<Chat avatar="ON" name="Ola Normann" timestamp="01.01.21 14:00">
|
||||
<Chat.Bubble>
|
||||
Reprehenderit pariatur officia exercitation laboris.
|
||||
</Chat.Bubble>
|
||||
<Chat.Bubble timestamp="14:02" toptextPosition="right">
|
||||
Enim velit deserunt do quis.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
<h3>Left</h3>
|
||||
|
||||
<h3>All left</h3>
|
||||
<Chat
|
||||
avatar="ON"
|
||||
name="Ola Normann"
|
||||
timestamp="01.01.21 15:00"
|
||||
toptextPosition="left"
|
||||
>
|
||||
<Chat.Bubble>
|
||||
Reprehenderit pariatur officia exercitation laboris.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
<h3>All left, position right</h3>
|
||||
<Chat
|
||||
name="Kari Høyli"
|
||||
timestamp="01.01.21 14:32"
|
||||
@@ -203,25 +188,6 @@ export const ToptextPosition: Story = {
|
||||
<Chat.Bubble>
|
||||
Reprehenderit pariatur officia exercitation laboris.
|
||||
</Chat.Bubble>
|
||||
<Chat.Bubble>
|
||||
Enim velit deserunt do quis. Eu fugiat magna esse dolore ad sunt sit
|
||||
est dolore incididunt. Occaecat cupidatat magna.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
<h3>Right</h3>
|
||||
<Chat
|
||||
name="Kari Høyli"
|
||||
timestamp="01.01.21 14:32"
|
||||
position="right"
|
||||
toptextPosition="right"
|
||||
>
|
||||
<Chat.Bubble>
|
||||
Reprehenderit pariatur officia exercitation laboris.
|
||||
</Chat.Bubble>
|
||||
<Chat.Bubble>
|
||||
Enim velit deserunt do quis. Eu fugiat magna esse dolore ad sunt sit
|
||||
est dolore incididunt. Occaecat cupidatat magna.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
</VStack>
|
||||
),
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createContext } from "../util/create-context";
|
||||
import { CollapsibleBaseProps } from "./Collapsible.types";
|
||||
|
||||
export type CollapsibleContextProps = Pick<CollapsibleBaseProps, "open"> & {
|
||||
/**
|
||||
* Allows connecting trigger to content
|
||||
*/
|
||||
contentId: string;
|
||||
/**
|
||||
* Allows connecting content to trigger
|
||||
*/
|
||||
triggerId: string;
|
||||
/**
|
||||
* Callback for toggling open state
|
||||
*/
|
||||
onOpenToggle: () => void;
|
||||
/**
|
||||
* Allows always rendering children in content when closed
|
||||
*/
|
||||
lazy: boolean;
|
||||
/**
|
||||
* Utility for getting state as string. Usefull for data-attributes.
|
||||
*/
|
||||
state: "open" | "closed";
|
||||
};
|
||||
|
||||
export const [CollapsibleContextProvider, useCollapsibleContext] =
|
||||
createContext<CollapsibleContextProps>({
|
||||
name: "CollapsibleContext",
|
||||
errorMessage:
|
||||
"<Collapsible.Trigger> and <Collapsible.Content> must be used within a <Collapsible>",
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
import { Meta, StoryFn, StoryObj } from "@storybook/react";
|
||||
import React, { useState } from "react";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "@navikt/aksel-icons";
|
||||
import { Button } from "../button";
|
||||
import { Box } from "../layout/box";
|
||||
import { HStack, VStack } from "../layout/stack";
|
||||
import { Collapsible } from "./Collapsible";
|
||||
|
||||
const meta: Meta<typeof Collapsible> = {
|
||||
title: "Utilities/Collapsible",
|
||||
component: Collapsible,
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Collapsible>;
|
||||
|
||||
export const Default: Story = {
|
||||
render: (...props) => (
|
||||
<Collapsible {...props}>
|
||||
<Collapsible.Trigger>Trigger</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae
|
||||
corporis maxime aliquam, voluptates nobis numquam, non odit optio
|
||||
architecto iure laborum possimus! Quibusdam sit ullam, consequatur sunt
|
||||
tempore optio aliquid!
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
),
|
||||
args: {
|
||||
open: false,
|
||||
defaultOpen: false,
|
||||
},
|
||||
};
|
||||
|
||||
export const InContext: StoryFn<typeof Collapsible> = () => {
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Collapsible open={open} onOpenChange={setOpen}>
|
||||
<HStack justify="space-between" gap="4" align="center">
|
||||
Steg 1 av 8{" "}
|
||||
<Collapsible.Trigger asChild>
|
||||
<Button
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
iconPosition="right"
|
||||
icon={
|
||||
open ? (
|
||||
<ChevronUpIcon aria-hidden />
|
||||
) : (
|
||||
<ChevronDownIcon aria-hidden />
|
||||
)
|
||||
}
|
||||
>
|
||||
Vis steg
|
||||
</Button>
|
||||
</Collapsible.Trigger>
|
||||
</HStack>
|
||||
<Collapsible.Content>
|
||||
<p>Lorem ipsum dolor sit</p>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
export const Animated: StoryFn<typeof Collapsible> = () => (
|
||||
<Collapsible lazy>
|
||||
<Collapsible.Trigger asChild>
|
||||
<Button size="small" variant="secondary">
|
||||
Animer open/lukk
|
||||
</Button>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content className="collapsible">
|
||||
<div className="collapsibleContent">
|
||||
<Box padding="8" background="surface-alt-3-subtle">
|
||||
<div>lorem ipsum</div>
|
||||
</Box>
|
||||
</div>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
Animated.decorators = [
|
||||
(Story) => (
|
||||
<div style={{ height: 300 }}>
|
||||
<style>{`
|
||||
.collapsible {
|
||||
display: grid;
|
||||
grid-template-rows: 0fr;
|
||||
overflow: hidden;
|
||||
transition: grid-template-rows 1s ease-in-out;
|
||||
}
|
||||
|
||||
.collapsible[data-state="closed"] {
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.collapsible[data-state="open"] {
|
||||
grid-template-rows: 1fr;
|
||||
}
|
||||
|
||||
.collapsibleContent {
|
||||
min-height: 0;
|
||||
transition: visibility 1s;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.collapsible[data-state="open"] .collapsibleContent {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
`}</style>
|
||||
{Story()}
|
||||
</div>
|
||||
),
|
||||
];
|
||||
|
||||
export const Lazy: StoryFn<typeof Collapsible> = () => (
|
||||
<Collapsible lazy>
|
||||
<Collapsible.Trigger>Trigger</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae
|
||||
corporis maxime aliquam, voluptates nobis numquam, non odit optio
|
||||
architecto iure laborum possimus! Quibusdam sit ullam, consequatur sunt
|
||||
tempore optio aliquid!
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
export const AsChild: StoryFn<typeof Collapsible> = () => (
|
||||
<Collapsible>
|
||||
<Collapsible.Trigger asChild>
|
||||
<Button>Button</Button>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content asChild>
|
||||
<Box padding="4" background="surface-alt-3-subtle">
|
||||
<div>lorem ipsum</div>
|
||||
</Box>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
export const DefaultOpen: StoryFn<typeof Collapsible> = () => (
|
||||
<Collapsible defaultOpen>
|
||||
<Collapsible.Trigger asChild>
|
||||
<Button>Button</Button>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content asChild>
|
||||
<Box padding="4" background="surface-alt-3-subtle">
|
||||
<div>lorem ipsum</div>
|
||||
</Box>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
export const ControlledOpen: StoryFn<typeof Collapsible> = () => (
|
||||
<Collapsible open>
|
||||
<Collapsible.Trigger asChild>
|
||||
<Button>Button</Button>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content asChild>
|
||||
<Box padding="4" background="surface-alt-3-subtle">
|
||||
<div>lorem ipsum</div>
|
||||
</Box>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
export const Disabled: StoryFn<typeof Collapsible> = ({ open = false }) => (
|
||||
<Collapsible open={open}>
|
||||
<Collapsible.Trigger asChild disabled>
|
||||
<Button>Button</Button>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content asChild>
|
||||
<Box padding="4" background="surface-alt-3-subtle">
|
||||
<div>lorem ipsum</div>
|
||||
</Box>
|
||||
</Collapsible.Content>
|
||||
</Collapsible>
|
||||
);
|
||||
|
||||
export const Chromatic: Story = {
|
||||
render: () => (
|
||||
<VStack gap="8">
|
||||
<div>
|
||||
<h2>In context</h2>
|
||||
<InContext />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Animated</h2>
|
||||
<Animated />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Lazy</h2>
|
||||
<Lazy />
|
||||
</div>
|
||||
<div>
|
||||
<h2>AsChild</h2>
|
||||
<AsChild />
|
||||
</div>
|
||||
<div>
|
||||
<h2>DefaultOpen</h2>
|
||||
<DefaultOpen />
|
||||
</div>
|
||||
<div>
|
||||
<h2>ControlledOpen</h2>
|
||||
<ControlledOpen />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Disabled</h2>
|
||||
<Disabled />
|
||||
<Disabled open />
|
||||
</div>
|
||||
</VStack>
|
||||
),
|
||||
parameters: {
|
||||
chromatic: { disable: false },
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
import React, { forwardRef, useCallback } from "react";
|
||||
import { useControllableState, useId } from "../util/hooks";
|
||||
import { CollapsibleContextProvider } from "./Collapsible.context";
|
||||
import { CollapsibleBaseProps } from "./Collapsible.types";
|
||||
import CollapsibleContent from "./parts/Collapsible.Content";
|
||||
import CollapsibleTrigger from "./parts/Collapsible.Trigger";
|
||||
|
||||
export interface CollapsibleProps
|
||||
extends CollapsibleBaseProps,
|
||||
React.HTMLAttributes<HTMLDivElement> {}
|
||||
|
||||
interface CollapsibleComponent
|
||||
extends React.ForwardRefExoticComponent<
|
||||
CollapsibleProps & React.RefAttributes<HTMLDivElement>
|
||||
> {
|
||||
/**
|
||||
* @see 🏷️ {@link CollapsibleTriggerProps}
|
||||
*/
|
||||
Trigger: typeof CollapsibleTrigger;
|
||||
/**
|
||||
* @see 🏷️ {@link CollapsibleContentProps}
|
||||
*/
|
||||
Content: typeof CollapsibleContent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collapsible is a component that allows you to toggle visibility of content.
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <Collapsible>
|
||||
* <Collapsible.Trigger>Trigger</Collapsible.Trigger>
|
||||
* <Collapsible.Content>
|
||||
* Lorem ipsum dolor sit amet consectetur adipisicing elit. Recusandae
|
||||
* corporis maxime aliquam, voluptates nobis numquam, non odit optio
|
||||
* architecto iure laborum possimus! Quibusdam sit ullam, consequatur sunt
|
||||
* tempore optio aliquid!
|
||||
* </Collapsible.Content>
|
||||
* </Collapsible>
|
||||
* ```
|
||||
*
|
||||
* @example With asChild
|
||||
* ```jsx
|
||||
* <Collapsible>
|
||||
* <Collapsible.Trigger asChild>
|
||||
* <Button>Button</Button>
|
||||
* </Collapsible.Trigger>
|
||||
* <Collapsible.Content asChild>
|
||||
* <Box padding="4" background="surface-alt-3-subtle">
|
||||
* <div>lorem ipsum</div>
|
||||
* </Box>
|
||||
* </Collapsible.Content>
|
||||
* </Collapsible>
|
||||
* ```
|
||||
*/
|
||||
export const Collapsible = forwardRef<HTMLDivElement, CollapsibleProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
open,
|
||||
defaultOpen = false,
|
||||
onOpenChange,
|
||||
lazy = false,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [_open, setOpen] = useControllableState({
|
||||
value: open,
|
||||
defaultValue: defaultOpen,
|
||||
onChange: onOpenChange,
|
||||
});
|
||||
|
||||
const internalId = useId();
|
||||
const state = _open ? "open" : "closed";
|
||||
|
||||
return (
|
||||
<CollapsibleContextProvider
|
||||
open={_open}
|
||||
onOpenToggle={useCallback(
|
||||
() => setOpen((prevOpen) => !prevOpen),
|
||||
[setOpen],
|
||||
)}
|
||||
contentId={`collapsible-content-${internalId}`}
|
||||
triggerId={`collapsible-trigger-${internalId}`}
|
||||
lazy={lazy}
|
||||
state={state}
|
||||
>
|
||||
<div ref={ref} data-state={state} {...rest}>
|
||||
{children}
|
||||
</div>
|
||||
</CollapsibleContextProvider>
|
||||
);
|
||||
},
|
||||
) as CollapsibleComponent;
|
||||
|
||||
Collapsible.Trigger = CollapsibleTrigger;
|
||||
Collapsible.Content = CollapsibleContent;
|
||||
|
||||
export default Collapsible;
|
||||
@@ -0,0 +1,19 @@
|
||||
export interface CollapsibleBaseProps {
|
||||
/**
|
||||
* The open state of the collapsible when it is initially rendered. Use when you do not need to control its open state.
|
||||
*/
|
||||
defaultOpen?: boolean;
|
||||
/**
|
||||
* The controlled open state of the collapsible. Must be used in conjunction with onOpenChange.
|
||||
*/
|
||||
open?: boolean;
|
||||
/**
|
||||
* Event handler called when the open state of the collapsible changes.
|
||||
*/
|
||||
onOpenChange?: (open: boolean) => void;
|
||||
/**
|
||||
* Render the collapsible content lazily. This means that the content will not be rendered until the collapsible is open.
|
||||
* @default false
|
||||
*/
|
||||
lazy?: boolean;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
"use client";
|
||||
export { default as Collapsible, type CollapsibleProps } from "./Collapsible";
|
||||
export {
|
||||
default as CollapsibleTrigger,
|
||||
type CollapsibleTriggerProps,
|
||||
} from "./parts/Collapsible.Trigger";
|
||||
export {
|
||||
default as CollapsibleContent,
|
||||
type CollapsibleContentProps,
|
||||
} from "./parts/Collapsible.Content";
|
||||
@@ -0,0 +1,39 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { Slot } from "../../util/Slot";
|
||||
import { useCollapsibleContext } from "../Collapsible.context";
|
||||
|
||||
export interface CollapsibleContentProps
|
||||
extends Omit<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
"hidden" | "aria-controls" | "id"
|
||||
> {
|
||||
/**
|
||||
* When true, will render element as its child. This merges classes, styles and event handlers.
|
||||
* @default false
|
||||
*/
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export const CollapsibleContent = forwardRef<
|
||||
HTMLDivElement,
|
||||
CollapsibleContentProps
|
||||
>(({ children, asChild, ...rest }, ref) => {
|
||||
const ctx = useCollapsibleContext();
|
||||
|
||||
const Comp = asChild ? Slot : "div";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
{...rest}
|
||||
data-state={ctx.state}
|
||||
hidden={!ctx.open}
|
||||
aria-controls={ctx.open ? ctx.triggerId : undefined}
|
||||
id={ctx.contentId}
|
||||
>
|
||||
{ctx.lazy || ctx.open ? children : null}
|
||||
</Comp>
|
||||
);
|
||||
});
|
||||
|
||||
export default CollapsibleContent;
|
||||
@@ -0,0 +1,42 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { Slot } from "../../util/Slot";
|
||||
import { composeEventHandlers } from "../../util/composeEventHandlers";
|
||||
import { useCollapsibleContext } from "../Collapsible.context";
|
||||
|
||||
export interface CollapsibleTriggerProps
|
||||
extends Omit<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
"id" | "aria-controls" | "aria-expanded"
|
||||
> {
|
||||
/**
|
||||
* When true, will render element as its child. This merges classes, styles and event handlers.
|
||||
* @default false
|
||||
*/
|
||||
asChild?: boolean;
|
||||
}
|
||||
|
||||
export const CollapsibleTrigger = forwardRef<
|
||||
HTMLButtonElement,
|
||||
CollapsibleTriggerProps
|
||||
>(({ children, asChild, onClick, ...rest }, ref) => {
|
||||
const ctx = useCollapsibleContext();
|
||||
|
||||
const Comp = asChild ? Slot : "button";
|
||||
|
||||
return (
|
||||
<Comp
|
||||
ref={ref}
|
||||
type="button"
|
||||
data-state={ctx.state}
|
||||
onClick={composeEventHandlers(onClick, ctx.onOpenToggle)}
|
||||
{...rest}
|
||||
id={ctx.triggerId}
|
||||
aria-controls={ctx.open ? ctx.contentId : undefined}
|
||||
aria-expanded={ctx.open}
|
||||
>
|
||||
{children}
|
||||
</Comp>
|
||||
);
|
||||
});
|
||||
|
||||
export default CollapsibleTrigger;
|
||||
@@ -33,6 +33,7 @@ export interface CopyButtonProps
|
||||
/**
|
||||
* Text shown when button is clicked.
|
||||
* Only set if used with `text`-prop.
|
||||
* @default "Kopiert!"
|
||||
*/
|
||||
activeText?: string;
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import { expect, userEvent, within } from "@storybook/test";
|
||||
import { isSameDay } from "date-fns";
|
||||
import React, { useId, useState } from "react";
|
||||
import { Button } from "../../button";
|
||||
@@ -440,6 +441,39 @@ export const WeekDayClick = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Required = {
|
||||
render: () => {
|
||||
const { datepickerProps } = useDatepicker({
|
||||
defaultSelected: new Date("Apr 10 2024"),
|
||||
required: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ height: "20rem" }}>
|
||||
<DatePicker.Standalone {...datepickerProps} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const button10 = canvas.getByRole("button", { pressed: true });
|
||||
|
||||
await userEvent.click(button10);
|
||||
|
||||
expect(button10.ariaPressed).toBe("true");
|
||||
|
||||
const button17 = canvas.getByText("17").closest("button");
|
||||
|
||||
expect(button17?.ariaPressed).toBe("false");
|
||||
|
||||
button17 && (await userEvent.click(button17));
|
||||
|
||||
expect(button17?.ariaPressed).toBe("true");
|
||||
expect(button10.ariaPressed).toBe("false");
|
||||
},
|
||||
};
|
||||
|
||||
export const ModalDemo = () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
fromDate: new Date("Aug 23 2019"),
|
||||
|
||||
@@ -20,8 +20,11 @@ const DayButton = (props: DayProps) => {
|
||||
name="day"
|
||||
ref={buttonRef}
|
||||
{...dayRender.buttonProps}
|
||||
role={undefined}
|
||||
aria-label={dateTime}
|
||||
aria-hidden={dayRender.activeModifiers.outside}
|
||||
aria-selected={undefined}
|
||||
aria-pressed={!!dayRender.activeModifiers.selected}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -37,7 +37,11 @@ export const DropdownCaption = ({ displayMonth, id }: CaptionProps) => {
|
||||
const handleMonthChange: React.ChangeEventHandler<HTMLSelectElement> = (e) =>
|
||||
goToMonth(setMonth(startOfMonth(displayMonth), Number(e.target.value)));
|
||||
|
||||
const years = getYears(fromDate, toDate, displayMonth.getFullYear());
|
||||
const years = getYears(
|
||||
fromDate,
|
||||
toDate,
|
||||
displayMonth.getFullYear(),
|
||||
).reverse();
|
||||
const months = getMonths(fromDate, toDate, displayMonth);
|
||||
|
||||
const previousLabel = labelPrevious(previousMonth, { locale });
|
||||
|
||||
@@ -229,12 +229,16 @@ export const useDatepicker = (
|
||||
|
||||
/* Only allow de-selecting if not required */
|
||||
const handleDayClick: DayClickEventHandler = (day, { selected }) => {
|
||||
if (selected && required) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (day && !selected) {
|
||||
handleOpen(false);
|
||||
anchorRef?.focus();
|
||||
}
|
||||
|
||||
if (!required && selected) {
|
||||
if (selected) {
|
||||
updateDate(undefined);
|
||||
setInputValue("");
|
||||
updateValidation({ isValidDate: false, isEmpty: true });
|
||||
|
||||
@@ -225,13 +225,17 @@ export const useMonthpicker = (
|
||||
|
||||
/* Only allow de-selecting if not required */
|
||||
const handleMonthClick = (month?: Date) => {
|
||||
if (!month && required) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (month) {
|
||||
handleOpen(false);
|
||||
setYear(month);
|
||||
anchorRef?.focus();
|
||||
}
|
||||
|
||||
if (!required && !month) {
|
||||
if (!month) {
|
||||
updateMonth(undefined);
|
||||
updateValidation({ isValidMonth: false, isEmpty: true });
|
||||
setInputValue("");
|
||||
|
||||
@@ -66,6 +66,7 @@ export const MonthButton = ({
|
||||
type="button"
|
||||
onClick={() => onSelect(isSelected ? undefined : month)}
|
||||
disabled={isDisabled}
|
||||
aria-pressed={!!isSelected}
|
||||
className={cl("navds-date__month-button", {
|
||||
"rdp-day_today": dateIsInCurrentMonth(month, year),
|
||||
"rdp-day_selected": isSelected,
|
||||
|
||||
@@ -35,7 +35,7 @@ export const MonthCaption = () => {
|
||||
if (!years.map((x) => x.getFullYear()).includes(year.getFullYear())) {
|
||||
years.push(setYear(startOfYear(new Date()), year.getFullYear()));
|
||||
}
|
||||
years.sort((a, b) => a.getFullYear() - b.getFullYear());
|
||||
years.sort((a, b) => b.getFullYear() - a.getFullYear());
|
||||
}
|
||||
|
||||
const handleYearChange = (e) =>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Meta, StoryFn, StoryObj } from "@storybook/react";
|
||||
import { expect, userEvent, within } from "@storybook/test";
|
||||
import { setYear } from "date-fns";
|
||||
import React, { useId, useState } from "react";
|
||||
import { Button } from "../../button";
|
||||
@@ -121,25 +122,37 @@ export const UseMonthpickerFormat = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Required = () => {
|
||||
const { inputProps, monthpickerProps } = useMonthpicker({
|
||||
locale: "nb",
|
||||
defaultSelected: new Date(),
|
||||
disabled: [new Date("Apr 1 2022")],
|
||||
required: true,
|
||||
});
|
||||
export const Required = {
|
||||
render: () => {
|
||||
const { monthpickerProps } = useMonthpicker({
|
||||
defaultSelected: new Date("Apr 10 2024"),
|
||||
required: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<div style={{ height: "20rem" }}>
|
||||
<MonthPicker {...monthpickerProps}>
|
||||
<MonthPicker.Input
|
||||
{...inputProps}
|
||||
label="Velg måned"
|
||||
variant="monthpicker"
|
||||
/>
|
||||
</MonthPicker>
|
||||
</div>
|
||||
);
|
||||
return (
|
||||
<div style={{ height: "20rem" }}>
|
||||
<MonthPicker.Standalone {...monthpickerProps} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const buttonApr = canvas.getByRole("button", { pressed: true });
|
||||
|
||||
await userEvent.click(buttonApr);
|
||||
|
||||
expect(buttonApr.ariaPressed).toBe("true");
|
||||
|
||||
const buttonSep = canvas.getByText("september").closest("button");
|
||||
|
||||
expect(buttonSep?.ariaPressed).toBe("false");
|
||||
|
||||
buttonSep && (await userEvent.click(buttonSep));
|
||||
|
||||
expect(buttonSep?.ariaPressed).toBe("true");
|
||||
expect(buttonApr.ariaPressed).toBe("false");
|
||||
},
|
||||
};
|
||||
|
||||
export const UserControlled = () => {
|
||||
@@ -193,7 +206,6 @@ export const Chromatic: Story = {
|
||||
<DisabledMonths />
|
||||
<UseMonthpicker />
|
||||
<UseMonthpickerFormat />
|
||||
<Required />
|
||||
<UserControlled />
|
||||
<FollowYear />
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef, useRef, useState } from "react";
|
||||
import React, { forwardRef, useRef } from "react";
|
||||
import { useControllableState } from "../util/hooks";
|
||||
import { OverridableComponent } from "../util/types";
|
||||
import ExpansionCardContent, {
|
||||
ExpansionCardContentProps,
|
||||
@@ -116,24 +117,24 @@ export const ExpansionCard = forwardRef<HTMLDivElement, ExpansionCardProps>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const [_open, _setOpen] = useState(defaultOpen);
|
||||
|
||||
const shouldFade = useRef<boolean>(!(Boolean(open) || defaultOpen));
|
||||
|
||||
const handleOpen = () => {
|
||||
if (open === undefined) {
|
||||
const newOpen = !_open;
|
||||
_setOpen(newOpen);
|
||||
onToggle?.(newOpen);
|
||||
} else {
|
||||
onToggle?.(!open);
|
||||
}
|
||||
shouldFade.current = true;
|
||||
};
|
||||
const [_open, _setOpen] = useControllableState({
|
||||
value: open,
|
||||
onChange: (newValue) => {
|
||||
onToggle?.(newValue);
|
||||
shouldFade.current = true;
|
||||
},
|
||||
defaultValue: defaultOpen,
|
||||
});
|
||||
|
||||
return (
|
||||
<ExpansionCardContext.Provider
|
||||
value={{ open: open ?? _open, toggleOpen: handleOpen, size }}
|
||||
value={{
|
||||
open: open ?? _open,
|
||||
toggleOpen: () => _setOpen((x) => !x),
|
||||
size,
|
||||
}}
|
||||
>
|
||||
<section
|
||||
{...rest}
|
||||
|
||||
@@ -16,7 +16,7 @@ export const CheckboxGroupContext = createContext<CheckboxGroupState | null>(
|
||||
export interface CheckboxGroupProps
|
||||
extends Omit<
|
||||
FieldsetProps,
|
||||
"onChange" | "errorPropagation" | "defaultValue"
|
||||
"onChange" | "errorPropagation" | "defaultValue" | "nativeReadOnly"
|
||||
> {
|
||||
/**
|
||||
* Collection of `<Checkbox/>`.
|
||||
@@ -27,7 +27,7 @@ export interface CheckboxGroupProps
|
||||
*/
|
||||
value?: any[];
|
||||
/**
|
||||
* Default checked checkboxes on render.
|
||||
* Default checked checkboxes.
|
||||
*/
|
||||
defaultValue?: any[];
|
||||
/**
|
||||
@@ -44,10 +44,10 @@ export interface CheckboxGroupProps
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <CheckboxGroup legend="Hvor vil du sitte?">
|
||||
* <Checkbox value="Bakerst">Bakerst</Checkbox>
|
||||
* <Checkbox value="Midterst">Midterst</Checkbox>
|
||||
* <Checkbox value="Fremst">Fremst</Checkbox>
|
||||
* <CheckboxGroup legend="Transportmiddel">
|
||||
* <Checkbox value="car">Bil</Checkbox>
|
||||
* <Checkbox value="taxi">Drosje</Checkbox>
|
||||
* <Checkbox value="public">Kollektivt</Checkbox>
|
||||
* </CheckboxGroup>
|
||||
* ```
|
||||
*/
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface CheckboxProps
|
||||
*/
|
||||
errorId?: string;
|
||||
/**
|
||||
* Checkbox label
|
||||
* Checkbox label.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
@@ -22,7 +22,7 @@ export interface CheckboxProps
|
||||
*/
|
||||
hideLabel?: boolean;
|
||||
/**
|
||||
* The value of the HTML element.
|
||||
* Checkbox value.
|
||||
*/
|
||||
value?: any;
|
||||
/**
|
||||
@@ -31,7 +31,7 @@ export interface CheckboxProps
|
||||
*/
|
||||
indeterminate?: boolean;
|
||||
/**
|
||||
* Adds a description to extend labeling of Checkbox.
|
||||
* Adds a description to extend the labeling.
|
||||
*/
|
||||
description?: string;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import React from "react";
|
||||
import { XMarkIcon } from "@navikt/aksel-icons";
|
||||
|
||||
interface ClearButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
|
||||
handleClear: (event: any) => void;
|
||||
clearButtonLabel?: string;
|
||||
}
|
||||
|
||||
export const ClearButton: React.FC<ClearButtonProps> = ({
|
||||
handleClear,
|
||||
clearButtonLabel,
|
||||
...rest
|
||||
}) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleClear}
|
||||
className="navds-combobox__button-clear"
|
||||
{...rest}
|
||||
>
|
||||
<span className="navds-sr-only">
|
||||
{clearButtonLabel ? clearButtonLabel : "Tøm"}
|
||||
</span>
|
||||
<XMarkIcon aria-hidden />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export default ClearButton;
|
||||
@@ -1,57 +1,31 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef, useRef } from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
import { BodyShort, ErrorMessage, Label } from "../../typography";
|
||||
import { useMergeRefs } from "../../util/hooks/useMergeRefs";
|
||||
import ClearButton from "./ClearButton";
|
||||
import ComboboxWrapper from "./ComboboxWrapper";
|
||||
import FilteredOptions from "./FilteredOptions/FilteredOptions";
|
||||
import { useFilteredOptionsContext } from "./FilteredOptions/filteredOptionsContext";
|
||||
import Input from "./Input/Input";
|
||||
import { useInputContext } from "./Input/inputContext";
|
||||
import SelectedOptions from "./SelectedOptions/SelectedOptions";
|
||||
import { useSelectedOptionsContext } from "./SelectedOptions/selectedOptionsContext";
|
||||
import ToggleListButton from "./ToggleListButton";
|
||||
import { useInputContext } from "./Input/Input.context";
|
||||
import { InputController } from "./Input/InputController";
|
||||
import { ComboboxProps } from "./types";
|
||||
|
||||
export const Combobox = forwardRef<
|
||||
HTMLInputElement,
|
||||
Omit<ComboboxProps, "onChange" | "options" | "size" | "onClear" | "value">
|
||||
>((props, ref) => {
|
||||
const {
|
||||
className,
|
||||
hideLabel = false,
|
||||
description,
|
||||
label,
|
||||
clearButton = true,
|
||||
clearButtonLabel,
|
||||
toggleListButton = true,
|
||||
toggleListButtonLabel,
|
||||
inputClassName,
|
||||
shouldShowSelectedOptions = true,
|
||||
...rest
|
||||
} = props;
|
||||
const { className, hideLabel = false, description, label, ...rest } = props;
|
||||
|
||||
const toggleListButtonRef = useRef<HTMLButtonElement>(null);
|
||||
|
||||
const { activeDecendantId, toggleIsListOpen } = useFilteredOptionsContext();
|
||||
const { selectedOptions } = useSelectedOptionsContext();
|
||||
const { toggleIsListOpen } = useFilteredOptionsContext();
|
||||
|
||||
const {
|
||||
clearInput,
|
||||
error,
|
||||
errorId,
|
||||
focusInput,
|
||||
hasError,
|
||||
inputDescriptionId,
|
||||
inputProps,
|
||||
inputRef,
|
||||
value,
|
||||
showErrorMsg,
|
||||
size = "medium",
|
||||
} = useInputContext();
|
||||
|
||||
const mergedInputRef = useMergeRefs(inputRef, ref);
|
||||
|
||||
return (
|
||||
<ComboboxWrapper
|
||||
className={className}
|
||||
@@ -59,7 +33,6 @@ export const Combobox = forwardRef<
|
||||
inputProps={inputProps}
|
||||
inputSize={size}
|
||||
toggleIsListOpen={toggleIsListOpen}
|
||||
toggleListButtonRef={toggleListButtonRef}
|
||||
>
|
||||
<Label
|
||||
htmlFor={inputProps.id}
|
||||
@@ -83,50 +56,7 @@ export const Combobox = forwardRef<
|
||||
</BodyShort>
|
||||
)}
|
||||
<div className="navds-combobox__wrapper">
|
||||
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */}
|
||||
<div
|
||||
className={cl(
|
||||
"navds-combobox__wrapper-inner navds-text-field__input",
|
||||
{
|
||||
"navds-combobox__wrapper-inner--virtually-unfocused":
|
||||
activeDecendantId !== undefined,
|
||||
},
|
||||
)}
|
||||
onClick={focusInput}
|
||||
>
|
||||
{!shouldShowSelectedOptions ? (
|
||||
<Input
|
||||
id={inputProps.id}
|
||||
ref={mergedInputRef}
|
||||
inputClassName={inputClassName}
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
<SelectedOptions selectedOptions={selectedOptions} size={size}>
|
||||
<Input
|
||||
id={inputProps.id}
|
||||
ref={mergedInputRef}
|
||||
inputClassName={inputClassName}
|
||||
{...rest}
|
||||
/>
|
||||
</SelectedOptions>
|
||||
)}
|
||||
<div>
|
||||
{value && clearButton && (
|
||||
<ClearButton
|
||||
handleClear={clearInput}
|
||||
clearButtonLabel={clearButtonLabel}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
)}
|
||||
{toggleListButton && (
|
||||
<ToggleListButton
|
||||
toggleListButtonLabel={toggleListButtonLabel}
|
||||
ref={toggleListButtonRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<InputController ref={ref} {...rest} />
|
||||
<FilteredOptions />
|
||||
</div>
|
||||
<div
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import Combobox from "./Combobox";
|
||||
import { FilteredOptionsProvider } from "./FilteredOptions/filteredOptionsContext";
|
||||
import { InputContextProvider } from "./Input/inputContext";
|
||||
import { InputContextProvider } from "./Input/Input.context";
|
||||
import { SelectedOptionsProvider } from "./SelectedOptions/selectedOptionsContext";
|
||||
import { mapToComboboxOptionArray } from "./combobox-utils";
|
||||
import { CustomOptionsProvider } from "./customOptionsContext";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import cl from "clsx";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useInputContext } from "./Input/Input.context";
|
||||
|
||||
type ComboboxWrapperProps = {
|
||||
children: any;
|
||||
@@ -10,7 +11,6 @@ type ComboboxWrapperProps = {
|
||||
};
|
||||
inputSize: string;
|
||||
toggleIsListOpen: (isListOpen: boolean) => void;
|
||||
toggleListButtonRef: React.RefObject<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
const ComboboxWrapper = ({
|
||||
@@ -20,15 +20,16 @@ const ComboboxWrapper = ({
|
||||
inputProps,
|
||||
inputSize,
|
||||
toggleIsListOpen,
|
||||
toggleListButtonRef,
|
||||
}: ComboboxWrapperProps) => {
|
||||
const { toggleOpenButtonRef } = useInputContext();
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const [hasFocusWithin, setHasFocusWithin] = useState(false);
|
||||
|
||||
function onFocusInsideWrapper(e) {
|
||||
if (
|
||||
!wrapperRef.current?.contains(e.relatedTarget) &&
|
||||
toggleListButtonRef?.current !== e.target
|
||||
toggleOpenButtonRef?.current !== e.target
|
||||
) {
|
||||
toggleIsListOpen(true);
|
||||
setHasFocusWithin(true);
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
const CheckIcon = () => {
|
||||
return (
|
||||
<svg
|
||||
width="16"
|
||||
height="13"
|
||||
viewBox="0 0 16 13"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
fill="#005B82"
|
||||
d="M14.2014 0L16 1.89047L4.77943 13L0 8.39552L1.79361 6.5L4.77418 9.3019L14.2014 0Z"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export default CheckIcon;
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
import { CheckmarkIcon, PlusIcon } from "@navikt/aksel-icons";
|
||||
import { Loader } from "../../../loader";
|
||||
import { BodyShort, Label } from "../../../typography";
|
||||
import { useInputContext } from "../Input/inputContext";
|
||||
import { useInputContext } from "../Input/Input.context";
|
||||
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
|
||||
import { isInList, toComboboxOption } from "../combobox-utils";
|
||||
import { ComboboxOption } from "../types";
|
||||
@@ -38,7 +38,7 @@ const FilteredOptions = () => {
|
||||
const shouldRenderNonSelectables =
|
||||
maxSelected?.isLimitReached || // Render maxSelected message
|
||||
isLoading || // Render loading message
|
||||
(!isLoading && filteredOptions.length === 0); // Render no hits message
|
||||
(!isLoading && filteredOptions.length === 0 && !allowNewValues); // Render no hits message
|
||||
|
||||
const shouldRenderFilteredOptionsList =
|
||||
(allowNewValues && isValueNew && !maxSelected?.isLimitReached) || // Render add new option
|
||||
@@ -72,7 +72,7 @@ const FilteredOptions = () => {
|
||||
<Loader title="Søker..." />
|
||||
</div>
|
||||
)}
|
||||
{!isLoading && filteredOptions.length === 0 && (
|
||||
{!isLoading && filteredOptions.length === 0 && !allowNewValues && (
|
||||
<div
|
||||
className="navds-combobox__list-item--no-options"
|
||||
id={filteredOptionsUtil.getNoHitsId(id)}
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import cl from "clsx";
|
||||
import React, {
|
||||
SetStateAction,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { SetStateAction, useCallback, useMemo, useState } from "react";
|
||||
import { createContext } from "../../../util/create-context";
|
||||
import { useClientLayoutEffect, usePrevious } from "../../../util/hooks";
|
||||
import { useInputContext } from "../Input/inputContext";
|
||||
import { useInputContext } from "../Input/Input.context";
|
||||
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
|
||||
import { toComboboxOption } from "../combobox-utils";
|
||||
import { useCustomOptionsContext } from "../customOptionsContext";
|
||||
import { useComboboxCustomOptions } from "../customOptionsContext";
|
||||
import { ComboboxOption, ComboboxProps } from "../types";
|
||||
import filteredOptionsUtils from "./filtered-options-util";
|
||||
import useVirtualFocus, { VirtualFocusType } from "./useVirtualFocus";
|
||||
@@ -24,7 +18,7 @@ type FilteredOptionsProps = {
|
||||
};
|
||||
};
|
||||
|
||||
type FilteredOptionsContextType = {
|
||||
type FilteredOptionsContextValue = {
|
||||
activeDecendantId?: string;
|
||||
allowNewValues?: boolean;
|
||||
ariaDescribedBy?: string;
|
||||
@@ -42,11 +36,14 @@ type FilteredOptionsContextType = {
|
||||
shouldAutocomplete?: boolean;
|
||||
virtualFocus: VirtualFocusType;
|
||||
};
|
||||
const FilteredOptionsContext = createContext<FilteredOptionsContextType>(
|
||||
{} as FilteredOptionsContextType,
|
||||
);
|
||||
const [FilteredOptionsContextProvider, useFilteredOptionsContext] =
|
||||
createContext<FilteredOptionsContextValue>({
|
||||
name: "FilteredOptionsContext",
|
||||
errorMessage:
|
||||
"useFilteredOptionsContext must be used within a FilteredOptionsProvider",
|
||||
});
|
||||
|
||||
export const FilteredOptionsProvider = ({
|
||||
const FilteredOptionsProvider = ({
|
||||
children,
|
||||
value: props,
|
||||
}: FilteredOptionsProps) => {
|
||||
@@ -71,7 +68,7 @@ export const FilteredOptionsProvider = ({
|
||||
const { maxSelected } = useSelectedOptionsContext();
|
||||
|
||||
const [isInternalListOpen, setInternalListOpen] = useState(false);
|
||||
const { customOptions } = useCustomOptionsContext();
|
||||
const { customOptions } = useComboboxCustomOptions();
|
||||
|
||||
const filteredOptions = useMemo(() => {
|
||||
if (externalFilteredOptions) {
|
||||
@@ -97,9 +94,17 @@ export const FilteredOptionsProvider = ({
|
||||
[filteredOptionsUtils.getAddNewOptionId(id)]: allowNewValues
|
||||
? toComboboxOption(value)
|
||||
: undefined,
|
||||
...customOptions.reduce(
|
||||
(acc, customOption) => ({
|
||||
...acc,
|
||||
[filteredOptionsUtils.getOptionId(id, customOption.label)]:
|
||||
customOption,
|
||||
}),
|
||||
{},
|
||||
),
|
||||
},
|
||||
),
|
||||
[allowNewValues, id, options, value],
|
||||
[allowNewValues, customOptions, id, options, value],
|
||||
);
|
||||
|
||||
useClientLayoutEffect(() => {
|
||||
@@ -144,9 +149,9 @@ export const FilteredOptionsProvider = ({
|
||||
|
||||
const ariaDescribedBy = useMemo(() => {
|
||||
let activeOption;
|
||||
if (!isLoading && filteredOptions.length === 0) {
|
||||
if (!isLoading && filteredOptions.length === 0 && !allowNewValues) {
|
||||
activeOption = filteredOptionsUtils.getNoHitsId(id);
|
||||
} else if ((value && value !== "") || isLoading) {
|
||||
} else if (value || isLoading) {
|
||||
if (shouldAutocomplete && filteredOptions[0]) {
|
||||
activeOption = filteredOptionsUtils.getOptionId(
|
||||
id,
|
||||
@@ -172,6 +177,7 @@ export const FilteredOptionsProvider = ({
|
||||
shouldAutocomplete,
|
||||
filteredOptions,
|
||||
id,
|
||||
allowNewValues,
|
||||
]);
|
||||
|
||||
const currentOption = useMemo(
|
||||
@@ -203,18 +209,10 @@ export const FilteredOptionsProvider = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<FilteredOptionsContext.Provider value={filteredOptionsState}>
|
||||
<FilteredOptionsContextProvider {...filteredOptionsState}>
|
||||
{children}
|
||||
</FilteredOptionsContext.Provider>
|
||||
</FilteredOptionsContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useFilteredOptionsContext = () => {
|
||||
const context = useContext(FilteredOptionsContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useFilteredOptionsContext must be used within a FilteredOptionsProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
export { FilteredOptionsProvider, useFilteredOptionsContext };
|
||||
+30
-33
@@ -1,18 +1,18 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
ChangeEventHandler,
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { createContext } from "../../../util/create-context";
|
||||
import { useClientLayoutEffect } from "../../../util/hooks";
|
||||
import { FormFieldType, useFormField } from "../../useFormField";
|
||||
import { ComboboxProps } from "../types";
|
||||
|
||||
interface InputContextType extends FormFieldType {
|
||||
clearInput: (event: React.PointerEvent | React.KeyboardEvent) => void;
|
||||
interface InputContextValue extends FormFieldType {
|
||||
clearInput: NonNullable<ComboboxProps["onClear"]>;
|
||||
error?: string;
|
||||
focusInput: () => void;
|
||||
inputRef: React.RefObject<HTMLInputElement>;
|
||||
@@ -22,11 +22,16 @@ interface InputContextType extends FormFieldType {
|
||||
searchTerm: string;
|
||||
setSearchTerm: React.Dispatch<React.SetStateAction<string>>;
|
||||
shouldAutocomplete?: boolean;
|
||||
toggleOpenButtonRef: React.RefObject<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
const InputContext = createContext<InputContextType>({} as InputContextType);
|
||||
const [InputContextProvider, useInputContext] =
|
||||
createContext<InputContextValue>({
|
||||
name: "InputContext",
|
||||
errorMessage: "useInputContext must be used within an InputContextProvider",
|
||||
});
|
||||
|
||||
export const InputContextProvider = ({ children, value: props }) => {
|
||||
const InputProvider = ({ children, value: props }) => {
|
||||
const {
|
||||
defaultValue = "",
|
||||
description,
|
||||
@@ -52,6 +57,7 @@ export const InputContextProvider = ({ children, value: props }) => {
|
||||
"comboboxfield",
|
||||
);
|
||||
const inputRef = useRef<HTMLInputElement | null>(null);
|
||||
const toggleOpenButtonRef = useRef<HTMLButtonElement>(null);
|
||||
const [internalValue, setInternalValue] = useState<string>(defaultValue);
|
||||
|
||||
const value = useMemo(
|
||||
@@ -79,7 +85,7 @@ export const InputContextProvider = ({ children, value: props }) => {
|
||||
);
|
||||
|
||||
const clearInput = useCallback(
|
||||
(event: React.PointerEvent | React.KeyboardEvent) => {
|
||||
(event: React.PointerEvent | React.KeyboardEvent | React.MouseEvent) => {
|
||||
onClear?.(event);
|
||||
externalOnChange?.(null, "");
|
||||
setValue("");
|
||||
@@ -98,33 +104,24 @@ export const InputContextProvider = ({ children, value: props }) => {
|
||||
}
|
||||
}, [value, searchTerm, shouldAutocomplete]);
|
||||
|
||||
const contextValue = {
|
||||
...formFieldProps,
|
||||
clearInput,
|
||||
error,
|
||||
focusInput,
|
||||
inputRef,
|
||||
value,
|
||||
setValue,
|
||||
onChange,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
shouldAutocomplete,
|
||||
toggleOpenButtonRef,
|
||||
};
|
||||
|
||||
return (
|
||||
<InputContext.Provider
|
||||
value={{
|
||||
...formFieldProps,
|
||||
clearInput,
|
||||
error,
|
||||
focusInput,
|
||||
inputRef,
|
||||
value,
|
||||
setValue,
|
||||
onChange,
|
||||
searchTerm,
|
||||
setSearchTerm,
|
||||
shouldAutocomplete,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</InputContext.Provider>
|
||||
<InputContextProvider {...contextValue}>{children}</InputContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useInputContext = () => {
|
||||
const context = useContext(InputContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useInputContext must be used within an InputContextProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
export { InputProvider as InputContextProvider, useInputContext };
|
||||
@@ -9,7 +9,7 @@ import { omit } from "../../../util";
|
||||
import filteredOptionsUtil from "../FilteredOptions/filtered-options-util";
|
||||
import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
|
||||
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
|
||||
import { useInputContext } from "./inputContext";
|
||||
import { useInputContext } from "./Input.context";
|
||||
|
||||
interface InputProps
|
||||
extends Omit<InputHTMLAttributes<HTMLInputElement>, "value"> {
|
||||
@@ -128,6 +128,10 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
removeSelectedOption(lastSelectedOption);
|
||||
}
|
||||
}
|
||||
} else if (e.key === "Enter" || e.key === "Accept") {
|
||||
if (activeDecendantId || value) {
|
||||
e.preventDefault();
|
||||
}
|
||||
} else if (e.key === "ArrowDown") {
|
||||
// Check that cursor position is at the end of the input field,
|
||||
// so we don't interfere with text editing
|
||||
@@ -182,6 +186,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
{...omit(inputProps, ["aria-invalid"])}
|
||||
ref={ref}
|
||||
value={value}
|
||||
onBlur={() => virtualFocus.moveFocusToTop()}
|
||||
onChange={onChangeHandler}
|
||||
type="text"
|
||||
role="combobox"
|
||||
|
||||
@@ -0,0 +1,102 @@
|
||||
/* eslint-disable jsx-a11y/no-static-element-interactions */
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import { XMarkIcon } from "@navikt/aksel-icons";
|
||||
import { useMergeRefs } from "../../../util/hooks";
|
||||
import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
|
||||
import SelectedOptions from "../SelectedOptions/SelectedOptions";
|
||||
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
|
||||
import { ComboboxProps } from "../types";
|
||||
import Input from "./Input";
|
||||
import { useInputContext } from "./Input.context";
|
||||
import ToggleListButton from "./ToggleListButton";
|
||||
|
||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||
export const InputController = forwardRef<
|
||||
HTMLInputElement,
|
||||
Omit<
|
||||
ComboboxProps,
|
||||
| "label"
|
||||
| "description"
|
||||
| "hideLabel"
|
||||
| "onChange"
|
||||
| "options"
|
||||
| "size"
|
||||
| "onClear"
|
||||
| "value"
|
||||
>
|
||||
>((props, ref) => {
|
||||
const {
|
||||
clearButton = true,
|
||||
clearButtonLabel,
|
||||
toggleListButton = true,
|
||||
toggleListButtonLabel,
|
||||
inputClassName,
|
||||
shouldShowSelectedOptions = true,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const {
|
||||
clearInput,
|
||||
focusInput,
|
||||
inputProps,
|
||||
value,
|
||||
size = "medium",
|
||||
inputRef,
|
||||
toggleOpenButtonRef,
|
||||
} = useInputContext();
|
||||
|
||||
const { activeDecendantId } = useFilteredOptionsContext();
|
||||
const { selectedOptions } = useSelectedOptionsContext();
|
||||
|
||||
const mergedInputRef = useMergeRefs(inputRef, ref);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cl("navds-combobox__wrapper-inner navds-text-field__input", {
|
||||
"navds-combobox__wrapper-inner--virtually-unfocused":
|
||||
activeDecendantId !== undefined,
|
||||
})}
|
||||
onClick={focusInput}
|
||||
>
|
||||
{!shouldShowSelectedOptions ? (
|
||||
<Input
|
||||
id={inputProps.id}
|
||||
ref={mergedInputRef}
|
||||
inputClassName={inputClassName}
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
<SelectedOptions selectedOptions={selectedOptions} size={size}>
|
||||
<Input
|
||||
id={inputProps.id}
|
||||
ref={mergedInputRef}
|
||||
inputClassName={inputClassName}
|
||||
{...rest}
|
||||
/>
|
||||
</SelectedOptions>
|
||||
)}
|
||||
<div>
|
||||
{value && clearButton && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={clearInput}
|
||||
className="navds-combobox__button-clear"
|
||||
tabIndex={-1}
|
||||
>
|
||||
<span className="navds-sr-only">
|
||||
{clearButtonLabel ? clearButtonLabel : "Tøm"}
|
||||
</span>
|
||||
<XMarkIcon aria-hidden />
|
||||
</button>
|
||||
)}
|
||||
{toggleListButton && (
|
||||
<ToggleListButton
|
||||
toggleListButtonLabel={toggleListButtonLabel}
|
||||
ref={toggleOpenButtonRef}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { ChevronDownIcon, ChevronUpIcon } from "@navikt/aksel-icons";
|
||||
import { useFilteredOptionsContext } from "./FilteredOptions/filteredOptionsContext";
|
||||
import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
|
||||
|
||||
interface ToggleListButtonProps {
|
||||
toggleListButtonLabel?: string;
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from "react";
|
||||
import { Chips } from "../../../chips";
|
||||
import { useInputContext } from "../Input/inputContext";
|
||||
import { useInputContext } from "../Input/Input.context";
|
||||
import { ComboboxOption } from "../types";
|
||||
import { useSelectedOptionsContext } from "./selectedOptionsContext";
|
||||
|
||||
|
||||
@@ -1,17 +1,12 @@
|
||||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useMemo,
|
||||
useState,
|
||||
} from "react";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { createContext } from "../../../util/create-context";
|
||||
import { usePrevious } from "../../../util/hooks";
|
||||
import { useInputContext } from "../Input/inputContext";
|
||||
import { useInputContext } from "../Input/Input.context";
|
||||
import { isInList } from "../combobox-utils";
|
||||
import { useCustomOptionsContext } from "../customOptionsContext";
|
||||
import { useComboboxCustomOptions } from "../customOptionsContext";
|
||||
import { ComboboxOption, ComboboxProps, MaxSelected } from "../types";
|
||||
|
||||
type SelectedOptionsContextType = {
|
||||
type SelectedOptionsContextValue = {
|
||||
addSelectedOption: (option: ComboboxOption) => void;
|
||||
isMultiSelect?: boolean;
|
||||
removeSelectedOption: (option: ComboboxOption) => void;
|
||||
@@ -25,11 +20,10 @@ type SelectedOptionsContextType = {
|
||||
) => void;
|
||||
};
|
||||
|
||||
const SelectedOptionsContext = createContext<SelectedOptionsContextType>(
|
||||
{} as SelectedOptionsContextType,
|
||||
);
|
||||
const [SelectedOptionsContextProvider, useSelectedOptionsContext] =
|
||||
createContext<SelectedOptionsContextValue>();
|
||||
|
||||
export const SelectedOptionsProvider = ({
|
||||
const SelectedOptionsProvider = ({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
@@ -45,7 +39,7 @@ export const SelectedOptionsProvider = ({
|
||||
removeCustomOption,
|
||||
addCustomOption,
|
||||
setCustomOptions,
|
||||
} = useCustomOptionsContext();
|
||||
} = useComboboxCustomOptions();
|
||||
const {
|
||||
allowNewValues,
|
||||
isMultiSelect,
|
||||
@@ -149,18 +143,10 @@ export const SelectedOptionsProvider = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<SelectedOptionsContext.Provider value={selectedOptionsState}>
|
||||
<SelectedOptionsContextProvider {...selectedOptionsState}>
|
||||
{children}
|
||||
</SelectedOptionsContext.Provider>
|
||||
</SelectedOptionsContextProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useSelectedOptionsContext = () => {
|
||||
const context = useContext(SelectedOptionsContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useSelectedOptionsContext must be used within a SelectedOptionsProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
export { SelectedOptionsProvider, useSelectedOptionsContext };
|
||||
+1
-1
@@ -2,7 +2,7 @@ import {
|
||||
isInList,
|
||||
mapToComboboxOptionArray,
|
||||
toComboboxOption,
|
||||
} from "./combobox-utils";
|
||||
} from "../combobox-utils";
|
||||
|
||||
const list = [
|
||||
{ label: "Hjelpemidler", value: "HJE" },
|
||||
+2
-3
@@ -1,10 +1,9 @@
|
||||
/* eslint-disable testing-library/no-unnecessary-act -- https://kentcdodds.com/blog/fix-the-not-wrapped-in-act-warning */
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import { act, render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import React, { useId } from "react";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { UNSAFE_Combobox } from "./index";
|
||||
import { UNSAFE_Combobox } from "../index";
|
||||
|
||||
const options = [
|
||||
"banana",
|
||||
@@ -464,13 +464,13 @@ export const RemoveSelectedMultiSelectTest: StoryObject = {
|
||||
},
|
||||
};
|
||||
|
||||
export const AddWhenAddNewDisabledTest: StoryObject = {
|
||||
export const AllowNewValuesTest: StoryObject = {
|
||||
render: () => {
|
||||
return (
|
||||
<UNSAFE_Combobox
|
||||
options={options}
|
||||
label="Hva er dine favorittfrukter?"
|
||||
isMultiSelect
|
||||
allowNewValues
|
||||
/>
|
||||
);
|
||||
},
|
||||
@@ -497,6 +497,35 @@ export const AddWhenAddNewDisabledTest: StoryObject = {
|
||||
},
|
||||
};
|
||||
|
||||
export const DisallowNewValuesTest: StoryObject = {
|
||||
render: () => {
|
||||
return (
|
||||
<UNSAFE_Combobox options={options} label="Hva er dine favorittfrukter?" />
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
const input = canvas.getByLabelText("Hva er dine favorittfrukter?");
|
||||
|
||||
userEvent.click(input);
|
||||
await userEvent.type(input, "aaa", { delay: 200 });
|
||||
await sleep(250);
|
||||
|
||||
userEvent.keyboard("{ArrowDown}");
|
||||
await sleep(250);
|
||||
userEvent.keyboard("{ArrowDown}");
|
||||
await sleep(250);
|
||||
userEvent.keyboard("{Enter}");
|
||||
await sleep(250);
|
||||
userEvent.keyboard("{Escape}");
|
||||
await sleep(250);
|
||||
|
||||
const invalidSelect = canvas.queryByLabelText("aaa slett");
|
||||
expect(invalidSelect).not.toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const TestThatCallbacksOnlyFireWhenExpected: StoryObj<{
|
||||
onChange: ReturnType<typeof fn>;
|
||||
onClear: ReturnType<typeof fn>;
|
||||
@@ -624,3 +653,71 @@ export const TestHoverAndFocusSwitching: StoryObject = {
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const TestEnterNotSubmittingForm: StoryObj<{
|
||||
onSubmit: ReturnType<typeof fn>;
|
||||
}> = {
|
||||
args: {
|
||||
onSubmit: fn(),
|
||||
},
|
||||
render: ({ onSubmit }) => {
|
||||
return (
|
||||
<form action="https://www.aksel.nav.no" method="get" onSubmit={onSubmit}>
|
||||
<UNSAFE_Combobox
|
||||
options={options}
|
||||
label="Hva er dine favorittfrukter?"
|
||||
isMultiSelect
|
||||
allowNewValues
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement, args }) => {
|
||||
args.onSubmit.mockClear();
|
||||
const canvas = within(canvasElement);
|
||||
const waitTime = 0; // Change for debugging
|
||||
|
||||
await sleep(waitTime);
|
||||
|
||||
const getInput = () =>
|
||||
canvas.getByRole("combobox", {
|
||||
name: "Hva er dine favorittfrukter?",
|
||||
});
|
||||
|
||||
const getOption = (name: string, selected: boolean) =>
|
||||
canvas.getByRole("option", { name, selected });
|
||||
await userEvent.click(getInput(), { delay: waitTime });
|
||||
|
||||
await userEvent.keyboard("{ArrowDown}", { delay: waitTime });
|
||||
expect(getInput().getAttribute("aria-activedescendant")).toBe(
|
||||
getOption("banana", false).getAttribute("id"),
|
||||
);
|
||||
|
||||
await userEvent.keyboard("{Enter}", { delay: waitTime });
|
||||
expect(args.onSubmit).not.toHaveBeenCalled();
|
||||
expect(getOption("banana", true)).toBeVisible();
|
||||
|
||||
await userEvent.keyboard("{Shift>}{Tab}", { delay: waitTime });
|
||||
|
||||
await userEvent.keyboard("{Enter}", { delay: waitTime });
|
||||
expect(args.onSubmit).not.toHaveBeenCalled();
|
||||
|
||||
await userEvent.keyboard("test"); // Type option that does not exist
|
||||
await userEvent.keyboard("{Enter}", { delay: waitTime });
|
||||
expect(args.onSubmit).not.toHaveBeenCalled();
|
||||
|
||||
await userEvent.keyboard("{ArrowDown}", { delay: waitTime }); // Select "test" custom option
|
||||
expect(
|
||||
canvas.getByRole("option", { name: "test", selected: true }),
|
||||
).toBeVisible();
|
||||
await userEvent.keyboard("{Enter}", { delay: waitTime }); // De-select "test"
|
||||
expect(
|
||||
canvas.queryByRole("option", { name: "test", selected: false }),
|
||||
).toBeNull();
|
||||
expect(args.onSubmit).not.toHaveBeenCalled();
|
||||
|
||||
await userEvent.keyboard("{Escape}", { delay: waitTime }); // Clear input field
|
||||
await userEvent.keyboard("{Enter}", { delay: waitTime }); // Enter on empty Input with closed list
|
||||
expect(args.onSubmit).toHaveBeenCalledOnce();
|
||||
},
|
||||
};
|
||||
@@ -1,19 +1,23 @@
|
||||
import React, { createContext, useCallback, useContext, useState } from "react";
|
||||
import { useInputContext } from "./Input/inputContext";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { createContext } from "../../util/create-context";
|
||||
import { useInputContext } from "./Input/Input.context";
|
||||
import { ComboboxOption } from "./types";
|
||||
|
||||
type CustomOptionsContextType = {
|
||||
type CustomOptionsContextValue = {
|
||||
customOptions: ComboboxOption[];
|
||||
removeCustomOption: (option: ComboboxOption) => void;
|
||||
addCustomOption: (option: ComboboxOption) => void;
|
||||
setCustomOptions: React.Dispatch<React.SetStateAction<ComboboxOption[]>>;
|
||||
};
|
||||
|
||||
const CustomOptionsContext = createContext<CustomOptionsContextType>(
|
||||
{} as CustomOptionsContextType,
|
||||
);
|
||||
const [ComboboxCustomOptionsProvider, useComboboxCustomOptions] =
|
||||
createContext<CustomOptionsContextValue>({
|
||||
name: "ComboboxCustomOptions",
|
||||
errorMessage:
|
||||
"useComboboxCustomOptions must be used within a ComboboxCustomOptionsProvider",
|
||||
});
|
||||
|
||||
export const CustomOptionsProvider = ({
|
||||
const CustomOptionsProvider = ({
|
||||
children,
|
||||
value,
|
||||
}: {
|
||||
@@ -54,18 +58,10 @@ export const CustomOptionsProvider = ({
|
||||
};
|
||||
|
||||
return (
|
||||
<CustomOptionsContext.Provider value={customOptionsState}>
|
||||
<ComboboxCustomOptionsProvider {...customOptionsState}>
|
||||
{children}
|
||||
</CustomOptionsContext.Provider>
|
||||
</ComboboxCustomOptionsProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useCustomOptionsContext = () => {
|
||||
const context = useContext(CustomOptionsContext);
|
||||
if (!context) {
|
||||
throw new Error(
|
||||
"useCustomOptionsContext must be used within a CustomOptionsProvider",
|
||||
);
|
||||
}
|
||||
return context;
|
||||
};
|
||||
export { CustomOptionsProvider, useComboboxCustomOptions };
|
||||
@@ -100,7 +100,9 @@ export interface ComboboxProps
|
||||
*
|
||||
* @param event
|
||||
*/
|
||||
onClear?: (event: React.PointerEvent | React.KeyboardEvent) => void;
|
||||
onClear?: (
|
||||
event: React.PointerEvent | React.KeyboardEvent | React.MouseEvent,
|
||||
) => void;
|
||||
/**
|
||||
* Callback function triggered whenever an option is selected or de-selected.
|
||||
*
|
||||
|
||||
@@ -89,8 +89,8 @@ interface FileUploadComponent
|
||||
* ```jsx
|
||||
* <FileUpload.Item file={file} status="uploading" />
|
||||
* <FileUpload.Item file={file} status="downloading" />
|
||||
* <FileUpload.Item file={file} itemAction="retry" onRetry={...} />
|
||||
* <FileUpload.Item file={file} itemAction="delete" onDelete={...} />
|
||||
* <FileUpload.Item file={file} button={{ action:"retry", onClick:... }} />
|
||||
* <FileUpload.Item file={file} button={{ action:"delete", onClick:... }} />
|
||||
* ```
|
||||
*/
|
||||
Item: typeof Item;
|
||||
|
||||
@@ -57,51 +57,63 @@ export const States: StoryObj<typeof FileUpload.Item> = {
|
||||
file={fileTxt}
|
||||
error="Error og uploading"
|
||||
status="uploading"
|
||||
onRetry={() => onRetry(fileTxt)}
|
||||
onDelete={() => onDelete(fileTxt)}
|
||||
button={{
|
||||
onClick: () => onDelete(fileTxt),
|
||||
action: "delete",
|
||||
}}
|
||||
/>
|
||||
<h2>Item Actions</h2>
|
||||
<h3>status + delete</h3>
|
||||
<FileUpload.Item
|
||||
file={fileDocx}
|
||||
onDelete={() => onDelete(fileDocx)}
|
||||
onRetry={() => onRetry(fileDocx)}
|
||||
status="uploading"
|
||||
itemAction="delete"
|
||||
button={{
|
||||
onClick: () => onDelete(fileDocx),
|
||||
action: "delete",
|
||||
}}
|
||||
/>
|
||||
<h3>status + retry</h3>
|
||||
<FileUpload.Item
|
||||
file={fileDocx}
|
||||
onDelete={() => onDelete(fileDocx)}
|
||||
onRetry={() => onRetry(fileDocx)}
|
||||
status="downloading"
|
||||
itemAction="retry"
|
||||
button={{
|
||||
onClick: () => onRetry(fileDocx),
|
||||
action: "retry",
|
||||
}}
|
||||
/>
|
||||
<h3>retry</h3>
|
||||
<FileUpload.Item
|
||||
file={fileCsv}
|
||||
onRetry={() => onRetry(fileCsv)}
|
||||
itemAction="retry"
|
||||
button={{
|
||||
onClick: () => onRetry(fileCsv),
|
||||
action: "retry",
|
||||
}}
|
||||
/>
|
||||
<h3>delete</h3>
|
||||
<FileUpload.Item
|
||||
file={filePptx}
|
||||
onDelete={() => onDelete(filePptx)}
|
||||
itemAction="delete"
|
||||
button={{
|
||||
onClick: () => onDelete(filePptx),
|
||||
action: "delete",
|
||||
}}
|
||||
/>
|
||||
<h3>retry + error</h3>
|
||||
<FileUpload.Item
|
||||
file={fileCsv}
|
||||
error="Error og onRetry"
|
||||
onRetry={() => onRetry(fileCsv)}
|
||||
itemAction="retry"
|
||||
button={{
|
||||
onClick: () => onRetry(fileCsv),
|
||||
action: "retry",
|
||||
}}
|
||||
/>
|
||||
<h3>delete + error</h3>
|
||||
<FileUpload.Item
|
||||
file={filePptx}
|
||||
error="Error og onDelete"
|
||||
onDelete={() => onDelete(filePptx)}
|
||||
itemAction="delete"
|
||||
button={{
|
||||
onClick: () => onDelete(filePptx),
|
||||
action: "delete",
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
|
||||
@@ -25,6 +25,7 @@ const MAX_SIZE = MAX_SIZE_MB * 1024 * 1024;
|
||||
|
||||
const CustomItem = ({
|
||||
index,
|
||||
onDelete,
|
||||
...props
|
||||
}: FileUploadItemProps & {
|
||||
index: number;
|
||||
@@ -45,7 +46,10 @@ const CustomItem = ({
|
||||
<FileUpload.Item
|
||||
{...props}
|
||||
status={loading ? "uploading" : "idle"}
|
||||
itemAction="delete"
|
||||
button={{
|
||||
action: "delete",
|
||||
onClick: onDelete,
|
||||
}}
|
||||
as="li"
|
||||
/>
|
||||
);
|
||||
@@ -165,7 +169,10 @@ export const Single: StoryFn = () => {
|
||||
key={file.file.name}
|
||||
file={file.file}
|
||||
error={file.error ? errors[file.reasons[0]] : undefined}
|
||||
onDelete={removeFile}
|
||||
button={{
|
||||
action: "delete",
|
||||
onClick: removeFile,
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</VStack>
|
||||
@@ -194,12 +201,17 @@ export const Translation = () => (
|
||||
<FileUpload.Dropzone label="Last opp bilder" onSelect={console.log} />
|
||||
<FileUpload.Item
|
||||
file={{ name: "eksempel.png", size: 200000 }}
|
||||
onDelete={() => null}
|
||||
button={{
|
||||
action: "delete",
|
||||
onClick: () => null,
|
||||
}}
|
||||
/>
|
||||
<FileUpload.Item
|
||||
file={{ name: "eksempel.png", size: 200000 }}
|
||||
itemAction="retry"
|
||||
onRetry={() => null}
|
||||
button={{
|
||||
action: "retry",
|
||||
onClick: () => null,
|
||||
}}
|
||||
/>
|
||||
<FileUpload.Item
|
||||
file={{ name: "eksempel.png", size: 200000 }}
|
||||
|
||||
@@ -30,6 +30,7 @@ const Dropzone = forwardRef<HTMLInputElement, FileUploadDropzoneProps>(
|
||||
disabled,
|
||||
translations,
|
||||
onClick,
|
||||
id: buttonId,
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
@@ -46,7 +47,10 @@ const Dropzone = forwardRef<HTMLInputElement, FileUploadDropzoneProps>(
|
||||
const _disabled = disabled ?? fileLimitReached;
|
||||
|
||||
const { inputProps, errorId, showErrorMsg, hasError, inputDescriptionId } =
|
||||
useFormField({ ...props, disabled: _disabled }, "fileUpload");
|
||||
useFormField(
|
||||
{ ...omit(props, ["id"]), disabled: _disabled },
|
||||
"fileUpload",
|
||||
);
|
||||
const {
|
||||
id: inputId,
|
||||
"aria-describedby": ariaDescribedby,
|
||||
@@ -129,8 +133,9 @@ const Dropzone = forwardRef<HTMLInputElement, FileUploadDropzoneProps>(
|
||||
<BodyShort as="div">{translate("dropzone.or")}</BodyShort>
|
||||
</div>
|
||||
<Button
|
||||
{...omit(rest, ["errorId", "id"])}
|
||||
{...omit(rest, ["errorId"])}
|
||||
{...inputPropsRest}
|
||||
id={buttonId}
|
||||
aria-describedby={cl(labelId, ariaDescribedby)}
|
||||
className="navds-dropzone__area-button"
|
||||
type="button"
|
||||
|
||||
@@ -12,12 +12,13 @@ import ItemIcon from "./ItemIcon";
|
||||
import ItemName from "./ItemName";
|
||||
import { formatFileSize } from "./utils/format-file-size";
|
||||
|
||||
export interface FileItemBaseProps {
|
||||
export interface FileUploadItemProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Overrides html-tag
|
||||
* @default "div"
|
||||
*/
|
||||
as?: "div" | "li";
|
||||
as?: ("div" | "li") & React.ElementType;
|
||||
/**
|
||||
* Either a native File or file metadata.
|
||||
*/
|
||||
@@ -43,37 +44,20 @@ export interface FileItemBaseProps {
|
||||
* @default "idle"
|
||||
*/
|
||||
status?: "downloading" | "uploading" | "idle";
|
||||
/**
|
||||
* Props for the action button.
|
||||
*/
|
||||
button?: {
|
||||
action: "delete" | "retry";
|
||||
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
|
||||
id?: string;
|
||||
};
|
||||
/**
|
||||
* i18n-API for customizing texts and labels
|
||||
*/
|
||||
translations?: ComponentTranslation<"FileUpload">["item"];
|
||||
onRetry?: (event: MouseEvent<HTMLButtonElement>) => void;
|
||||
onDelete?: (event: MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
type FileItemActionDelete = {
|
||||
onDelete: (event: MouseEvent<HTMLButtonElement>) => void;
|
||||
itemAction: "delete";
|
||||
};
|
||||
|
||||
type FileItemActionRetry = {
|
||||
onRetry: (event: MouseEvent<HTMLButtonElement>) => void;
|
||||
itemAction: "retry";
|
||||
};
|
||||
|
||||
type FileItemActionNone = {
|
||||
itemAction?: "none";
|
||||
};
|
||||
|
||||
type FileItemConditionalProps =
|
||||
| FileItemActionDelete
|
||||
| FileItemActionRetry
|
||||
| FileItemActionNone;
|
||||
|
||||
export type FileUploadItemProps = FileItemBaseProps &
|
||||
FileItemConditionalProps &
|
||||
React.HTMLAttributes<HTMLDivElement>;
|
||||
|
||||
export const Item: OverridableComponent<FileUploadItemProps, HTMLDivElement> =
|
||||
forwardRef(
|
||||
(
|
||||
@@ -81,16 +65,14 @@ export const Item: OverridableComponent<FileUploadItemProps, HTMLDivElement> =
|
||||
as: Component = "div",
|
||||
file,
|
||||
status = "idle",
|
||||
onDelete,
|
||||
onRetry,
|
||||
error,
|
||||
className,
|
||||
href,
|
||||
onFileClick,
|
||||
itemAction = "delete",
|
||||
button,
|
||||
translations,
|
||||
...rest
|
||||
},
|
||||
}: FileUploadItemProps,
|
||||
ref,
|
||||
) => {
|
||||
const context = useFileUploadTranslation(false);
|
||||
@@ -148,14 +130,14 @@ export const Item: OverridableComponent<FileUploadItemProps, HTMLDivElement> =
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{status === "idle" && (
|
||||
{status === "idle" && button && (
|
||||
<ItemButton
|
||||
file={file}
|
||||
onRetry={onRetry}
|
||||
onDelete={onDelete}
|
||||
action={itemAction}
|
||||
retryTitle={translate("item.retryButtonTitle")}
|
||||
deleteTitle={translate("item.deleteButtonTitle")}
|
||||
{...button}
|
||||
title={translate(
|
||||
button.action === "retry"
|
||||
? "item.retryButtonTitle"
|
||||
: "item.deleteButtonTitle",
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,52 +1,27 @@
|
||||
import React from "react";
|
||||
import { ArrowsCirclepathIcon, TrashIcon } from "@navikt/aksel-icons";
|
||||
import { Button } from "../../../../button";
|
||||
import { FileItem } from "./Item.types";
|
||||
|
||||
interface Props {
|
||||
file: FileItem;
|
||||
onRetry?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
onDelete?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
action: "delete" | "retry" | "none";
|
||||
retryTitle: string;
|
||||
deleteTitle: string;
|
||||
action: "delete" | "retry";
|
||||
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
id?: string;
|
||||
title: string;
|
||||
}
|
||||
|
||||
const ItemButton = ({
|
||||
onRetry,
|
||||
onDelete,
|
||||
action,
|
||||
retryTitle,
|
||||
deleteTitle,
|
||||
}: Props) => {
|
||||
if (action === "none") {
|
||||
return null;
|
||||
}
|
||||
const ItemButton = ({ action, onClick, id, title }: Props) => {
|
||||
const Icon = action === "delete" ? TrashIcon : ArrowsCirclepathIcon;
|
||||
|
||||
if (onRetry && action === "retry") {
|
||||
return (
|
||||
<Button
|
||||
className="navds-file-item__button"
|
||||
type="button"
|
||||
variant="tertiary-neutral"
|
||||
onClick={onRetry}
|
||||
icon={<ArrowsCirclepathIcon title={retryTitle} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (onDelete && action === "delete") {
|
||||
return (
|
||||
<Button
|
||||
className="navds-file-item__button"
|
||||
type="button"
|
||||
variant="tertiary-neutral"
|
||||
onClick={onDelete}
|
||||
icon={<TrashIcon title={deleteTitle} />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
return (
|
||||
<Button
|
||||
id={id}
|
||||
className="navds-file-item__button"
|
||||
type="button"
|
||||
variant="tertiary-neutral"
|
||||
onClick={onClick}
|
||||
icon={<Icon title={title} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ItemButton;
|
||||
@@ -0,0 +1,388 @@
|
||||
import { Meta, StoryFn, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { LinkIcon } from "@navikt/aksel-icons";
|
||||
import { VStack } from "../../layout/stack";
|
||||
import { Link } from "../../link";
|
||||
import FormSummary from "./FormSummary";
|
||||
|
||||
const meta: Meta<typeof FormSummary> = {
|
||||
title: "ds-react/FormSummary",
|
||||
component: FormSummary,
|
||||
decorators: [
|
||||
(story) => (
|
||||
<div style={{ width: 600, maxWidth: "100%", margin: "0 auto" }}>
|
||||
{story()}
|
||||
</div>
|
||||
),
|
||||
],
|
||||
parameters: { layout: "padded", chromatic: { disable: true } },
|
||||
};
|
||||
export default meta;
|
||||
|
||||
export const Default: StoryFn<typeof FormSummary> = () => (
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Personalia</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Navn</FormSummary.Label>
|
||||
<FormSummary.Value>Ola Nordmann</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Adresse</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
Gate 123
|
||||
<br />
|
||||
1234 Sted
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Fødselsnummer</FormSummary.Label>
|
||||
<FormSummary.Value>12345678910</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Barn nr. 1</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Navn</FormSummary.Label>
|
||||
<FormSummary.Value>Kari Nordmann</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Kjønn</FormSummary.Label>
|
||||
<FormSummary.Value>Jente</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Alder</FormSummary.Label>
|
||||
<FormSummary.Value>6</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Barn nr. 2</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Navn</FormSummary.Label>
|
||||
<FormSummary.Value>Per Nordmann</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Kjønn</FormSummary.Label>
|
||||
<FormSummary.Value>Gutt</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Alder</FormSummary.Label>
|
||||
<FormSummary.Value>7</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Kontaktinformasjon</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
<Link>Fyll ut kontaktinformasjon</Link>
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Hvordan vil du bli varslet?</FormSummary.Label>
|
||||
<FormSummary.Value>E-post</FormSummary.Value>
|
||||
<FormSummary.Value>SMS</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
);
|
||||
|
||||
export const LongTexts: StoryFn<typeof FormSummary> = () => (
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">
|
||||
Arbeidsforhold som du har i eller utenfor EØS-området
|
||||
</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#">
|
||||
<LinkIcon />
|
||||
Jeg vil endre dette svaret
|
||||
</FormSummary.EditLink>
|
||||
</FormSummary.Header>
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>
|
||||
Har du jobbet i et annet EØS-land, Sveits eller Storbritannia i løpet
|
||||
av de siste 36 månedene? Om ja, hvilket land og hvor lenge jobbet du
|
||||
der? Kan du fortelle litt om hva du jobbet med?
|
||||
</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
Nei, jeg har ikke jobbet i et annet EØS-land, Sveits eller
|
||||
Storbritannia i løpet av de siste 36 månedene. Og jeg har heller ikke
|
||||
jobbet i Norge i løpet av de siste 36 månedene. Men jeg har jobbet i
|
||||
et annet land utenfor EØS-området. Det var i Sør-Korea, og jeg jobbet
|
||||
som lærer på en internasjonal skole. Jeg jobbet der i 12 måneder, og
|
||||
det var en veldig spennende opplevelse. Jeg lærte mye om koreansk
|
||||
kultur og språk, og jeg fikk mange nye venner. Jeg savner Sør-Korea
|
||||
veldig mye, og jeg håper å kunne dra tilbake.
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>
|
||||
Har du jobbet i et annet EØS-land, Sveits eller Storbritannia i
|
||||
løpet av de siste 36 månedene? Om ja, hvilket land og hvor lenge
|
||||
jobbet du der? Kan du fortelle litt om hva du jobbet med?
|
||||
</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
Nei, jeg har ikke jobbet i et annet EØS-land, Sveits eller
|
||||
Storbritannia i løpet av de siste 36 månedene. Og jeg har heller
|
||||
ikke jobbet i Norge i løpet av de siste 36 månedene. Men jeg har
|
||||
jobbet i et annet land utenfor EØS-området. Det var i Sør-Korea,
|
||||
og jeg jobbet som lærer på en internasjonal skole. Jeg jobbet
|
||||
der i 12 måneder, og det var en veldig spennende opplevelse. Jeg
|
||||
lærte mye om koreansk kultur og språk, og jeg fikk mange nye
|
||||
venner. Jeg savner Sør-Korea veldig mye, og jeg håper å kunne
|
||||
dra tilbake.
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
);
|
||||
|
||||
export const NoLink: StoryFn<typeof FormSummary> = () => (
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">
|
||||
Arbeidsforhold som du har i eller utenfor EØS-området
|
||||
</FormSummary.Heading>
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Navn</FormSummary.Label>
|
||||
<FormSummary.Value>Ola Nordmann</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Fødselsnummer</FormSummary.Label>
|
||||
<FormSummary.Value>12345678910</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
);
|
||||
|
||||
const answers = [
|
||||
{
|
||||
label: "Fortell din livshistorie",
|
||||
value: `Det hele startet da jeg ble født en kald vinterdag i desember.
|
||||
Jeg var en litt spesiell baby, for jeg hadde masse hår på hodet. Mamma
|
||||
og pappa ble veldig overrasket da de så meg for første gang. De hadde egentlig
|
||||
forventet at jeg skulle være helt normal og kjedelig. Du vil kanskje ikke høre resten
|
||||
av historien min, for den er ganske lang og kjedelig...`,
|
||||
},
|
||||
{
|
||||
label: "Fødselsnummer",
|
||||
value: "12345678910",
|
||||
},
|
||||
{
|
||||
label: "Ting du har i sekken.",
|
||||
value: [
|
||||
{
|
||||
label: "Saks x2",
|
||||
value: `Jeg liker å ha med meg en saks i sekken min. Det er alltid kjekt å ha en saks. Jeg pakker gjerne med meg to saks. Da har jeg en reserve hvis den første saksen min skulle bli borte. Det er veldig kjekt.`,
|
||||
},
|
||||
{
|
||||
label: "Blyant",
|
||||
value:
|
||||
"Det er ikke mulig å skrive uten en blyant. Da blir det bare rot.",
|
||||
},
|
||||
{
|
||||
label: "Kake",
|
||||
value: `Jeg har alltid en kake i sekken min. Den blir litt most etter en
|
||||
stund i sekken min men det gjør ingenting. Jeg liker moste kaker. Det er veldig godt.`,
|
||||
},
|
||||
{
|
||||
label: "Kaffekopp",
|
||||
value: "Dette har jeg ikke i sekken min",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
label: "Flere ting du har i sekken.",
|
||||
value: [
|
||||
{
|
||||
label: "Saks",
|
||||
value: `En saks til`,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export const RealisticUsage: StoryFn<typeof FormSummary> = () => (
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">
|
||||
Diverse informasjon om forskjellige ting og tang. Kjekt å vite.
|
||||
</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
{answers.map((answer, index) => (
|
||||
<FormSummary.Answer key={index}>
|
||||
<FormSummary.Label>{answer.label}</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
{Array.isArray(answer.value) ? (
|
||||
<FormSummary.Answers>
|
||||
{answer.value.map((subAnswer, subIndex) => (
|
||||
<FormSummary.Answer key={subIndex}>
|
||||
<FormSummary.Label>{subAnswer.label}</FormSummary.Label>
|
||||
<FormSummary.Value>{subAnswer.value}</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
))}
|
||||
</FormSummary.Answers>
|
||||
) : (
|
||||
answer.value
|
||||
)}
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
))}
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
);
|
||||
|
||||
export const Empty: StoryFn<typeof FormSummary> = () => (
|
||||
<VStack gap="8">
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Just Header</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
</FormSummary>
|
||||
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Empty Answers</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>{null}</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Empty Answer</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>{null}</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Empty Label & Value</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>{null}</FormSummary.Label>
|
||||
<FormSummary.Value>{null}</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Empty Value</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Adresse</FormSummary.Label>
|
||||
<FormSummary.Value>{null}</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
</VStack>
|
||||
);
|
||||
|
||||
export const Chromatic: StoryObj<typeof FormSummary> = {
|
||||
render: () => (
|
||||
<div>
|
||||
<div>
|
||||
<h2>Default</h2>
|
||||
<Default>{null}</Default>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Long Texts</h2>
|
||||
<LongTexts>{null}</LongTexts>
|
||||
</div>
|
||||
<div>
|
||||
<h2>No Link</h2>
|
||||
<NoLink>{null}</NoLink>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Realistic Usage</h2>
|
||||
<RealisticUsage>{null}</RealisticUsage>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Empty</h2>
|
||||
<Empty>{null}</Empty>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
parameters: {
|
||||
chromatic: {
|
||||
disable: false,
|
||||
modes: {
|
||||
default: {
|
||||
viewport: {
|
||||
width: 620,
|
||||
},
|
||||
},
|
||||
sm: {
|
||||
viewport: {
|
||||
width: 479,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const CommaSeparated: StoryFn<typeof FormSummary> = () => (
|
||||
<FormSummary>
|
||||
<FormSummary.Header>
|
||||
<FormSummary.Heading level="2">Personalia</FormSummary.Heading>
|
||||
<FormSummary.EditLink href="#" />
|
||||
</FormSummary.Header>
|
||||
|
||||
<FormSummary.Answers>
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>
|
||||
Hvilket transportmiddel er du mest kjent for?
|
||||
</FormSummary.Label>
|
||||
<FormSummary.Value>
|
||||
sykkel, tog, bil, buss, sparkesykkel
|
||||
</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
|
||||
<FormSummary.Answer>
|
||||
<FormSummary.Label>Favoritt slagord?</FormSummary.Label>
|
||||
<FormSummary.Value>{`I'll be back.`}</FormSummary.Value>
|
||||
<FormSummary.Value>Hasta la vista, baby.</FormSummary.Value>
|
||||
<FormSummary.Value>Say hello to my little friend.</FormSummary.Value>
|
||||
</FormSummary.Answer>
|
||||
</FormSummary.Answers>
|
||||
</FormSummary>
|
||||
);
|
||||
@@ -0,0 +1,106 @@
|
||||
import cl from "clsx";
|
||||
import React, { HTMLAttributes, forwardRef } from "react";
|
||||
import FormSummaryAnswer from "./FormSummaryAnswer";
|
||||
import FormSummaryAnswers from "./FormSummaryAnswers";
|
||||
import FormSummaryEditLink from "./FormSummaryEditLink";
|
||||
import FormSummaryHeader from "./FormSummaryHeader";
|
||||
import FormSummaryHeading from "./FormSummaryHeading";
|
||||
import FormSummaryLabel from "./FormSummaryLabel";
|
||||
import FormSummaryValue from "./FormSummaryValue";
|
||||
|
||||
interface FormSummaryComponent
|
||||
extends React.ForwardRefExoticComponent<
|
||||
FormSummaryProps & React.RefAttributes<HTMLDivElement>
|
||||
> {
|
||||
/**
|
||||
* Must include `<FormSummary.Heading>` and optionally `<FormSummary.EditLink>`.
|
||||
*/
|
||||
Header: typeof FormSummaryHeader;
|
||||
/**
|
||||
* Typographic Heading to use in the `FormSummary.Header` component.
|
||||
*/
|
||||
Heading: typeof FormSummaryHeading;
|
||||
/**
|
||||
* Link to edit the answers to use in the `FormSummary.Header` component. Should link to the relevant part of the form.
|
||||
*/
|
||||
EditLink: typeof FormSummaryEditLink;
|
||||
/**
|
||||
* Wrapper component for the answers.
|
||||
*/
|
||||
Answers: typeof FormSummaryAnswers;
|
||||
/**
|
||||
* Wrapper component for each answer. To be used in the `FormSummary.Answers` component.
|
||||
*/
|
||||
Answer: typeof FormSummaryAnswer;
|
||||
/**
|
||||
* Corresponds to the question in the form. To be used in the `FormSummary.Answer` component.
|
||||
*/
|
||||
Label: typeof FormSummaryLabel;
|
||||
/**
|
||||
* Corresponds to the answer in the form. To be used in the `FormSummary.Answer` component.
|
||||
*/
|
||||
Value: typeof FormSummaryValue;
|
||||
}
|
||||
|
||||
export interface FormSummaryProps extends HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Must include:
|
||||
*
|
||||
* - `<FormSummary.Header>`
|
||||
* - `<FormSummary.Answers>`
|
||||
*
|
||||
* @example
|
||||
* <FormSummary>
|
||||
* <FormSummary.Header>
|
||||
* <FormSummary.Heading level="2">HeadingTekst</FormSummary.Heading>
|
||||
* <FormSummary.EditLink href="#" />
|
||||
* </FormSummary.Header>
|
||||
* <FormSummary.Answers>
|
||||
* <FormSummary.Answer>
|
||||
* <FormSummary.Label>Navn</FormSummary.Label>
|
||||
* <FormSummary.Value>Ola Nordmann</FormSummary.Value>
|
||||
* </FormSummary.Answer>
|
||||
* </FormSummary.Answers>
|
||||
* </FormSummary>
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* A summary of a previously answered form.
|
||||
*
|
||||
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/formsummary)
|
||||
*
|
||||
* @example
|
||||
* <FormSummary>
|
||||
* <FormSummary.Header>
|
||||
* <FormSummary.Heading level="2">HeadingTekst</FormSummary.Heading>
|
||||
* <FormSummary.EditLink href="#" />
|
||||
* </FormSummary.Header>
|
||||
* <FormSummary.Answers>
|
||||
* <FormSummary.Answer>
|
||||
* <FormSummary.Label>Navn</FormSummary.Label>
|
||||
* <FormSummary.Value>Ola Nordmann</FormSummary.Value>
|
||||
* </FormSummary.Answer>
|
||||
* </FormSummary.Answers>
|
||||
* </FormSummary>
|
||||
*/
|
||||
export const FormSummary = forwardRef<HTMLDivElement, FormSummaryProps>(
|
||||
({ children, className, ...rest }, ref) => {
|
||||
return (
|
||||
<div ref={ref} {...rest} className={cl("navds-form-summary", className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
) as FormSummaryComponent;
|
||||
|
||||
FormSummary.Header = FormSummaryHeader;
|
||||
FormSummary.Heading = FormSummaryHeading;
|
||||
FormSummary.EditLink = FormSummaryEditLink;
|
||||
FormSummary.Answers = FormSummaryAnswers;
|
||||
FormSummary.Answer = FormSummaryAnswer;
|
||||
FormSummary.Label = FormSummaryLabel;
|
||||
FormSummary.Value = FormSummaryValue;
|
||||
|
||||
export default FormSummary;
|
||||
@@ -0,0 +1,27 @@
|
||||
import cl from "clsx";
|
||||
import React from "react";
|
||||
|
||||
export interface FormSummaryAnswerProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Must include:
|
||||
* - `<FormSummary.Label>`
|
||||
* - `<FormSummary.Value>`
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormSummaryAnswer = React.forwardRef<
|
||||
HTMLDivElement,
|
||||
FormSummaryAnswerProps
|
||||
>(({ children, className, ...rest }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={cl("navds-form-summary__answer", className)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
));
|
||||
|
||||
export default FormSummaryAnswer;
|
||||
@@ -0,0 +1,25 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
export interface FormSummaryAnswersProps
|
||||
extends React.HTMLAttributes<HTMLDListElement> {
|
||||
/**
|
||||
* Must include one or more of `<FormSummary.Answer>`.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormSummaryAnswers = forwardRef<
|
||||
HTMLDListElement,
|
||||
FormSummaryAnswersProps
|
||||
>(({ children, className, ...rest }: FormSummaryAnswersProps, ref) => (
|
||||
<dl
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={cl("navds-form-summary__answers", className)}
|
||||
>
|
||||
{children}
|
||||
</dl>
|
||||
));
|
||||
|
||||
export default FormSummaryAnswers;
|
||||
@@ -0,0 +1,35 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import { Link } from "../../link";
|
||||
import { OverridableComponent } from "../../util/types";
|
||||
|
||||
// export type FormSummaryEditProps = Partial<LinkProps>;
|
||||
export interface FormSummaryEditProps
|
||||
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
|
||||
/**
|
||||
* URL to the relevant part of the form, where the answers can be edited.
|
||||
*/
|
||||
href?: string;
|
||||
/**
|
||||
* Link text.
|
||||
*
|
||||
* [How to write good links](https://aksel.nav.no/god-praksis/artikler/lenker).
|
||||
* @default "Endre svar"
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormSummaryEditLink: OverridableComponent<
|
||||
FormSummaryEditProps,
|
||||
HTMLAnchorElement
|
||||
> = forwardRef(({ children = "Endre svar", className, ...rest }, ref) => (
|
||||
<Link
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={cl("navds-form-summary__edit", className)}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
));
|
||||
|
||||
export default FormSummaryEditLink;
|
||||
@@ -0,0 +1,25 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
|
||||
export interface FormSummaryHeaderProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Must include `<FormSummary.Heading>` and optionally `<FormSummary.EditLink>`.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormSummaryHeader = forwardRef<
|
||||
HTMLDivElement,
|
||||
FormSummaryHeaderProps
|
||||
>(({ children, className, ...rest }, ref) => (
|
||||
<header
|
||||
ref={ref}
|
||||
{...rest}
|
||||
className={cl("navds-form-summary__header", className)}
|
||||
>
|
||||
{children}
|
||||
</header>
|
||||
));
|
||||
|
||||
export default FormSummaryHeader;
|
||||
@@ -0,0 +1,23 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { Heading, HeadingProps } from "../../typography";
|
||||
|
||||
export interface FormSummaryHeadingProps
|
||||
extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
/**
|
||||
* Heading text.
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* The heading level.
|
||||
*/
|
||||
level: Exclude<HeadingProps["level"], "1">;
|
||||
}
|
||||
|
||||
export const FormSummaryHeading = forwardRef<
|
||||
HTMLHeadingElement,
|
||||
FormSummaryHeadingProps
|
||||
>((props: FormSummaryHeadingProps, ref) => (
|
||||
<Heading ref={ref} {...props} size="medium" />
|
||||
));
|
||||
|
||||
export default FormSummaryHeading;
|
||||
@@ -0,0 +1,17 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { Label } from "../../typography";
|
||||
|
||||
export interface FormSummaryLabelProps
|
||||
extends React.HTMLAttributes<HTMLElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormSummaryLabel = forwardRef<HTMLElement, FormSummaryLabelProps>(
|
||||
({ children, ...rest }, ref) => (
|
||||
<Label ref={ref} {...rest} as="dt">
|
||||
{children}
|
||||
</Label>
|
||||
),
|
||||
);
|
||||
|
||||
export default FormSummaryLabel;
|
||||
@@ -0,0 +1,24 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import { BodyLong } from "../../typography";
|
||||
|
||||
export interface FormSummaryValueProps
|
||||
extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const FormSummaryValue = forwardRef<
|
||||
HTMLDivElement,
|
||||
FormSummaryValueProps
|
||||
>(({ children, className, ...rest }, ref) => (
|
||||
<BodyLong
|
||||
ref={ref}
|
||||
{...rest}
|
||||
as="dd"
|
||||
className={cl("navds-form-summary__value", className)}
|
||||
>
|
||||
{children}
|
||||
</BodyLong>
|
||||
));
|
||||
|
||||
export default FormSummaryValue;
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
export { default as FormSummary, type FormSummaryProps } from "./FormSummary";
|
||||
export {
|
||||
default as FormSummaryAnswer,
|
||||
type FormSummaryAnswerProps,
|
||||
} from "./FormSummaryAnswer";
|
||||
export {
|
||||
default as FormSummaryAnswers,
|
||||
type FormSummaryAnswersProps,
|
||||
} from "./FormSummaryAnswers";
|
||||
export {
|
||||
default as FormSummaryEditLink,
|
||||
type FormSummaryEditProps,
|
||||
} from "./FormSummaryEditLink";
|
||||
export {
|
||||
default as FormSummaryHeader,
|
||||
type FormSummaryHeaderProps,
|
||||
} from "./FormSummaryHeader";
|
||||
export {
|
||||
default as FormSummaryHeading,
|
||||
type FormSummaryHeadingProps,
|
||||
} from "./FormSummaryHeading";
|
||||
export {
|
||||
default as FormSummaryLabel,
|
||||
type FormSummaryLabelProps,
|
||||
} from "./FormSummaryLabel";
|
||||
export {
|
||||
default as FormSummaryValue,
|
||||
type FormSummaryValueProps,
|
||||
} from "./FormSummaryValue";
|
||||
@@ -18,7 +18,7 @@ export const RadioGroupContext =
|
||||
export interface RadioGroupProps
|
||||
extends Omit<
|
||||
FieldsetProps,
|
||||
"onChange" | "errorPropagation" | "defaultValue"
|
||||
"onChange" | "errorPropagation" | "defaultValue" | "nativeReadOnly"
|
||||
> {
|
||||
/**
|
||||
* Collection of `<Radio />`-elements
|
||||
|
||||
@@ -9,11 +9,11 @@ export interface RadioProps
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* The value of the HTML element.
|
||||
* Radio value.
|
||||
*/
|
||||
value: any;
|
||||
/**
|
||||
* Adds a description to extend labeling of Radio.
|
||||
* Adds a description to extend the labeling.
|
||||
*/
|
||||
description?: string;
|
||||
}
|
||||
@@ -85,7 +85,7 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
||||
...rest
|
||||
} = props;
|
||||
|
||||
const [checked, setChecked] = useState(
|
||||
const [_checked, setChecked] = useState(
|
||||
defaultChecked ?? checkedProp ?? false,
|
||||
);
|
||||
|
||||
@@ -93,6 +93,8 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
||||
checkedProp !== undefined && setChecked(checkedProp);
|
||||
}, [checkedProp]);
|
||||
|
||||
const checked = checkedProp ?? _checked;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cl(
|
||||
|
||||
@@ -5,8 +5,7 @@ import { FieldsetContext } from "./fieldset/context";
|
||||
|
||||
export interface FormFieldProps {
|
||||
/**
|
||||
* Adds a description to extend labeling of the field.
|
||||
* Error message for element.
|
||||
* Error message.
|
||||
*/
|
||||
error?: React.ReactNode;
|
||||
/**
|
||||
@@ -24,7 +23,7 @@ export interface FormFieldProps {
|
||||
*/
|
||||
disabled?: boolean;
|
||||
/**
|
||||
* Adds a description to extend labeling of a field.
|
||||
* Adds a description to extend the labeling.
|
||||
*/
|
||||
description?: React.ReactNode;
|
||||
/**
|
||||
|
||||
@@ -41,16 +41,14 @@ export const PosterVariants = {
|
||||
render: () => (
|
||||
<VStack gap="6" align="start">
|
||||
<GuidePanel>
|
||||
If you exclude the <code>poster</code> prop, you will get the poster
|
||||
variant on mobile (<480px) and the non-poster variant otherwise.
|
||||
If you exclude the `poster` prop, you will get the poster variant on
|
||||
mobile (<480px) and the non-poster variant otherwise.
|
||||
</GuidePanel>
|
||||
<GuidePanel poster>
|
||||
Use the <code>poster</code> prop to get the poster variant on all
|
||||
viewports.
|
||||
Use the `poster` prop to get the poster variant on all viewports.
|
||||
</GuidePanel>
|
||||
<GuidePanel poster={false}>
|
||||
Set <code>poster=false</code> to get the non-poster variant on all
|
||||
viewports.
|
||||
Set `poster=false` to get the non-poster variant on all viewports.
|
||||
</GuidePanel>
|
||||
</VStack>
|
||||
),
|
||||
|
||||
@@ -26,8 +26,8 @@ export { Dropdown, type DropdownProps } from "./dropdown";
|
||||
export { ExpansionCard, type ExpansionCardProps } from "./expansion-card";
|
||||
export {
|
||||
GuidePanel,
|
||||
type GuidePanelProps,
|
||||
GuidePanelDefaultIllustration,
|
||||
type GuidePanelProps,
|
||||
} from "./guide-panel";
|
||||
export { HelpText, type HelpTextProps } from "./help-text";
|
||||
export {
|
||||
@@ -59,6 +59,7 @@ export { Modal, type ModalProps } from "./modal";
|
||||
export { Pagination, type PaginationProps } from "./pagination";
|
||||
export { Popover, type PopoverProps } from "./popover";
|
||||
export { Portal, type PortalProps } from "./portal";
|
||||
export { ProgressBar, type ProgressBarProps } from "./progress-bar";
|
||||
export { Provider, type ProviderProps } from "./provider";
|
||||
export { ReadMore, type ReadMoreProps } from "./read-more";
|
||||
export { Skeleton, type SkeletonProps } from "./skeleton";
|
||||
@@ -125,6 +126,21 @@ export {
|
||||
} from "./form/confirmation-panel";
|
||||
export { ErrorSummary, type ErrorSummaryProps } from "./form/error-summary";
|
||||
export { Fieldset, type FieldsetProps } from "./form/fieldset";
|
||||
export {
|
||||
UNSAFE_FileUpload,
|
||||
type FileAccepted,
|
||||
type FileItem,
|
||||
type FileMetadata,
|
||||
type FileObject,
|
||||
type FileRejected,
|
||||
type FileRejectedPartitioned,
|
||||
type FileRejectionReason,
|
||||
type FileUploadDropzoneProps,
|
||||
type FileUploadItemProps,
|
||||
type FileUploadTriggerProps,
|
||||
type FilesPartitioned,
|
||||
} from "./form/file-upload";
|
||||
export { FormSummary, type FormSummaryProps } from "./form/form-summary";
|
||||
export {
|
||||
Radio,
|
||||
RadioGroup,
|
||||
@@ -136,20 +152,6 @@ export { Select, type SelectProps } from "./form/select";
|
||||
export { Switch, type SwitchProps } from "./form/switch";
|
||||
export { Textarea, type TextareaProps } from "./form/textarea";
|
||||
export { TextField, type TextFieldProps } from "./form/textfield";
|
||||
export {
|
||||
UNSAFE_FileUpload,
|
||||
type FileUploadDropzoneProps,
|
||||
type FileUploadTriggerProps,
|
||||
type FileObject,
|
||||
type FileRejected,
|
||||
type FileAccepted,
|
||||
type FileRejectedPartitioned,
|
||||
type FilesPartitioned,
|
||||
type FileRejectionReason,
|
||||
type FileUploadItemProps,
|
||||
type FileItem,
|
||||
type FileMetadata,
|
||||
} from "./form/file-upload";
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta } from "@storybook/react";
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
import {
|
||||
ChevronDownIcon,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
LeaveIcon,
|
||||
MenuGridIcon,
|
||||
} from "@navikt/aksel-icons";
|
||||
import { BodyLong, BodyShort, Detail, Spacer } from "@navikt/ds-react";
|
||||
import { BodyLong, BodyShort, Detail, Spacer, VStack } from "@navikt/ds-react";
|
||||
import { Dropdown } from "../dropdown";
|
||||
import InternalHeader from "./InternalHeader";
|
||||
|
||||
@@ -22,8 +22,13 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<typeof InternalHeader>;
|
||||
|
||||
export const Default = {
|
||||
render: (props) => {
|
||||
return (
|
||||
@@ -227,3 +232,37 @@ export const UserWithMenuGridIconMenu = () => (
|
||||
<InternalHeader.User name="Ola Normann" description="id: 123456" />
|
||||
</InternalHeader>
|
||||
);
|
||||
|
||||
export const Chromatic: Story = {
|
||||
render: () => (
|
||||
<VStack gap="4">
|
||||
<div>
|
||||
<h2>TitleAsHeading</h2>
|
||||
<TitleAsHeading />
|
||||
</div>
|
||||
<div>
|
||||
<h2>TitleAsLink</h2>
|
||||
<TitleAsLink />
|
||||
</div>
|
||||
<div>
|
||||
<h2>User</h2>
|
||||
<User />
|
||||
</div>
|
||||
<div>
|
||||
<h2>UserWithDescription</h2>
|
||||
<UserWithDescription />
|
||||
</div>
|
||||
<div>
|
||||
<h2>UserWithMenu</h2>
|
||||
<UserWithMenu />
|
||||
</div>
|
||||
<div>
|
||||
<h2>UserWithMenuGridIconMenu</h2>
|
||||
<UserWithMenuGridIconMenu />
|
||||
</div>
|
||||
</VStack>
|
||||
),
|
||||
parameters: {
|
||||
chromatic: { disable: false },
|
||||
},
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Meta } from "@storybook/react";
|
||||
import type { Meta, StoryObj } from "@storybook/react";
|
||||
import React, { ReactNode } from "react";
|
||||
import { ChevronRightIcon } from "@navikt/aksel-icons";
|
||||
import { BodyLong, Detail, Heading } from "../../typography";
|
||||
@@ -10,9 +10,15 @@ import { Box } from "./Box";
|
||||
const meta: Meta<typeof Box> = {
|
||||
title: "ds-react/Primitives/Box",
|
||||
component: Box,
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Box>;
|
||||
|
||||
export const Default = () => (
|
||||
<Box>
|
||||
<BodyLong>
|
||||
@@ -378,3 +384,77 @@ export const PaddingDemo = () => (
|
||||
</Box>
|
||||
</VStack>
|
||||
);
|
||||
|
||||
export const Chromatic: Story = {
|
||||
render: () => (
|
||||
<VStack gap="2">
|
||||
<div>
|
||||
<h2>Default</h2>
|
||||
<Default />
|
||||
</div>
|
||||
<div>
|
||||
<h2>As card</h2>
|
||||
<AsCard />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Theming demo</h2>
|
||||
<ThemingDemo />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Padding breakpoints</h2>
|
||||
<PaddingBreakpoints.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Padding breakpoints inherit 1</h2>
|
||||
<PaddingBreakpointsInherit1.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Padding breakpoints inherit 2</h2>
|
||||
<PaddingBreakpointsInherit2 />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Padding</h2>
|
||||
<Padding />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Box in box</h2>
|
||||
<BoxInBox />
|
||||
</div>
|
||||
<div>
|
||||
<h2>With HGrid</h2>
|
||||
<WithHGrid />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Border width</h2>
|
||||
<BorderWidth />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Border radius</h2>
|
||||
<BorderRadius />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Padding demo</h2>
|
||||
<PaddingDemo />
|
||||
</div>
|
||||
</VStack>
|
||||
),
|
||||
parameters: {
|
||||
chromatic: {
|
||||
disable: false,
|
||||
modes: {
|
||||
mobile_portrait: {
|
||||
viewport: {
|
||||
width: 400,
|
||||
height: 850,
|
||||
},
|
||||
},
|
||||
desktop: {
|
||||
viewport: {
|
||||
width: 1280,
|
||||
height: 960,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,3 +1,4 @@
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { HGrid } from ".";
|
||||
import { VStack } from "../stack";
|
||||
@@ -13,6 +14,7 @@ export default {
|
||||
component: HGrid,
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
argTypes: {
|
||||
columnsType: {
|
||||
@@ -21,7 +23,9 @@ export default {
|
||||
control: { type: "radio" },
|
||||
},
|
||||
},
|
||||
};
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<typeof HGrid>;
|
||||
|
||||
/* const getColumnsProp = () */
|
||||
|
||||
@@ -120,3 +124,50 @@ function Placeholder({ text, height }: { text: string; height?: string }) {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const Chromatic: Story = {
|
||||
render: () => (
|
||||
<VStack gap="4">
|
||||
<div>
|
||||
<h2>Gap</h2>
|
||||
<Gap.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>DynamicGap</h2>
|
||||
<DynamicGap.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Columns</h2>
|
||||
<Columns.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>DynamicColumns</h2>
|
||||
<DynamicColumns.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>AlignItems</h2>
|
||||
<AlignItems.render />
|
||||
</div>
|
||||
</VStack>
|
||||
),
|
||||
parameters: {
|
||||
layout: "fullscreen",
|
||||
chromatic: {
|
||||
disable: false,
|
||||
modes: {
|
||||
mobile_portrait: {
|
||||
viewport: {
|
||||
width: 400,
|
||||
height: 850,
|
||||
},
|
||||
},
|
||||
desktop: {
|
||||
viewport: {
|
||||
width: 1280,
|
||||
height: 960,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -37,7 +37,7 @@ export const Default: StoryFn = ({
|
||||
Default.argTypes = {
|
||||
width: {
|
||||
control: "radio",
|
||||
options: widths,
|
||||
options: [...widths],
|
||||
},
|
||||
background: {
|
||||
control: "radio",
|
||||
|
||||
@@ -12,6 +12,6 @@ import React from "react";
|
||||
* <MyComponent />
|
||||
* </HStack>
|
||||
*/
|
||||
export const Spacer = () => <div className="navds-stack__spacer" />;
|
||||
export const Spacer = () => <span className="navds-stack__spacer" />;
|
||||
|
||||
export default Spacer;
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Meta } from "@storybook/react";
|
||||
import { Meta, StoryObj } from "@storybook/react";
|
||||
import React from "react";
|
||||
import {
|
||||
BabyWrappedIcon,
|
||||
@@ -12,8 +12,13 @@ import List from "./List";
|
||||
export default {
|
||||
title: "ds-react/List",
|
||||
component: List,
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<typeof List>;
|
||||
|
||||
export const Default = {
|
||||
render: () => {
|
||||
return (
|
||||
@@ -278,3 +283,45 @@ export const Nested = {
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const Chromatic: Story = {
|
||||
render: () => (
|
||||
<VStack gap="2">
|
||||
<div>
|
||||
<h2>Default</h2>
|
||||
<Default.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Ordered</h2>
|
||||
<Ordered.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>WithHeading</h2>
|
||||
<WithHeading.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>SizesUl</h2>
|
||||
<SizesUl.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>SizesOl</h2>
|
||||
<SizesOl.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>SizesIcons</h2>
|
||||
<SizesIcons.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Icons</h2>
|
||||
<Icons.render />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Nested</h2>
|
||||
<Nested.render />
|
||||
</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 Loader, { LoaderProps } from "./Loader";
|
||||
|
||||
export default {
|
||||
@@ -27,8 +28,13 @@ export default {
|
||||
},
|
||||
},
|
||||
},
|
||||
parameters: {
|
||||
chromatic: { disable: false },
|
||||
},
|
||||
} as Meta;
|
||||
|
||||
type Story = StoryObj<typeof Loader>;
|
||||
|
||||
export const Default = {
|
||||
render: (props: LoaderProps) => {
|
||||
return <Loader {...props} />;
|
||||
@@ -80,3 +86,25 @@ export const Transparent = () => (
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const Chromatic: Story = {
|
||||
render: () => (
|
||||
<VStack gap="2">
|
||||
<div>
|
||||
<h2>Size</h2>
|
||||
<Size />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Variant</h2>
|
||||
<Variant />
|
||||
</div>
|
||||
<div>
|
||||
<h2>Transparent</h2>
|
||||
<Transparent />
|
||||
</div>
|
||||
</VStack>
|
||||
),
|
||||
parameters: {
|
||||
chromatic: { disable: false },
|
||||
},
|
||||
};
|
||||
@@ -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>
|
||||
|
||||
@@ -146,6 +146,7 @@ export const WithUseState: StoryFn = () => {
|
||||
);
|
||||
};
|
||||
WithUseState.storyName = "With useState";
|
||||
WithUseState.parameters = { chromatic: { disable: true } };
|
||||
|
||||
export const EmptyHeader: StoryFn = () => (
|
||||
<Modal open onClose={() => null} aria-label="Modal with empty header">
|
||||
@@ -225,6 +226,7 @@ export const WithTooltip: StoryFn = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
WithTooltip.parameters = { chromatic: { disable: true } }; // The tooltip behaves unpredictably in Chromatic
|
||||
|
||||
export const WithSrOnlyElement: StoryFn = () => (
|
||||
<Modal
|
||||
@@ -330,12 +332,6 @@ ChromaticViewportTesting.parameters = {
|
||||
height: 400,
|
||||
},
|
||||
},
|
||||
desktop: {
|
||||
viewport: {
|
||||
width: 1280,
|
||||
height: 960,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# DismissableLayer API
|
||||
|
||||
`DismissableLayer`-API provides a robust handler for layers that can be dismissed by interacting outside of it.
|
||||
|
||||
One of the benefits is its ability to handle nested elements, relevant when creating nested dropdowns and similar solutions.
|
||||
Loaded 100 of 452 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user