mirror of
https://github.com/navikt/aksel.git
synced 2026-10-20 20:52:04 +00:00
Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
388ffb6906 | ||
|
|
3fe9563078 | ||
|
|
62d2f5af01 | ||
|
|
53780d0c67 | ||
|
|
d476be6604 | ||
|
|
a0c9494b07 | ||
|
|
bf857007cd | ||
|
|
e6db21586e | ||
|
|
619ebd5088 | ||
|
|
1b539a9b13 | ||
|
|
8859bbb7ea | ||
|
|
dff2c96b4b | ||
|
|
2177f3ab64 | ||
|
|
51629f9229 |
No files matched your search
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@navikt/ds-react": patch
|
||||
---
|
||||
|
||||
Lookup, Badge: Mark as deprecated when imported from @navikt/ds-react/PREVIEW
|
||||
@@ -49,7 +49,8 @@
|
||||
- `corepack yarn tsc --noEmit --incremental false --project <tsconfig>` - targeted type-check
|
||||
- `corepack yarn workspace <name> <script>` - e.g. `@navikt/ds-react test` (Vitest + tsc), `website test`
|
||||
- `corepack yarn lint` / `corepack yarn test` / `corepack yarn boot` - root suites (slow; cross-workspace changes only)
|
||||
- `corepack yarn storybook` (:6006) / `corepack yarn storybook:aksel` (website examples, :6007, no secrets) / `corepack yarn storybook:test` (play tests, Playwright Firefox) / `corepack yarn dev` (website, :3000, needs Sanity token)
|
||||
- `corepack yarn storybook` (:6006) / `corepack yarn storybook:aksel` (website examples, :6007, no secrets) / `corepack yarn storybook:test` (play tests, Playwright Firefox; Chromium when `COPILOT_CLI` is set) / `corepack yarn dev` (website, :3000, needs Sanity token)
|
||||
- Targeted play tests: `corepack yarn vitest run --config ./vitest.config.ts --project=storybook <path/to/X.tests.stories.tsx>` (repo root). Run after changing play tests. Under Copilot CLI this runs Chromium; if the browser can't launch, report tests as not run with the exact command.
|
||||
|
||||
## Where to edit
|
||||
|
||||
|
||||
@@ -12,4 +12,4 @@ applyTo: "**/*.test.ts,**/*.test.tsx,**/*.stories.tsx"
|
||||
- Query by role and accessible name (`getByRole("button", { name })`). Avoid snapshots unless nearby tests already use them.
|
||||
- Cover the main path plus 1-2 meaningful edge cases. A behavior change updates the nearest story and test in the same change.
|
||||
- ds-react unit tests run with `TZ=UTC`. Date logic must not depend on the local timezone.
|
||||
- Browser tests need Playwright Firefox. Yarn has `enableScripts: false`, so if the browser is missing, report it instead of installing globally.
|
||||
- Browser tests need Playwright Firefox (Chromium under Copilot CLI, see `vitest.config.ts`). Yarn has `enableScripts: false`, so if the browser is missing, report it instead of installing globally.
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
yarn lint-staged
|
||||
# Runs the local lint-staged checks before committing
|
||||
./node_modules/.bin/lint-staged
|
||||
@@ -1,5 +1,7 @@
|
||||
# @navikt/aksel-icons
|
||||
|
||||
## 8.20.0
|
||||
|
||||
## 8.19.2
|
||||
|
||||
## 8.19.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-icons",
|
||||
"version": "8.19.2",
|
||||
"version": "8.20.0",
|
||||
"author": "Aksel | Nav",
|
||||
"homepage": "https://aksel.nav.no/ikoner",
|
||||
"repository": {
|
||||
@@ -82,7 +82,7 @@
|
||||
"copyfiles": "^2.4.1",
|
||||
"fast-glob": "3.3.3",
|
||||
"hast-util-select": "6.0.4",
|
||||
"js-yaml": "^5.4.3",
|
||||
"js-yaml": "^5.5.0",
|
||||
"lodash": "4.18.1",
|
||||
"prettier": "3.9.6",
|
||||
"react": "19.3.0",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @navikt/aksel-stylelint
|
||||
|
||||
## 8.20.0
|
||||
|
||||
## 8.19.2
|
||||
|
||||
## 8.19.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-stylelint",
|
||||
"version": "8.19.2",
|
||||
"version": "8.20.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
@@ -43,10 +43,10 @@
|
||||
"dev": "yarn watch:lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-css": "^8.19.2",
|
||||
"@navikt/ds-tokens": "^8.19.2",
|
||||
"@navikt/ds-css": "^8.20.0",
|
||||
"@navikt/ds-tokens": "^8.20.0",
|
||||
"concurrently": "10.0.4",
|
||||
"postcss-selector-parser": "^7.1.6",
|
||||
"postcss-selector-parser": "^7.1.7",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"stylelint": "^17.15.0",
|
||||
"stylelint-test-rule-node": "^1.2.0",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @navikt/aksel
|
||||
|
||||
## 8.20.0
|
||||
|
||||
## 8.19.2
|
||||
|
||||
## 8.19.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel",
|
||||
"version": "8.19.2",
|
||||
"version": "8.20.0",
|
||||
"description": "Aksel command line interface. Codemods and other utilities for Aksel users.",
|
||||
"author": "Aksel | Nav designsystem team",
|
||||
"license": "MIT",
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
# @navikt/ds-css
|
||||
|
||||
## 8.20.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Checkbox: Add `CheckboxCard` and `CheckboxCardGroup` components ([#5364](https://github.com/navikt/aksel/pull/5364))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ToggleGroup: Add xsmall as size ([#5411](https://github.com/navikt/aksel/pull/5411))
|
||||
|
||||
## 8.19.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "8.19.2",
|
||||
"version": "8.20.0",
|
||||
"description": "CSS for Nav Designsystem",
|
||||
"author": "Aksel | Nav designsystem team",
|
||||
"keywords": [
|
||||
@@ -32,7 +32,7 @@
|
||||
"css:get-version": "node config/get-version.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^8.19.2",
|
||||
"@navikt/ds-tokens": "^8.20.0",
|
||||
"browserslist": "^4.28.7",
|
||||
"esbuild": "^0.28.2",
|
||||
"fast-glob": "3.3.3",
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Radio card */
|
||||
/* Checkbox and Radio card */
|
||||
.aksel-checkbox-card,
|
||||
.aksel-radio-card {
|
||||
width: 100%;
|
||||
align-content: flex-start;
|
||||
@@ -30,26 +31,30 @@
|
||||
border-radius: var(--ax-radius-8);
|
||||
|
||||
&:focus-within::after,
|
||||
&.aksel-radio--small:focus-within::after {
|
||||
&:is(.aksel-checkbox--small, .aksel-radio--small):focus-within::after {
|
||||
inset: 0;
|
||||
border-radius: calc(var(--ax-radius-8) - 1px);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Radio card group */
|
||||
.aksel-radio-card-group {
|
||||
& > .aksel-radio-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
/* Checkbox and Radio card group */
|
||||
.aksel-checkbox-card-group > .aksel-checkboxes,
|
||||
.aksel-radio-card-group > .aksel-radio-buttons {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--ax-space-12);
|
||||
|
||||
> .aksel-hgrid:not(.aksel-hgrid-gap) {
|
||||
gap: var(--ax-space-12);
|
||||
|
||||
> .aksel-hgrid:not(.aksel-hgrid-gap) {
|
||||
gap: var(--ax-space-12);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.aksel-radio-group--small > .aksel-radio-buttons > .aksel-hgrid:not(.aksel-hgrid-gap) {
|
||||
.aksel-checkbox-card-group.aksel-checkbox-group--small > .aksel-checkboxes,
|
||||
.aksel-radio-card-group.aksel-radio-group--small > .aksel-radio-buttons {
|
||||
gap: var(--ax-space-8);
|
||||
|
||||
> .aksel-hgrid:not(.aksel-hgrid-gap) {
|
||||
gap: var(--ax-space-8);
|
||||
}
|
||||
}
|
||||
@@ -65,14 +70,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.aksel-checkbox-card.aksel-checkbox--small,
|
||||
.aksel-radio-card.aksel-radio--small {
|
||||
padding: var(--ax-space-12) var(--ax-space-16);
|
||||
}
|
||||
|
||||
.aksel-radio-card-group.aksel-radio-group--small > .aksel-radio-buttons {
|
||||
gap: var(--ax-space-8);
|
||||
}
|
||||
|
||||
/* Label */
|
||||
.aksel-checkbox__label,
|
||||
.aksel-radio__label {
|
||||
@@ -246,7 +248,11 @@
|
||||
--__axc-radio-checkbox-marker-border: 7px;
|
||||
}
|
||||
|
||||
/* Radio card border in hover and checked states */
|
||||
/* Card border in hover and checked states */
|
||||
.aksel-checkbox-card:is(
|
||||
:hover:not(:where(.aksel-checkbox--disabled, .aksel-checkbox--readonly, .aksel-checkbox--error)),
|
||||
:has(.aksel-checkbox__input:checked):not(:where(.aksel-checkbox--disabled, .aksel-checkbox--readonly))
|
||||
),
|
||||
.aksel-radio-card:is(
|
||||
:hover:not(:where(.aksel-radio--disabled, .aksel-radio--readonly, .aksel-radio--error)),
|
||||
:has(> .aksel-radio__input:checked):not(:where(.aksel-radio--disabled, .aksel-radio--readonly))
|
||||
@@ -255,7 +261,10 @@
|
||||
box-shadow: 0 0 0 1px var(--ax-border-default);
|
||||
}
|
||||
|
||||
/* Radio card background in checked state */
|
||||
/* Card background in checked state */
|
||||
.aksel-checkbox-card:has(.aksel-checkbox__input:checked):not(
|
||||
:where(.aksel-checkbox--disabled, .aksel-checkbox--readonly, .aksel-checkbox--error)
|
||||
),
|
||||
.aksel-radio-card:has(> .aksel-radio__input:checked):not(
|
||||
:where(.aksel-radio--disabled, .aksel-radio--readonly, .aksel-radio--error)
|
||||
) {
|
||||
@@ -265,6 +274,8 @@
|
||||
/* Marker colors in unchecked hover state (unless disabled) */
|
||||
.aksel-checkbox__input:hover:not(:disabled, :checked, :indeterminate),
|
||||
.aksel-radio__input:hover:not(:disabled, :checked),
|
||||
.aksel-checkbox-card:hover:not(:where(.aksel-checkbox--disabled, .aksel-checkbox--readonly))
|
||||
.aksel-checkbox__input:not(:disabled, :checked, :indeterminate),
|
||||
.aksel-radio-card:hover:not(:where(.aksel-radio--disabled, .aksel-radio--readonly))
|
||||
> .aksel-radio__input:not(:disabled, :checked) {
|
||||
border-color: var(--ax-border-strong);
|
||||
@@ -283,11 +294,18 @@
|
||||
opacity: var(--ax-opacity-disabled);
|
||||
}
|
||||
|
||||
/* Radio card background in disabled and readonly states */
|
||||
/* Card background in readonly state */
|
||||
.aksel-checkbox-card:where(.aksel-checkbox--readonly),
|
||||
.aksel-radio-card:where(.aksel-radio--readonly) {
|
||||
background-color: var(--ax-bg-neutral-moderate);
|
||||
}
|
||||
|
||||
/* Card marker colors in disabled + checked state */
|
||||
.aksel-checkbox-card.aksel-checkbox--disabled .aksel-checkbox__input:where(:checked, :indeterminate) {
|
||||
background-color: var(--ax-bg-neutral-strong);
|
||||
border-color: var(--ax-border-neutral-strong);
|
||||
}
|
||||
|
||||
.aksel-radio-card.aksel-radio--disabled > .aksel-radio__input:checked {
|
||||
border-color: var(--ax-border-neutral-strong);
|
||||
}
|
||||
@@ -295,6 +313,7 @@
|
||||
/* Cursor in disabled state on otherwise clickable elements */
|
||||
.aksel-checkbox--disabled :where(.aksel-checkbox__input, .aksel-checkbox__label),
|
||||
.aksel-radio--disabled > :where(.aksel-radio__input, .aksel-radio__label),
|
||||
.aksel-checkbox-card.aksel-checkbox--disabled,
|
||||
.aksel-radio-card.aksel-radio--disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
@@ -302,6 +321,7 @@
|
||||
/* Cursor in readonly state on otherwise clickable elements */
|
||||
.aksel-checkbox--readonly :where(.aksel-checkbox__input, .aksel-checkbox__label),
|
||||
.aksel-radio--readonly > :where(.aksel-radio__input, .aksel-radio__label),
|
||||
.aksel-checkbox-card.aksel-checkbox--readonly,
|
||||
.aksel-radio-card.aksel-radio--readonly {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
@@ -87,6 +87,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Modifier xsmall size */
|
||||
.aksel-toggle-group--xsmall {
|
||||
& .aksel-toggle-group__button {
|
||||
padding: var(--ax-space-1) var(--ax-space-8);
|
||||
min-height: 1.5rem;
|
||||
}
|
||||
|
||||
& .aksel-toggle-group__button-inner > svg {
|
||||
font-size: var(--ax-font-size-large);
|
||||
}
|
||||
}
|
||||
|
||||
/* High contrast */
|
||||
@media (forced-colors: active) {
|
||||
.aksel-toggle-group {
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# @navikt/ds-react
|
||||
|
||||
## 8.20.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Checkbox: Add `CheckboxCard` and `CheckboxCardGroup` components ([#5364](https://github.com/navikt/aksel/pull/5364))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Lookup, Badge: Mark as deprecated when imported from @navikt/ds-react/PREVIEW ([#5401](https://github.com/navikt/aksel/pull/5401))
|
||||
|
||||
- ToggleGroup: Add xsmall as size ([#5411](https://github.com/navikt/aksel/pull/5411))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@8.20.0
|
||||
- @navikt/aksel-icons@8.20.0
|
||||
|
||||
## 8.19.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "8.19.2",
|
||||
"version": "8.20.0",
|
||||
"description": "React components from the Norwegian Labour and Welfare Administration.",
|
||||
"author": "Aksel, a team part of the Norwegian Labour and Welfare Administration.",
|
||||
"license": "MIT",
|
||||
@@ -96,6 +96,16 @@
|
||||
"default": "./cjs/form/radio-card/index.js"
|
||||
}
|
||||
},
|
||||
"./PREVIEW/CheckboxCard": {
|
||||
"import": {
|
||||
"types": "./esm/form/checkbox-card/index.d.ts",
|
||||
"default": "./esm/form/checkbox-card/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./cjs/form/checkbox-card/index.d.ts",
|
||||
"default": "./cjs/form/checkbox-card/index.js"
|
||||
}
|
||||
},
|
||||
"./types/theme": {
|
||||
"types": "./esm/types/theme.d.ts"
|
||||
},
|
||||
@@ -778,8 +788,8 @@
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "0.27.20",
|
||||
"@floating-ui/react-dom": "^2.1.9",
|
||||
"@navikt/aksel-icons": "^8.19.2",
|
||||
"@navikt/ds-tokens": "^8.19.2",
|
||||
"@navikt/aksel-icons": "^8.20.0",
|
||||
"@navikt/ds-tokens": "^8.20.0",
|
||||
"date-fns": "^4.0.0",
|
||||
"react-day-picker": "10.0.1"
|
||||
},
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { ComponentMetadata } from "../../utils/types/metadata";
|
||||
import { CheckboxCard, CheckboxCardGroup } from "./index";
|
||||
|
||||
const metadata: ComponentMetadata = {
|
||||
name: "CheckboxCard",
|
||||
components: {
|
||||
CheckboxCard,
|
||||
CheckboxCardGroup,
|
||||
},
|
||||
keywords: ["checkbox", "avkrysningsboks", "form", "input", "choice", "card"],
|
||||
related: ["Checkbox"],
|
||||
};
|
||||
|
||||
export { metadata };
|
||||
@@ -0,0 +1,129 @@
|
||||
import type { Meta } from "@storybook/react-vite";
|
||||
import React from "react";
|
||||
import { HGrid } from "../../primitives/grid";
|
||||
import { renderStoriesForChromatic } from "../../utils/renderStoriesForChromatic";
|
||||
import { CheckboxCard } from "./CheckboxCard";
|
||||
import { CheckboxCardGroup } from "./CheckboxCardGroup";
|
||||
|
||||
const meta: Meta<typeof CheckboxCardGroup> = {
|
||||
title: "ds-react/CheckboxCard",
|
||||
component: CheckboxCardGroup,
|
||||
subcomponents: { CheckboxCard },
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
};
|
||||
export default meta;
|
||||
|
||||
export const Default = () => (
|
||||
<CheckboxCardGroup
|
||||
legend="Velg varslingskanaler"
|
||||
description="Du kan velge flere alternativer."
|
||||
>
|
||||
<CheckboxCard value="sms" description="Sendes til mobilnummeret ditt.">
|
||||
SMS
|
||||
</CheckboxCard>
|
||||
<CheckboxCard value="epost" description="Sendes til e-postadressen din.">
|
||||
E-post
|
||||
</CheckboxCard>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
|
||||
export const HorizontalLayout = () => (
|
||||
<CheckboxCardGroup
|
||||
legend="Velg varslingskanaler"
|
||||
description="Du kan velge flere alternativer."
|
||||
defaultValue={["sms"]}
|
||||
size="small"
|
||||
>
|
||||
<HGrid columns={{ xs: 1, md: 3 }}>
|
||||
<CheckboxCard value="sms" description="Sendes til mobilnummeret ditt.">
|
||||
SMS
|
||||
</CheckboxCard>
|
||||
<CheckboxCard value="epost" description="Sendes til e-postadressen din.">
|
||||
E-post
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="brev"
|
||||
description="Sendes til folkeregistrert adresse."
|
||||
>
|
||||
Brev
|
||||
</CheckboxCard>
|
||||
</HGrid>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
|
||||
export const Small = () => (
|
||||
<CheckboxCardGroup legend="Velg varslingskanaler" size="small">
|
||||
<CheckboxCard value="sms" description="Sendes til mobilnummeret ditt.">
|
||||
SMS
|
||||
</CheckboxCard>
|
||||
<CheckboxCard value="epost" description="Sendes til e-postadressen din.">
|
||||
E-post
|
||||
</CheckboxCard>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
|
||||
export const ErrorProp = () => (
|
||||
<CheckboxCardGroup
|
||||
legend="Velg varslingskanaler"
|
||||
error="Du må velge minst én varslingskanal."
|
||||
defaultValue={["sms"]}
|
||||
>
|
||||
<CheckboxCard value="sms" description="Sendes til mobilnummeret ditt.">
|
||||
SMS
|
||||
</CheckboxCard>
|
||||
<CheckboxCard value="epost" description="Sendes til e-postadressen din.">
|
||||
E-post
|
||||
</CheckboxCard>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
|
||||
export const Readonly = () => (
|
||||
<CheckboxCardGroup
|
||||
legend="Valgte varslingskanaler"
|
||||
defaultValue={["sms"]}
|
||||
readOnly
|
||||
>
|
||||
<CheckboxCard value="sms" description="Sendes til mobilnummeret ditt.">
|
||||
SMS
|
||||
</CheckboxCard>
|
||||
<CheckboxCard value="epost" description="Sendes til e-postadressen din.">
|
||||
E-post
|
||||
</CheckboxCard>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
|
||||
export const Disabled = () => (
|
||||
<CheckboxCardGroup
|
||||
legend="Velg varslingskanaler"
|
||||
defaultValue={["sms"]}
|
||||
disabled
|
||||
>
|
||||
<CheckboxCard value="sms" description="Sendes til mobilnummeret ditt.">
|
||||
SMS
|
||||
</CheckboxCard>
|
||||
<CheckboxCard value="epost" description="Sendes til e-postadressen din.">
|
||||
E-post
|
||||
</CheckboxCard>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
|
||||
export const Chromatic = renderStoriesForChromatic({
|
||||
Default,
|
||||
HorizontalLayout,
|
||||
Small,
|
||||
ErrorProp,
|
||||
Readonly,
|
||||
Disabled,
|
||||
});
|
||||
|
||||
export const ChromaticDark = renderStoriesForChromatic({
|
||||
Default,
|
||||
HorizontalLayout,
|
||||
Small,
|
||||
ErrorProp,
|
||||
Readonly,
|
||||
Disabled,
|
||||
});
|
||||
ChromaticDark.globals = { theme: "dark" };
|
||||
@@ -0,0 +1,41 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { cl } from "../../utils/helpers";
|
||||
import { Checkbox, CheckboxCardContextProvider } from "../checkbox/Checkbox";
|
||||
import type { CheckboxProps } from "../checkbox/types";
|
||||
|
||||
type CheckboxCardProps = CheckboxProps;
|
||||
|
||||
/**
|
||||
* Checkbox option styled as a card.
|
||||
* @see 🏷️ {@link CheckboxCardProps}
|
||||
* @example
|
||||
* ```tsx
|
||||
* <CheckboxCardGroup legend="Velg varslingskanaler">
|
||||
* <CheckboxCard value="sms" description="Sendes til mobilnummeret ditt.">
|
||||
* SMS
|
||||
* </CheckboxCard>
|
||||
* <CheckboxCard value="epost">E-post</CheckboxCard>
|
||||
* </CheckboxCardGroup>
|
||||
* ```
|
||||
*/
|
||||
const CheckboxCard = forwardRef<HTMLInputElement, CheckboxCardProps>(
|
||||
({ className, ...rest }, ref) => {
|
||||
return (
|
||||
<CheckboxCardContextProvider>
|
||||
<Checkbox
|
||||
{...rest}
|
||||
ref={ref}
|
||||
className={cl("aksel-checkbox-card", className)}
|
||||
/>
|
||||
</CheckboxCardContextProvider>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace CheckboxCard {
|
||||
export type Props = CheckboxCardProps;
|
||||
}
|
||||
|
||||
export { CheckboxCard };
|
||||
export type { CheckboxCardProps };
|
||||
@@ -0,0 +1,37 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { cl } from "../../utils/helpers";
|
||||
import CheckboxGroup, {
|
||||
type CheckboxGroupProps,
|
||||
} from "../checkbox/CheckboxGroup";
|
||||
|
||||
type CheckboxCardGroupProps = CheckboxGroupProps;
|
||||
|
||||
/**
|
||||
* CheckboxGroup for `<CheckboxCard />` elements. Horizontal layout is best suited for a small number of checkbox cards.
|
||||
* @see 🏷️ {@link CheckboxCardGroupProps}
|
||||
* @example
|
||||
* ```tsx
|
||||
* <CheckboxCardGroup legend="Velg varslingskanaler">
|
||||
* <CheckboxCard value="sms">SMS</CheckboxCard>
|
||||
* <CheckboxCard value="epost">E-post</CheckboxCard>
|
||||
* </CheckboxCardGroup>
|
||||
* ```
|
||||
*/
|
||||
const CheckboxCardGroup = forwardRef<
|
||||
HTMLFieldSetElement,
|
||||
CheckboxCardGroupProps
|
||||
>(({ className, ...rest }, ref) => (
|
||||
<CheckboxGroup
|
||||
{...rest}
|
||||
ref={ref}
|
||||
className={cl("aksel-checkbox-card-group", className)}
|
||||
/>
|
||||
));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace CheckboxCardGroup {
|
||||
export type Props = CheckboxCardGroupProps;
|
||||
}
|
||||
|
||||
export { CheckboxCardGroup };
|
||||
export type { CheckboxCardGroupProps };
|
||||
@@ -0,0 +1,3 @@
|
||||
"use client";
|
||||
export { CheckboxCard } from "./CheckboxCard";
|
||||
export { CheckboxCardGroup } from "./CheckboxCardGroup";
|
||||
@@ -88,3 +88,22 @@ describe("Checkbox handles controlled-state correctly", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Checkbox outside CheckboxCard", () => {
|
||||
test("does not toggle when clicking description", () => {
|
||||
render(
|
||||
<CheckboxGroup legend="legend">
|
||||
<Checkbox value="value1" description="Checkbox description">
|
||||
label1
|
||||
</Checkbox>
|
||||
</CheckboxGroup>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText("Checkbox description"));
|
||||
|
||||
expect(
|
||||
screen.getByRole<HTMLInputElement>("checkbox", { name: "label1" })
|
||||
.checked,
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,33 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import React, { forwardRef, useRef } from "react";
|
||||
import { BodyShort } from "../../typography";
|
||||
import { omit, useId } from "../../utils-external";
|
||||
import { cl } from "../../utils/helpers";
|
||||
import { cl, createStrictContext } from "../../utils/helpers";
|
||||
import { useMergeRefs } from "../../utils/hooks";
|
||||
import { ReadOnlyIconWithTitle } from "../ReadOnlyIcon";
|
||||
import { CheckboxInput } from "./checkbox-input/CheckboxInput";
|
||||
import type { CheckboxProps } from "./types";
|
||||
import useCheckbox from "./useCheckbox";
|
||||
|
||||
const {
|
||||
Provider: CheckboxCardContextProvider,
|
||||
useContext: useCheckboxCardContext,
|
||||
} = createStrictContext({ name: "checkbox-card" });
|
||||
|
||||
export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
(props: CheckboxProps, forwardedRef) => {
|
||||
const { inputProps, hasError, size, readOnly, nested } = useCheckbox(props);
|
||||
const descriptionId = useId();
|
||||
const cardContext = useCheckboxCardContext(false);
|
||||
|
||||
const checkboxRef = useRef<HTMLInputElement>(null);
|
||||
const mergedRefs = useMergeRefs(forwardedRef, checkboxRef);
|
||||
|
||||
const { className, description, children, indeterminate, hideLabel } =
|
||||
props;
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: clickable div for checkbox card
|
||||
// biome-ignore lint/a11y/useKeyWithClickEvents: clickable div for checkbox card
|
||||
<div
|
||||
className={cl(className, "aksel-checkbox", `aksel-checkbox--${size}`, {
|
||||
"aksel-checkbox--error": hasError,
|
||||
@@ -23,9 +35,26 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
"aksel-checkbox--readonly": readOnly,
|
||||
})}
|
||||
data-color={hasError ? "danger" : props["data-color"]}
|
||||
onClick={(event) => {
|
||||
if (inputProps.disabled || readOnly || !cardContext) {
|
||||
return;
|
||||
}
|
||||
|
||||
/* Let input and label handle their own click events */
|
||||
const target = event.target;
|
||||
const nativeHandler =
|
||||
target instanceof Element ? target.closest("label, input") : null;
|
||||
|
||||
if (nativeHandler && event.currentTarget.contains(nativeHandler)) {
|
||||
return;
|
||||
}
|
||||
|
||||
checkboxRef.current?.click();
|
||||
checkboxRef.current?.focus({ preventScroll: true });
|
||||
}}
|
||||
>
|
||||
<CheckboxInput
|
||||
ref={forwardedRef}
|
||||
ref={mergedRefs}
|
||||
{...omit(props, [
|
||||
"children",
|
||||
"size",
|
||||
@@ -72,3 +101,4 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
);
|
||||
|
||||
export default Checkbox;
|
||||
export { CheckboxCardContextProvider };
|
||||
@@ -21,20 +21,19 @@ import { ComboboxTrigger } from "./trigger/ComboboxTrigger";
|
||||
interface ComboboxProps<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
>
|
||||
extends
|
||||
Omit<
|
||||
ComboboxRootProps<T>,
|
||||
"children" | "selectedOptions" | "onToggleOption"
|
||||
>,
|
||||
Partial<Pick<ComboboxRootProps<T>, "selectedOptions" | "onToggleOption">>,
|
||||
Omit<FormFieldProps, "size"> {
|
||||
extends Omit<ComboboxRootProps<T>, "children">, Omit<FormFieldProps, "size"> {
|
||||
/**
|
||||
* Label
|
||||
*/
|
||||
label: string;
|
||||
/**
|
||||
* Hides the label and description visually while keeping them available to screen readers.
|
||||
*/
|
||||
hideLabel?: boolean;
|
||||
/**
|
||||
* Field name. When provided, the field value will be included in form submissions.
|
||||
*/
|
||||
name?: string;
|
||||
defaultSelectedOptions?: ComboboxRootProps<T>["selectedOptions"];
|
||||
onSelectedOptionsChange?: (
|
||||
newSelectedOptions: ComboboxRootProps<T>["selectedOptions"],
|
||||
) => void;
|
||||
}
|
||||
|
||||
function Combobox<
|
||||
@@ -43,8 +42,6 @@ function Combobox<
|
||||
label,
|
||||
hideLabel,
|
||||
name,
|
||||
defaultSelectedOptions,
|
||||
onSelectedOptionsChange,
|
||||
size: sizeProp,
|
||||
error,
|
||||
errorId: errorIdProp,
|
||||
@@ -52,9 +49,7 @@ function Combobox<
|
||||
description,
|
||||
id,
|
||||
readOnly: readOnlyProp,
|
||||
multiselect = false, // Default value should be synced with ComboboxRoot
|
||||
selectedOptions: selectedOptionsProp,
|
||||
onToggleOption: onToggleOptionProp,
|
||||
multiple,
|
||||
...rest
|
||||
}: ComboboxProps<T>) {
|
||||
const {
|
||||
@@ -78,35 +73,11 @@ function Combobox<
|
||||
"combobox",
|
||||
);
|
||||
|
||||
const [selectedOptions, setSelectedOptions] = useControllableState({
|
||||
defaultValue: defaultSelectedOptions || [],
|
||||
value: selectedOptionsProp,
|
||||
onChange: onSelectedOptionsChange,
|
||||
});
|
||||
|
||||
const onToggleOption: ComboboxRootProps<T>["onToggleOption"] = (
|
||||
option,
|
||||
isSelected,
|
||||
) => {
|
||||
onToggleOptionProp?.(option, isSelected);
|
||||
setSelectedOptions((prevSelectedOptions) => {
|
||||
if (!multiselect) {
|
||||
return [option.value];
|
||||
}
|
||||
if (isSelected) {
|
||||
return [...prevSelectedOptions, option.value];
|
||||
}
|
||||
return prevSelectedOptions.filter((value) => value !== option.value);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<ComboboxRoot
|
||||
size={size}
|
||||
disabled={inputProps.disabled}
|
||||
multiselect={multiselect}
|
||||
selectedOptions={selectedOptions}
|
||||
onToggleOption={onToggleOption}
|
||||
multiple={multiple}
|
||||
{...rest}
|
||||
>
|
||||
<ComboboxLabel
|
||||
@@ -146,8 +117,8 @@ function Combobox<
|
||||
</div>
|
||||
|
||||
{name &&
|
||||
selectedOptions.map((value) => (
|
||||
<input key={value} type="hidden" name={name} value={value} />
|
||||
rest.value.map((val) => (
|
||||
<input key={val} type="hidden" name={name} value={val} />
|
||||
))}
|
||||
|
||||
<ComboboxOverlay
|
||||
@@ -169,5 +140,126 @@ function Combobox<
|
||||
);
|
||||
}
|
||||
|
||||
export { Combobox };
|
||||
export type { ComboboxProps };
|
||||
// ----------- ComboboxSingle ----------- //
|
||||
|
||||
interface ComboboxSingleProps<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
> extends Omit<ComboboxProps<T>, "multiple" | "value" | "onToggleOption"> {
|
||||
/**
|
||||
* Called when selected option changes.
|
||||
*/
|
||||
onChange?: (
|
||||
newValue: NonNullable<ComboboxProps<T>["value"]>[number] | undefined,
|
||||
) => void;
|
||||
/**
|
||||
* Value of the selected option.
|
||||
*/
|
||||
value?: NonNullable<ComboboxProps<T>["value"]>[number];
|
||||
/**
|
||||
* Initial value when uncontrolled.
|
||||
*/
|
||||
defaultValue?: NonNullable<ComboboxProps<T>["value"]>[number];
|
||||
}
|
||||
|
||||
function ComboboxSingle<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
>({ onChange, value, defaultValue, ...rest }: ComboboxSingleProps<T>) {
|
||||
const [localValue, setLocalValue] = useControllableState({
|
||||
defaultValue,
|
||||
value,
|
||||
onChange,
|
||||
});
|
||||
|
||||
const onToggleOption: ComboboxProps<T>["onToggleOption"] = (
|
||||
option,
|
||||
isSelected,
|
||||
) => {
|
||||
setLocalValue(isSelected ? option.value : undefined);
|
||||
}; // TODO: Dumt at ikke får hele option-objektet... Legge til i onChange? Hvis vi tilbyr onToggle så blir det så likt onChange...
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
{...rest}
|
||||
value={localValue ? [localValue] : []}
|
||||
defaultValue={defaultValue ? [defaultValue] : []}
|
||||
multiple={false}
|
||||
onToggleOption={onToggleOption}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace ComboboxSingle {
|
||||
export type Props<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
> = ComboboxSingleProps<T>;
|
||||
}
|
||||
|
||||
// ----------- ComboboxMulti ----------- //
|
||||
|
||||
interface ComboboxMultiProps<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
>
|
||||
extends
|
||||
Omit<ComboboxProps<T>, "multiple" | "value" | "onToggleOption">,
|
||||
Partial<Pick<ComboboxProps<T>, "onToggleOption">> {
|
||||
/**
|
||||
* Called when selected options change.
|
||||
*/
|
||||
onChange?: (newValue: ComboboxProps<T>["value"]) => void;
|
||||
/**
|
||||
* Value of the selected options.
|
||||
*/
|
||||
value?: ComboboxProps<T>["value"];
|
||||
/**
|
||||
* Initial value when uncontrolled.
|
||||
*/
|
||||
defaultValue?: ComboboxProps<T>["value"];
|
||||
}
|
||||
|
||||
function ComboboxMulti<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
>({
|
||||
onChange,
|
||||
value,
|
||||
defaultValue,
|
||||
onToggleOption,
|
||||
...rest
|
||||
}: ComboboxMultiProps<T>) {
|
||||
const [localValue, setLocalValue] = useControllableState({
|
||||
defaultValue: defaultValue || [],
|
||||
value,
|
||||
onChange,
|
||||
});
|
||||
|
||||
const localOnToggleOption: ComboboxProps<T>["onToggleOption"] = (
|
||||
option,
|
||||
isSelected,
|
||||
) => {
|
||||
onToggleOption?.(option, isSelected);
|
||||
setLocalValue((prevSelectedOptions) =>
|
||||
isSelected
|
||||
? [...prevSelectedOptions, option.value]
|
||||
: prevSelectedOptions.filter((optVal) => optVal !== option.value),
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
{...rest}
|
||||
value={localValue}
|
||||
defaultValue={defaultValue}
|
||||
multiple
|
||||
onToggleOption={localOnToggleOption}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace ComboboxMulti {
|
||||
export type Props<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
> = ComboboxMultiProps<T>;
|
||||
}
|
||||
|
||||
export { ComboboxSingle, ComboboxMulti };
|
||||
@@ -3,8 +3,7 @@ import React, { useMemo, useState } from "react";
|
||||
import { Button } from "../../button";
|
||||
import { Dialog } from "../../dialog";
|
||||
import { HStack, VStack } from "../../primitives/stack";
|
||||
import { Select } from "../select";
|
||||
import { Combobox, type ComboboxProps } from "./Combobox";
|
||||
import { ComboboxMulti, ComboboxSingle } from "./Combobox";
|
||||
import { ComboboxField } from "./field/ComboboxField";
|
||||
import { ComboboxFilter } from "./filter/ComboboxFilter";
|
||||
import { ComboboxLabel } from "./label/ComboboxLabel";
|
||||
@@ -18,9 +17,9 @@ import {
|
||||
} from "./root/ComboboxRoot";
|
||||
import { ComboboxTrigger } from "./trigger/ComboboxTrigger";
|
||||
|
||||
const meta: Meta<typeof Combobox> = {
|
||||
const meta: Meta<typeof ComboboxSingle> = {
|
||||
title: "ds-react/Combobox2",
|
||||
component: Combobox,
|
||||
component: ComboboxSingle,
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
@@ -77,13 +76,12 @@ const groupedCountries: (MyGroup | MyOption)[] = [
|
||||
];
|
||||
|
||||
type DefaultProps = Pick<
|
||||
ComboboxProps<MyOption>,
|
||||
ComboboxSingle.Props<MyOption>,
|
||||
| "label"
|
||||
| "description"
|
||||
| "hideLabel"
|
||||
| "size"
|
||||
| "defaultOpen"
|
||||
| "multiselect"
|
||||
| "id"
|
||||
| "readOnly"
|
||||
| "disabled"
|
||||
@@ -91,14 +89,13 @@ type DefaultProps = Pick<
|
||||
>;
|
||||
|
||||
export const Default: StoryFn<DefaultProps> = (props) => {
|
||||
return <Combobox defaultOpen options={countries} {...props} />;
|
||||
return <ComboboxSingle options={countries} {...props} />;
|
||||
};
|
||||
Default.args = {
|
||||
label: "Velg land",
|
||||
description: "Landet hvor du er født.",
|
||||
hideLabel: false,
|
||||
defaultOpen: false,
|
||||
multiselect: false,
|
||||
readOnly: false,
|
||||
disabled: false,
|
||||
error: "",
|
||||
@@ -113,17 +110,21 @@ Default.argTypes = {
|
||||
},
|
||||
};
|
||||
|
||||
export const Multiselect = () => {
|
||||
return <ComboboxMulti label="Velg land" options={countries} />;
|
||||
};
|
||||
Multiselect.storyName = "Multiselect (WIP)";
|
||||
|
||||
function BasicCombobox<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData> =
|
||||
MyOption,
|
||||
>(props: Partial<ComboboxProps<T>>) {
|
||||
>(props: Partial<ComboboxSingle.Props<T>>) {
|
||||
return (
|
||||
<Combobox
|
||||
<ComboboxSingle
|
||||
options={countries as T[]}
|
||||
defaultSelectedOptions={["opt-1"]}
|
||||
defaultValue="opt-1"
|
||||
label="Velg land"
|
||||
description="Landet hvor du er født."
|
||||
multiselect={false}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -133,13 +134,13 @@ export const Small = () => <BasicCombobox size="small" error="Error" />;
|
||||
|
||||
export const Hidelabel = () => <BasicCombobox hideLabel />;
|
||||
|
||||
export const Readonly = () => <BasicCombobox readOnly />;
|
||||
export const Readonly = () => <BasicCombobox readOnly error="Test" />;
|
||||
|
||||
export const Disabled = () => <BasicCombobox disabled />;
|
||||
export const Disabled = () => <BasicCombobox disabled error="Test" />;
|
||||
|
||||
export const ErrorStory = () => (
|
||||
<VStack gap="space-16">
|
||||
<BasicCombobox error="Du må velge et land." defaultSelectedOptions={[]} />
|
||||
<BasicCombobox error="Du må velge et land." defaultValue={undefined} />
|
||||
<BasicCombobox error="Ugyldig land." />
|
||||
</VStack>
|
||||
);
|
||||
@@ -148,10 +149,8 @@ ErrorStory.storyName = "Error";
|
||||
export const DefaultOpen = () => <BasicCombobox defaultOpen />;
|
||||
DefaultOpen.storyName = "DefaultOpen";
|
||||
|
||||
export const DefaultSelectedOptions = () => (
|
||||
<BasicCombobox defaultSelectedOptions={["opt-1"]} />
|
||||
);
|
||||
DefaultSelectedOptions.storyName = "DefaultSelectedOptions";
|
||||
export const DefaultValue = () => <BasicCombobox defaultValue="opt-1" />;
|
||||
DefaultValue.storyName = "DefaultValue";
|
||||
|
||||
export const Name = () => (
|
||||
<form
|
||||
@@ -169,50 +168,19 @@ export const Name = () => (
|
||||
);
|
||||
|
||||
export const Controlled = () => {
|
||||
const [selectedOptions, setSelectedOptions] = useState<MyOption["value"][]>([
|
||||
"opt-1",
|
||||
]);
|
||||
const [value, setValue] = useState<MyOption["value"] | undefined>("opt-1");
|
||||
|
||||
return (
|
||||
<VStack gap="space-8" minWidth="250px">
|
||||
<Combobox
|
||||
<ComboboxSingle
|
||||
options={countries}
|
||||
selectedOptions={selectedOptions}
|
||||
onToggleOption={(option, newSelected) => {
|
||||
setSelectedOptions((prev) =>
|
||||
newSelected
|
||||
? [...prev, option.value]
|
||||
: prev.filter((v) => v !== option.value),
|
||||
);
|
||||
}}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
label="Velg land"
|
||||
/>
|
||||
Selected: {selectedOptions.join(", ")}
|
||||
Selected: {value}
|
||||
<div>
|
||||
<button type="button" onClick={() => setSelectedOptions([])}>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export const Controlled2 = () => {
|
||||
const [selectedOptions, setSelectedOptions] = useState<MyOption["value"][]>([
|
||||
"opt-1",
|
||||
]);
|
||||
|
||||
return (
|
||||
<VStack gap="space-8" minWidth="250px">
|
||||
<Combobox
|
||||
options={countries}
|
||||
selectedOptions={selectedOptions}
|
||||
onSelectedOptionsChange={setSelectedOptions}
|
||||
label="Velg land"
|
||||
/>
|
||||
Selected: {selectedOptions.join(", ")}
|
||||
<div>
|
||||
<button type="button" onClick={() => setSelectedOptions([])}>
|
||||
<button type="button" onClick={() => setValue(undefined)}>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
@@ -243,7 +211,7 @@ export const ManyOptions: StoryFn<ManyOptionsProps> = ({ count }) => {
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "400px", width: "250px" }}>
|
||||
<Combobox options={manyOptions} label="Test" />
|
||||
<ComboboxSingle options={manyOptions} label="Test" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -278,7 +246,7 @@ export const LongLabels = () => (
|
||||
],
|
||||
},
|
||||
]}
|
||||
selectedOptions={["opt-1"]}
|
||||
value={["opt-1"]}
|
||||
onToggleOption={() => {}}
|
||||
>
|
||||
<ComboboxField />
|
||||
@@ -290,15 +258,13 @@ export const LongLabels = () => (
|
||||
);
|
||||
|
||||
export const Composition = () => {
|
||||
const [selectedOptions, setSelectedOptions] = useState<MyOption["value"][]>([
|
||||
"opt-1",
|
||||
]);
|
||||
const [value, setSelectedOptions] = useState<MyOption["value"][]>(["opt-1"]);
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState<MyOption>(countries[0]);
|
||||
|
||||
const rootProps = {
|
||||
options: countries,
|
||||
selectedOptions,
|
||||
value,
|
||||
onToggleOption: (option) => {
|
||||
setSelectedOptions((prev) =>
|
||||
prev.includes(option.value)
|
||||
@@ -335,9 +301,9 @@ export const Composition = () => {
|
||||
|
||||
<ComboboxRoot
|
||||
options={countries}
|
||||
selectedOptions={[selectedOption.value]}
|
||||
value={[selectedOption.value]}
|
||||
onToggleOption={setSelectedOption}
|
||||
multiselect={false}
|
||||
multiple={false}
|
||||
>
|
||||
<ComboboxTrigger>
|
||||
<Button>Single select</Button>
|
||||
@@ -366,9 +332,9 @@ export const Composition = () => {
|
||||
|
||||
<ComboboxRoot
|
||||
options={countries}
|
||||
selectedOptions={[selectedOption.value]}
|
||||
value={[selectedOption.value]}
|
||||
onToggleOption={setSelectedOption}
|
||||
multiselect={false}
|
||||
multiple={false}
|
||||
>
|
||||
<ComboboxLabel htmlFor="demo2">Velg land (single select)</ComboboxLabel>
|
||||
<ComboboxTrigger id="demo2">
|
||||
@@ -383,7 +349,7 @@ export const Composition = () => {
|
||||
</ComboboxRoot>
|
||||
|
||||
<ComboboxRoot {...rootProps}>
|
||||
<ComboboxLabel htmlFor="demo3">Velg land (multiselect)</ComboboxLabel>
|
||||
<ComboboxLabel htmlFor="demo3">Velg land (multiple)</ComboboxLabel>
|
||||
<ComboboxTrigger id="demo3">
|
||||
<ComboboxField />
|
||||
</ComboboxTrigger>
|
||||
@@ -423,60 +389,20 @@ export const InDialog = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Testing = () => {
|
||||
return (
|
||||
<VStack gap="space-32" width="300px">
|
||||
<button type="button" onClick={() => console.log("Knapp før")}>
|
||||
Knapp før
|
||||
</button>
|
||||
|
||||
<Combobox
|
||||
options={countries}
|
||||
multiselect={false}
|
||||
label="Velg land (Combobox)"
|
||||
description="Beskrivelse"
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Velg land (vanlig select)"
|
||||
description="Dette er en vanlig select."
|
||||
//error="Test"
|
||||
>
|
||||
<option value=""></option>
|
||||
<option value="norway">Norway</option>
|
||||
<option value="finland">Finland</option>
|
||||
<option value="sweden">Sweden</option>
|
||||
<option value="denmark">Denmark</option>
|
||||
<option value="iceland">Iceland</option>
|
||||
<option value="faroe-islands">Faroe Islands</option>
|
||||
<option value="aland-islands">Åland Islands</option>
|
||||
<option value="estonia">Estonia</option>
|
||||
<option value="latvia">Latvia</option>
|
||||
<option value="lithuania">Lithuania</option>
|
||||
</Select>
|
||||
|
||||
<Select label="Mange alternativer (vanlig select)">
|
||||
{Array.from({ length: 100 }, (_, i) => (
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Don't care
|
||||
<option key={i} value={`opt-${i + 1}`}>
|
||||
{`Option ${String(i + 1).padStart(4, "0")}`}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
function WithValidation() {
|
||||
const [error, setError] = useState("Du må velge et land");
|
||||
return (
|
||||
<BasicCombobox
|
||||
options={nordicCountries}
|
||||
defaultSelectedOptions={[]}
|
||||
defaultValue={undefined}
|
||||
error={error}
|
||||
onToggleOption={(option) =>
|
||||
setError(option.label === "Island" ? "" : "Du må velge Island")
|
||||
}
|
||||
onChange={(newVal) => {
|
||||
if (!newVal) {
|
||||
setError("Du må velge et land");
|
||||
return;
|
||||
}
|
||||
setError(newVal === "opt-5" ? "" : "Du må velge Island");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -484,10 +410,10 @@ function WithValidation() {
|
||||
export const ScreenreaderTesting = () => (
|
||||
<div>
|
||||
<h2>Med grupper</h2>
|
||||
<BasicCombobox options={groupedCountries} defaultSelectedOptions={[]} />
|
||||
<BasicCombobox options={groupedCountries} defaultValue={undefined} />
|
||||
|
||||
<h2>Uten grupper</h2>
|
||||
<BasicCombobox options={nordicCountries} defaultSelectedOptions={[]} />
|
||||
<BasicCombobox options={nordicCountries} defaultValue={undefined} />
|
||||
|
||||
<h2>Label skjult visuelt</h2>
|
||||
<BasicCombobox options={nordicCountries} hideLabel />
|
||||
@@ -499,18 +425,9 @@ export const ScreenreaderTesting = () => (
|
||||
<BasicCombobox readOnly />
|
||||
|
||||
<h2>Readonly uten verdi</h2>
|
||||
<BasicCombobox readOnly defaultSelectedOptions={[]} />
|
||||
<BasicCombobox readOnly defaultValue={undefined} />
|
||||
|
||||
<h2>Disabled</h2>
|
||||
<BasicCombobox disabled />
|
||||
</div>
|
||||
);
|
||||
|
||||
/* TODO:
|
||||
|
||||
- Navngivning: Vurder å dele opp i separate komponenter for single- og multiselect.
|
||||
- Hvordan unngå CSS-konflikt med dagens CB?
|
||||
- Vurder om Label og Description (og error?) skal være sub-komponenter eller ikke.
|
||||
- Test med skjermleser
|
||||
|
||||
*/
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta } from "@storybook/react-vite";
|
||||
import React, { useState } from "react";
|
||||
import { expect, userEvent, within } from "storybook/test";
|
||||
import { Combobox, type ComboboxProps } from "./Combobox";
|
||||
import { ComboboxSingle } from "./Combobox";
|
||||
import { ComboboxFilter } from "./filter/ComboboxFilter";
|
||||
import { ComboboxList } from "./list/ComboboxList";
|
||||
import { ComboboxPopup } from "./popup/ComboboxPopup";
|
||||
@@ -59,20 +59,14 @@ const groupedOptions = [
|
||||
|
||||
function BaseCombobox<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
>(props: Partial<ComboboxProps<T>>) {
|
||||
const [selectedOptions, setSelectedOptions] = useState(["option-1"]);
|
||||
>(props: Partial<ComboboxSingle.Props<T>>) {
|
||||
const [value, setValue] = useState<string | undefined>("option-1");
|
||||
|
||||
return (
|
||||
<Combobox
|
||||
<ComboboxSingle
|
||||
options={countries as T[]}
|
||||
selectedOptions={selectedOptions}
|
||||
onToggleOption={(option, newSelected) => {
|
||||
setSelectedOptions((prev) =>
|
||||
newSelected
|
||||
? [...prev, option.value]
|
||||
: prev.filter((v) => v !== option.value),
|
||||
);
|
||||
}}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
label="Velg land"
|
||||
description="Landet hvor du er født."
|
||||
{...props}
|
||||
@@ -99,7 +93,7 @@ const optionMemoTestRenderFn = (
|
||||
return optOrGroup.label;
|
||||
};
|
||||
export const OptionMemoization = () => {
|
||||
const [selectedOptions, setSelectedOptions] = useState(["option-1"]);
|
||||
const [value, setValue] = useState(["option-1"]);
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "450px" }}>
|
||||
@@ -112,9 +106,9 @@ export const OptionMemoization = () => {
|
||||
<ComboboxRoot
|
||||
defaultOpen
|
||||
options={groupedOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
value={value}
|
||||
onToggleOption={(option) => {
|
||||
setSelectedOptions((prev) =>
|
||||
setValue((prev) =>
|
||||
prev.includes(option.value)
|
||||
? prev.filter((v) => v !== option.value)
|
||||
: [...prev, option.value],
|
||||
|
||||
@@ -13,9 +13,9 @@ interface ComboboxFieldProps extends React.ButtonHTMLAttributes<HTMLButtonElemen
|
||||
|
||||
const ComboboxField = forwardRef<HTMLButtonElement, ComboboxFieldProps>(
|
||||
({ children, hasError, ...rest }, ref) => {
|
||||
const { options, selectedOptions, size } = useComboboxRootContext();
|
||||
const { options, value, size } = useComboboxRootContext();
|
||||
|
||||
const labels = getLabels(selectedOptions, options);
|
||||
const labels = getLabels(value, options);
|
||||
const label =
|
||||
children ?? (labels.length > 1 ? `${labels.length} valgt` : labels[0]);
|
||||
|
||||
@@ -45,7 +45,7 @@ const ComboboxField = forwardRef<HTMLButtonElement, ComboboxFieldProps>(
|
||||
);
|
||||
|
||||
const getLabels = (
|
||||
selectedOptions: ComboboxRootProps["selectedOptions"],
|
||||
value: ComboboxRootProps["value"],
|
||||
options: ComboboxRootProps["options"],
|
||||
) => {
|
||||
const labels: string[] = [];
|
||||
@@ -54,13 +54,13 @@ const getLabels = (
|
||||
if ("options" in optOrGroup) {
|
||||
// It's a group
|
||||
for (const option of optOrGroup.options) {
|
||||
if (selectedOptions.includes(option.value)) {
|
||||
if (value.includes(option.value)) {
|
||||
labels.push(option.label);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// It's a single option
|
||||
if (selectedOptions.includes(optOrGroup.value)) {
|
||||
if (value.includes(optOrGroup.value)) {
|
||||
labels.push(optOrGroup.label);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,12 @@ type ComboboxFilterProps = Pick<
|
||||
|
||||
export const ComboboxFilter = (props: ComboboxFilterProps) => {
|
||||
const { size } = useComboboxRootContext();
|
||||
const { filterString, setFilterString, virtuallyFocusedOptionValue } =
|
||||
useComboboxPopupContext();
|
||||
const {
|
||||
filterString,
|
||||
setFilterString,
|
||||
virtuallyFocusedOptionValue,
|
||||
setVirtuallyFocusedOptionValue,
|
||||
} = useComboboxPopupContext();
|
||||
|
||||
return (
|
||||
<div className="aksel-combobox2__filter">
|
||||
@@ -30,6 +34,11 @@ export const ComboboxFilter = (props: ComboboxFilterProps) => {
|
||||
event.preventDefault(); // Prevents navigation with screen reader
|
||||
}
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (["ArrowLeft", "ArrowRight"].includes(event.key)) {
|
||||
setVirtuallyFocusedOptionValue("");
|
||||
}
|
||||
}}
|
||||
aria-autocomplete="list"
|
||||
inputMode="text"
|
||||
/>
|
||||
|
||||
@@ -47,28 +47,22 @@ function ComboboxList<
|
||||
function ComboboxListContent<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
>({ children }: ComboboxListProps<T>) {
|
||||
const { virtuallyFocusedOptionValue, filterString } =
|
||||
const { virtuallyFocusedOptionValue, filterString, setAnnounceText } =
|
||||
useComboboxPopupContext();
|
||||
const {
|
||||
setOpen,
|
||||
options,
|
||||
selectedOptions,
|
||||
onToggleOption,
|
||||
multiselect,
|
||||
triggerRef,
|
||||
} = useComboboxRootContext();
|
||||
const { setOpen, options, value, onToggleOption, multiple, triggerRef } =
|
||||
useComboboxRootContext();
|
||||
const memoizedOnToggleOption = useEventCallback(onToggleOption);
|
||||
const deferredFilterString = useDeferredValue(filterString);
|
||||
|
||||
const localOnToggleOption = useCallback(
|
||||
(option: ComboboxOptionData, isSelected: boolean) => {
|
||||
memoizedOnToggleOption(option, isSelected);
|
||||
if (!multiselect) {
|
||||
if (!multiple) {
|
||||
setOpen(false);
|
||||
triggerRef.current?.focus();
|
||||
}
|
||||
},
|
||||
[memoizedOnToggleOption, multiselect, triggerRef, setOpen],
|
||||
[memoizedOnToggleOption, multiple, triggerRef, setOpen],
|
||||
);
|
||||
|
||||
if (children && typeof children !== "function") {
|
||||
@@ -80,6 +74,7 @@ function ComboboxListContent<
|
||||
: options;
|
||||
|
||||
if (filteredOptions.length === 0) {
|
||||
setAnnounceText("Ingen treff");
|
||||
return (
|
||||
<div className="aksel-combobox2__no-results" role="status">
|
||||
Ingen treff
|
||||
@@ -87,6 +82,8 @@ function ComboboxListContent<
|
||||
);
|
||||
}
|
||||
|
||||
setAnnounceText("");
|
||||
|
||||
return filteredOptions.map((optOrGroup) =>
|
||||
"options" in optOrGroup ? (
|
||||
<ComboboxGroup
|
||||
@@ -99,7 +96,7 @@ function ComboboxListContent<
|
||||
key={option.value}
|
||||
option={option}
|
||||
onToggleOption={localOnToggleOption}
|
||||
isSelected={selectedOptions.includes(option.value)}
|
||||
isSelected={value.includes(option.value)}
|
||||
hasVirtualFocus={virtuallyFocusedOptionValue === option.value}
|
||||
highlightText={deferredFilterString}
|
||||
>
|
||||
@@ -112,7 +109,7 @@ function ComboboxListContent<
|
||||
key={optOrGroup.value}
|
||||
option={optOrGroup}
|
||||
onToggleOption={localOnToggleOption}
|
||||
isSelected={selectedOptions.includes(optOrGroup.value)}
|
||||
isSelected={value.includes(optOrGroup.value)}
|
||||
hasVirtualFocus={virtuallyFocusedOptionValue === optOrGroup.value}
|
||||
highlightText={deferredFilterString} // TODO: Vurder å kunne slå av dette for bedre ytelse
|
||||
>
|
||||
|
||||
@@ -103,6 +103,7 @@ function ComboboxModal({
|
||||
useEffect(() => {
|
||||
// See "Overlay" section in CSS file for context. This handles the case when
|
||||
// the list doesn't overflow and overscroll-behavior doesn't work on iOS.
|
||||
// Works best on iOS 27. On iOS 26 you can scroll a little before it triggers.
|
||||
if (open) {
|
||||
const scrollToTop = () => (document.documentElement.scrollTop = 0);
|
||||
visualViewport?.addEventListener("scroll", scrollToTop);
|
||||
|
||||
@@ -57,17 +57,15 @@ const groupedOptions: (MyGroup | MyOption)[] = [
|
||||
];
|
||||
|
||||
export const Default = () => {
|
||||
const [selectedOptions, setSelectedOptions] = useState<MyOption["value"][]>([
|
||||
"opt-3",
|
||||
]);
|
||||
const [value, setValue] = useState<MyOption["value"][]>(["opt-3"]);
|
||||
|
||||
return (
|
||||
<ComboboxRoot
|
||||
defaultOpen
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
value={value}
|
||||
onToggleOption={(option, newSelected) => {
|
||||
setSelectedOptions((prev) =>
|
||||
setValue((prev) =>
|
||||
newSelected
|
||||
? [...prev, option.value]
|
||||
: prev.filter((v) => v !== option.value),
|
||||
@@ -89,7 +87,7 @@ export const SingleSelect = () => {
|
||||
<ComboboxRoot
|
||||
defaultOpen
|
||||
options={options}
|
||||
selectedOptions={[selectedOption.value]}
|
||||
value={[selectedOption.value]}
|
||||
onToggleOption={setSelectedOption}
|
||||
>
|
||||
<ComboboxPopup>
|
||||
@@ -108,7 +106,7 @@ export const Groups = () => {
|
||||
<ComboboxRoot
|
||||
defaultOpen
|
||||
options={groupedOptions}
|
||||
selectedOptions={[selectedOption.value]}
|
||||
value={[selectedOption.value]}
|
||||
onToggleOption={setSelectedOption}
|
||||
>
|
||||
<ComboboxPopup>
|
||||
@@ -121,7 +119,7 @@ export const Groups = () => {
|
||||
size="small"
|
||||
defaultOpen
|
||||
options={groupedOptions}
|
||||
selectedOptions={[selectedOption.value]}
|
||||
value={[selectedOption.value]}
|
||||
onToggleOption={setSelectedOption}
|
||||
>
|
||||
<ComboboxPopup>
|
||||
@@ -147,7 +145,7 @@ export const WithoutFilter = () => {
|
||||
options: options.slice(0, 3),
|
||||
},
|
||||
]}
|
||||
selectedOptions={[selectedOption.value]}
|
||||
value={[selectedOption.value]}
|
||||
onToggleOption={setSelectedOption}
|
||||
>
|
||||
<ComboboxPopup>
|
||||
@@ -158,7 +156,7 @@ export const WithoutFilter = () => {
|
||||
<ComboboxRoot
|
||||
defaultOpen
|
||||
options={options.slice(0, 3)}
|
||||
selectedOptions={[selectedOption.value]}
|
||||
value={[selectedOption.value]}
|
||||
onToggleOption={setSelectedOption}
|
||||
>
|
||||
<ComboboxPopup>
|
||||
@@ -170,20 +168,18 @@ export const WithoutFilter = () => {
|
||||
};
|
||||
|
||||
export const CustomOptionRendering = () => {
|
||||
const [selectedOptions, setSelectedOptions] = useState<MyOption["value"][]>([
|
||||
"opt-1",
|
||||
]);
|
||||
const [value, setValue] = useState<MyOption["value"][]>(["opt-1"]);
|
||||
|
||||
return (
|
||||
<ComboboxRoot
|
||||
defaultOpen
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
value={value}
|
||||
onToggleOption={(option, isSelected) => {
|
||||
setSelectedOptions(
|
||||
setValue(
|
||||
isSelected
|
||||
? [...selectedOptions, option.value]
|
||||
: selectedOptions.filter((v) => v !== option.value),
|
||||
? [...value, option.value]
|
||||
: value.filter((v) => v !== option.value),
|
||||
);
|
||||
}}
|
||||
>
|
||||
@@ -212,7 +208,7 @@ export const CustomOptionRenderingGroups = () => {
|
||||
<ComboboxRoot
|
||||
defaultOpen
|
||||
options={groupedOptions}
|
||||
selectedOptions={[selectedOption.value]}
|
||||
value={[selectedOption.value]}
|
||||
onToggleOption={setSelectedOption}
|
||||
>
|
||||
<ComboboxPopup>
|
||||
@@ -239,9 +235,7 @@ export const CustomOptionRenderingGroups = () => {
|
||||
// Alternativer: Kun støtte option, måtte iterere selv, renderOption/group-props på root, flytte props ned (hvordan få tak i valgte i Input?) 🤔
|
||||
|
||||
export const ControlledInput = () => {
|
||||
const [selectedOptions, setSelectedOptions] = useState<MyOption["value"][]>([
|
||||
"opt-1",
|
||||
]);
|
||||
const [value, setValue] = useState<MyOption["value"][]>(["opt-1"]);
|
||||
const [filterString, setFilterString] = useState("");
|
||||
const filterStringLowerCase = filterString.toLocaleLowerCase();
|
||||
|
||||
@@ -266,9 +260,9 @@ export const ControlledInput = () => {
|
||||
<ComboboxRoot
|
||||
defaultOpen
|
||||
options={filteredOptions}
|
||||
selectedOptions={selectedOptions}
|
||||
value={value}
|
||||
onToggleOption={(option) => {
|
||||
setSelectedOptions((prev) =>
|
||||
setValue((prev) =>
|
||||
prev.includes(option.value)
|
||||
? prev.filter((v) => v !== option.value)
|
||||
: [...prev, option.value],
|
||||
@@ -290,7 +284,7 @@ export const States: StoryFn = () => {
|
||||
<ComboboxRoot
|
||||
defaultOpen
|
||||
options={options.slice(0, 3)}
|
||||
selectedOptions={[selectedOption.value]}
|
||||
value={[selectedOption.value]}
|
||||
onToggleOption={setSelectedOption}
|
||||
>
|
||||
<ComboboxPopup>
|
||||
|
||||
@@ -5,8 +5,10 @@ import { useComboboxRootContext } from "../root/ComboboxRoot";
|
||||
|
||||
interface ComboboxPopupContextProps {
|
||||
virtuallyFocusedOptionValue: string;
|
||||
setVirtuallyFocusedOptionValue: (value: string) => void;
|
||||
filterString: string;
|
||||
setFilterString: (value: string) => void;
|
||||
setAnnounceText: (text: string) => void;
|
||||
}
|
||||
|
||||
export const {
|
||||
@@ -27,6 +29,7 @@ export const ComboboxPopup = ({ children }: ComboboxPopupProps) => {
|
||||
const [filterString, setFilterString] = useState("");
|
||||
const rootContext = useComboboxRootContext();
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [announceText, setAnnounceText] = useState("");
|
||||
|
||||
useEffect(
|
||||
function onOpen() {
|
||||
@@ -80,10 +83,18 @@ export const ComboboxPopup = ({ children }: ComboboxPopupProps) => {
|
||||
>
|
||||
<ComboboxPopupContextProvider
|
||||
virtuallyFocusedOptionValue={virtuallyFocusedOptionValue}
|
||||
setVirtuallyFocusedOptionValue={setVirtuallyFocusedOptionValue}
|
||||
filterString={filterString}
|
||||
setFilterString={setFilterString}
|
||||
setFilterString={(newString) => {
|
||||
setFilterString(newString);
|
||||
setVirtuallyFocusedOptionValue("");
|
||||
}}
|
||||
setAnnounceText={setAnnounceText}
|
||||
>
|
||||
{children}
|
||||
<div className="aksel-sr-only" role="status">
|
||||
{announceText}
|
||||
</div>
|
||||
</ComboboxPopupContextProvider>
|
||||
</Listbox>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,26 @@ import { cl, createStrictContext } from "../../../utils/helpers";
|
||||
import type { FormFieldProps } from "../../useFormField";
|
||||
import type { ComboboxOptionProps } from "../option/ComboboxOption";
|
||||
|
||||
type HtmlAttributes = Omit<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
| "onChange"
|
||||
| "defaultChecked"
|
||||
| "autoCapitalize"
|
||||
| "contentEditable"
|
||||
| "enterKeyHint"
|
||||
| "spellCheck"
|
||||
| "inputMode"
|
||||
| "autoCorrect"
|
||||
| "autoSave"
|
||||
| "color"
|
||||
| "results"
|
||||
| "security"
|
||||
| "unselectable"
|
||||
| "popover"
|
||||
| "popoverTargetAction"
|
||||
| "popoverTarget"
|
||||
>;
|
||||
|
||||
/** Resolves the option type based on whether the parameter is a group or a single option */
|
||||
export type ResolveOption<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
@@ -28,9 +48,7 @@ export interface ComboboxRootProps<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData> =
|
||||
ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
>
|
||||
extends
|
||||
Pick<FormFieldProps, "disabled">,
|
||||
React.HTMLAttributes<HTMLDivElement> {
|
||||
extends Pick<FormFieldProps, "disabled" | "size">, HtmlAttributes {
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* Can be either an array of options ({@link ComboboxOptionData})
|
||||
@@ -40,14 +58,25 @@ export interface ComboboxRootProps<
|
||||
* put the single options first.
|
||||
*/
|
||||
options: T[];
|
||||
selectedOptions: ResolveOption<T>["value"][]; // TODO: Vurder navn: value(s)
|
||||
onToggleOption: ComboboxOptionProps<ResolveOption<T>>["onToggleOption"];
|
||||
defaultOpen?: boolean;
|
||||
multiselect?: boolean; // TODO: Hva skal være default? Vurder navn
|
||||
/**
|
||||
* @default "medium"
|
||||
* Value of the selected options.
|
||||
*/
|
||||
size?: "small" | "medium";
|
||||
value: ResolveOption<T>["value"][];
|
||||
/**
|
||||
* Called when an option is toggled.
|
||||
* @param option The option that was toggled.
|
||||
* @param isSelected The new selected state of the option.
|
||||
*/
|
||||
onToggleOption: ComboboxOptionProps<ResolveOption<T>>["onToggleOption"];
|
||||
/**
|
||||
* Whether the combobox is open by default.
|
||||
*/
|
||||
defaultOpen?: boolean;
|
||||
/**
|
||||
* Whether multiple options can be selected.
|
||||
* @default true
|
||||
*/
|
||||
multiple?: boolean; // TODO: Hva skal være default?
|
||||
}
|
||||
|
||||
export interface ComboboxRootContextProps {
|
||||
@@ -55,9 +84,9 @@ export interface ComboboxRootContextProps {
|
||||
setOpen: (newOpen: boolean) => void;
|
||||
triggerRef: React.RefObject<HTMLDivElement | null>;
|
||||
options: ComboboxRootProps["options"];
|
||||
selectedOptions: ComboboxRootProps["selectedOptions"];
|
||||
value: ComboboxRootProps["value"];
|
||||
onToggleOption: ComboboxRootProps["onToggleOption"];
|
||||
multiselect: boolean;
|
||||
multiple: boolean;
|
||||
size: Exclude<ComboboxRootProps["size"], undefined>;
|
||||
disabled: ComboboxRootProps["disabled"];
|
||||
}
|
||||
@@ -75,10 +104,10 @@ export function ComboboxRoot<
|
||||
>({
|
||||
children,
|
||||
options,
|
||||
selectedOptions,
|
||||
value,
|
||||
onToggleOption,
|
||||
defaultOpen = false,
|
||||
multiselect = true,
|
||||
multiple = true,
|
||||
size = "medium",
|
||||
disabled,
|
||||
className,
|
||||
@@ -99,11 +128,11 @@ export function ComboboxRoot<
|
||||
setOpen={setOpen}
|
||||
triggerRef={triggerRef}
|
||||
options={options}
|
||||
selectedOptions={selectedOptions}
|
||||
value={value}
|
||||
onToggleOption={
|
||||
onToggleOption as ComboboxRootContextProps["onToggleOption"]
|
||||
}
|
||||
multiselect={multiselect}
|
||||
multiple={multiple}
|
||||
size={size}
|
||||
disabled={disabled}
|
||||
>
|
||||
|
||||
@@ -29,7 +29,10 @@ const ComboboxTrigger = ({
|
||||
if (!readOnly && !rootContext.open) event.preventDefault();
|
||||
}}
|
||||
onKeyDown={(event) => {
|
||||
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
|
||||
if (
|
||||
!readOnly &&
|
||||
(event.key === "ArrowDown" || event.key === "ArrowUp")
|
||||
) {
|
||||
rootContext.setOpen(true);
|
||||
// Stop keydown from scrolling window
|
||||
event.preventDefault();
|
||||
|
||||
@@ -16,6 +16,7 @@ export interface FormFieldProps {
|
||||
errorId?: string;
|
||||
/**
|
||||
* Changes font-size, padding and gaps.
|
||||
* @default "medium"
|
||||
*/
|
||||
size?: "medium" | "small";
|
||||
/**
|
||||
|
||||
@@ -3,3 +3,4 @@ export { DataGrid } from "./data-grid";
|
||||
export { LookupDeprecated as Lookup } from "./lookup/Lookup";
|
||||
export { BadgeDeprecated as Badge } from "./badge/Badge";
|
||||
export { RadioCard, RadioCardGroup } from "./form/radio-card";
|
||||
export { CheckboxCard, CheckboxCardGroup } from "./form/checkbox-card";
|
||||
@@ -4,7 +4,7 @@ import type { ToggleGroupProps } from "./ToggleGroup.types";
|
||||
import type { useToggleGroup } from "./useToggleGroup";
|
||||
|
||||
interface ToggleContextProps {
|
||||
size: "medium" | "small";
|
||||
size: "medium" | "small" | "xsmall";
|
||||
}
|
||||
|
||||
export const ToggleGroupContext = ReactCreateContext<ToggleContextProps | null>(
|
||||
|
||||
@@ -147,6 +147,32 @@ export const Small = () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const XSmall = () => {
|
||||
const [activeValue, setActiveValue] = useState("ulest");
|
||||
|
||||
return (
|
||||
<VStack gap="space-24">
|
||||
<ToggleGroup size="xsmall" value={activeValue} onChange={setActiveValue}>
|
||||
{Items()}
|
||||
</ToggleGroup>
|
||||
<ToggleGroup size="xsmall" value={activeValue} onChange={setActiveValue}>
|
||||
{Items(true, true)}
|
||||
</ToggleGroup>
|
||||
<ToggleGroup size="xsmall" value={activeValue} onChange={setActiveValue}>
|
||||
{Items(true)}
|
||||
</ToggleGroup>
|
||||
<ToggleGroup
|
||||
size="xsmall"
|
||||
fill
|
||||
value={activeValue}
|
||||
onChange={setActiveValue}
|
||||
>
|
||||
{Items(true)}
|
||||
</ToggleGroup>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export const PlayControlled: Story = {
|
||||
render: () => {
|
||||
const [activeValue, setActiveValue] = useState("ny");
|
||||
@@ -205,4 +231,5 @@ export const Chromatic = renderStoriesForChromatic({
|
||||
Label,
|
||||
Compositions,
|
||||
Small,
|
||||
XSmall,
|
||||
});
|
||||
@@ -65,6 +65,8 @@ export const ToggleGroup = forwardRef<HTMLDivElement, ToggleGroupProps>(
|
||||
|
||||
const labelId = useId();
|
||||
|
||||
const labelSize = size === "xsmall" ? "small" : size;
|
||||
|
||||
if (!value && !defaultValue) {
|
||||
console.error("ToggleGroup without value or defaultvalue is not allowed");
|
||||
}
|
||||
@@ -84,7 +86,7 @@ export const ToggleGroup = forwardRef<HTMLDivElement, ToggleGroupProps>(
|
||||
{label && (
|
||||
<Label
|
||||
as="div"
|
||||
size={size}
|
||||
size={labelSize}
|
||||
className="aksel-toggle-group__label"
|
||||
id={labelId}
|
||||
>
|
||||
|
||||
@@ -14,7 +14,7 @@ export interface ToggleGroupProps extends Omit<
|
||||
* Changes padding and font-size.
|
||||
* @default "medium"
|
||||
*/
|
||||
size?: "medium" | "small";
|
||||
size?: "medium" | "small" | "xsmall";
|
||||
/**
|
||||
* Controlled selected value.
|
||||
*/
|
||||
|
||||
@@ -74,6 +74,7 @@ const ToggleItem = forwardRef<HTMLButtonElement, ToggleGroupItemProps>(
|
||||
onKeyDown,
|
||||
});
|
||||
const ctx = useToggleGroupContext();
|
||||
const size = ctx?.size === "xsmall" ? "small" : ctx?.size;
|
||||
|
||||
return (
|
||||
<button
|
||||
@@ -86,7 +87,7 @@ const ToggleItem = forwardRef<HTMLButtonElement, ToggleGroupItemProps>(
|
||||
<BodyShort
|
||||
as="span"
|
||||
className="aksel-toggle-group__button-inner"
|
||||
size={ctx?.size}
|
||||
size={size}
|
||||
>
|
||||
{children ?? (
|
||||
<>
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { Meta, StoryFn } from "@storybook/react-vite";
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { Search } from "../../../form/search";
|
||||
import { Box } from "../../../primitives/box";
|
||||
import { BodyShort } from "../../../typography";
|
||||
import { useDeferredValue } from "../../hooks/useDeferredValue";
|
||||
import { HighlightText } from "../HighlightText/HighlightText";
|
||||
import { DismissableLayer } from "../dismissablelayer/DismissableLayer";
|
||||
@@ -17,84 +18,95 @@ const meta: Meta<typeof Listbox> = {
|
||||
};
|
||||
export default meta;
|
||||
|
||||
type MyItem = {
|
||||
type MyOption = {
|
||||
label: string;
|
||||
value: `item-${number}`;
|
||||
value: `opt-${number}`;
|
||||
metadata?: string;
|
||||
};
|
||||
const myItems: MyItem[] = [
|
||||
{ label: "Norway", value: "item-1", metadata: "foo1" },
|
||||
{ label: "Finland", value: "item-2" },
|
||||
{ label: "Sweden", value: "item-3", metadata: "foo3" },
|
||||
{ label: "Denmark", value: "item-4" },
|
||||
{ label: "Iceland", value: "item-5" },
|
||||
{ label: "Faroe Islands", value: "item-6" },
|
||||
{ label: "Åland Islands", value: "item-7" },
|
||||
{ label: "Estonia", value: "item-8" },
|
||||
{ label: "Latvia", value: "item-9" },
|
||||
{ label: "Lithuania", value: "item-10" },
|
||||
|
||||
const nordicCountries: MyOption[] = [
|
||||
{ label: "Norge", value: "opt-1", metadata: "foo1" },
|
||||
{ label: "Finland", value: "opt-2" },
|
||||
{ label: "Sverige", value: "opt-3", metadata: "foo3" },
|
||||
{ label: "Danmark", value: "opt-4" },
|
||||
{ label: "Island", value: "opt-5" },
|
||||
{ label: "Grønland", value: "opt-6" },
|
||||
{ label: "Færøyene", value: "opt-7" },
|
||||
{ label: "Åland", value: "opt-8" },
|
||||
];
|
||||
const centralEuropeanCountries: MyOption[] = [
|
||||
{ label: "Tyskland", value: "opt-9" },
|
||||
{ label: "Østerrike", value: "opt-10" },
|
||||
{ label: "Sveits", value: "opt-11" },
|
||||
{ label: "Liechtenstein", value: "opt-12" },
|
||||
{ label: "Polen", value: "opt-13" },
|
||||
{ label: "Tsjekkia", value: "opt-14" },
|
||||
{ label: "Slovakia", value: "opt-15" },
|
||||
{ label: "Ungarn", value: "opt-16" },
|
||||
{ label: "Slovenia", value: "opt-17" },
|
||||
];
|
||||
const countries: MyOption[] = [...nordicCountries, ...centralEuropeanCountries];
|
||||
|
||||
type MyGroup = {
|
||||
label: string;
|
||||
id: `group-${number}`;
|
||||
items: MyItem[];
|
||||
options: MyOption[];
|
||||
};
|
||||
const groupedItems: (MyGroup | MyItem)[] = [
|
||||
{ label: "Single item first", value: "item-01" },
|
||||
|
||||
const groupedCountries: (MyGroup | MyOption)[] = [
|
||||
{
|
||||
label: "Nordic countries",
|
||||
label: "Norden",
|
||||
id: "group-1",
|
||||
items: myItems.slice(0, 6),
|
||||
options: nordicCountries,
|
||||
},
|
||||
{
|
||||
label: "Baltic countries",
|
||||
label: "Sentral-Europa",
|
||||
id: "group-2",
|
||||
items: myItems.slice(6),
|
||||
options: centralEuropeanCountries,
|
||||
},
|
||||
{ label: "Single item last", value: "item-02" },
|
||||
{ label: "Annet (ikke gruppert)", value: "opt-01" },
|
||||
];
|
||||
|
||||
interface RenderItemsProps {
|
||||
items: (MyGroup | MyItem)[];
|
||||
selectedItems: MyItem["value"][];
|
||||
interface RenderOptionsProps {
|
||||
options: (MyGroup | MyOption)[];
|
||||
selectedOptions: MyOption["value"][];
|
||||
filterString: string;
|
||||
virtuallyFocusedOptionId: string;
|
||||
onSelect: (item: MyItem) => void;
|
||||
onSelect: (option: MyOption) => void;
|
||||
}
|
||||
const RenderItems = ({
|
||||
items,
|
||||
selectedItems,
|
||||
const RenderOptions = ({
|
||||
options,
|
||||
selectedOptions,
|
||||
filterString,
|
||||
virtuallyFocusedOptionId,
|
||||
onSelect,
|
||||
}: RenderItemsProps) =>
|
||||
items.map((itemOrGroup) =>
|
||||
"items" in itemOrGroup ? (
|
||||
<Listbox.Group key={itemOrGroup.label} label={itemOrGroup.label}>
|
||||
{itemOrGroup.items.map((item) => (
|
||||
}: RenderOptionsProps) =>
|
||||
options.map((optionOrGroup) =>
|
||||
"options" in optionOrGroup ? (
|
||||
<Listbox.Group key={optionOrGroup.label} label={optionOrGroup.label}>
|
||||
{optionOrGroup.options.map((option) => (
|
||||
<Listbox.Option
|
||||
key={item.value}
|
||||
id={item.value}
|
||||
onClick={() => onSelect(item)}
|
||||
aria-selected={selectedItems.includes(item.value)}
|
||||
hasVirtualFocus={virtuallyFocusedOptionId === item.value}
|
||||
key={option.value}
|
||||
id={option.value}
|
||||
onClick={() => onSelect(option)}
|
||||
aria-selected={selectedOptions.includes(option.value)}
|
||||
hasVirtualFocus={virtuallyFocusedOptionId === option.value}
|
||||
>
|
||||
<HighlightText text={filterString}>{item.label}</HighlightText>
|
||||
{selectedItems.includes(item.value) && <Checkmark />}
|
||||
<HighlightText text={filterString}>{option.label}</HighlightText>
|
||||
{selectedOptions.includes(option.value) && <Checkmark />}
|
||||
</Listbox.Option>
|
||||
))}
|
||||
</Listbox.Group>
|
||||
) : (
|
||||
<Listbox.Option
|
||||
key={itemOrGroup.value}
|
||||
id={itemOrGroup.value}
|
||||
onClick={() => onSelect(itemOrGroup)}
|
||||
aria-selected={selectedItems.includes(itemOrGroup.value)}
|
||||
hasVirtualFocus={virtuallyFocusedOptionId === itemOrGroup.value}
|
||||
key={optionOrGroup.value}
|
||||
id={optionOrGroup.value}
|
||||
onClick={() => onSelect(optionOrGroup)}
|
||||
aria-selected={selectedOptions.includes(optionOrGroup.value)}
|
||||
hasVirtualFocus={virtuallyFocusedOptionId === optionOrGroup.value}
|
||||
>
|
||||
<HighlightText text={filterString}>{itemOrGroup.label}</HighlightText>
|
||||
{selectedItems.includes(itemOrGroup.value) && <Checkmark />}
|
||||
<HighlightText text={filterString}>{optionOrGroup.label}</HighlightText>
|
||||
{selectedOptions.includes(optionOrGroup.value) && <Checkmark />}
|
||||
</Listbox.Option>
|
||||
),
|
||||
);
|
||||
@@ -102,23 +114,23 @@ const Checkmark = () => <div style={{ float: "right" }}>✓</div>;
|
||||
|
||||
export const Default = () => {
|
||||
const [filterString, setFilterString] = useState("");
|
||||
const [selectedItem, setSelectedItem] = useState<MyItem["value"] | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedOption, setSelectedOption] = useState<
|
||||
MyOption["value"] | null
|
||||
>(null);
|
||||
const [virtuallyFocusedOptionId, setVirtuallyFocusedOptionId] = useState("");
|
||||
|
||||
const filteredItems = filterString
|
||||
? filterItems(groupedItems, filterString)
|
||||
: groupedItems;
|
||||
const filteredOptions = filterString
|
||||
? filterOptions(groupedCountries, filterString)
|
||||
: groupedCountries;
|
||||
|
||||
const onSelect = (item: MyItem) => {
|
||||
setSelectedItem(item.value);
|
||||
console.log(item);
|
||||
const onSelect = (option: MyOption) => {
|
||||
setSelectedOption(option.value);
|
||||
console.log(option);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>Selected: {selectedItem}</div>
|
||||
<div>Selected: {selectedOption}</div>
|
||||
|
||||
<Listbox setVirtuallyFocusedOptionId={setVirtuallyFocusedOptionId}>
|
||||
<Listbox.InputSlot virtuallyFocusedOptionId={virtuallyFocusedOptionId}>
|
||||
@@ -132,9 +144,9 @@ export const Default = () => {
|
||||
</Listbox.InputSlot>
|
||||
|
||||
<Listbox.Options>
|
||||
<RenderItems
|
||||
items={filteredItems}
|
||||
selectedItems={selectedItem ? [selectedItem] : []}
|
||||
<RenderOptions
|
||||
options={filteredOptions}
|
||||
selectedOptions={selectedOption ? [selectedOption] : []}
|
||||
filterString={filterString}
|
||||
virtuallyFocusedOptionId={virtuallyFocusedOptionId}
|
||||
onSelect={onSelect}
|
||||
@@ -152,14 +164,14 @@ interface OptimizedProps {
|
||||
export const Optimized: StoryFn<OptimizedProps> = ({ count, highlight }) => {
|
||||
const [filterString, setFilterString] = useState("");
|
||||
const deferredFilterString = useDeferredValue(filterString);
|
||||
const [selectedItem, setSelectedItem] = useState<string>("");
|
||||
const [selectedOption, setSelectedOption] = useState<string>("");
|
||||
const [virtuallyFocusedOptionId, setVirtuallyFocusedOptionId] = useState("");
|
||||
|
||||
const allOptions = useMemo(
|
||||
() =>
|
||||
Array.from(
|
||||
{ length: count },
|
||||
(_, i) => `Item ${String(i + 1).padStart(4, "0")}`,
|
||||
(_, i) => `Option ${String(i + 1).padStart(4, "0")}`,
|
||||
),
|
||||
[count],
|
||||
);
|
||||
@@ -174,7 +186,7 @@ export const Optimized: StoryFn<OptimizedProps> = ({ count, highlight }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<div>Selected: {selectedItem}</div>
|
||||
<div>Selected: {selectedOption}</div>
|
||||
|
||||
<Listbox setVirtuallyFocusedOptionId={setVirtuallyFocusedOptionId}>
|
||||
<Listbox.InputSlot virtuallyFocusedOptionId={virtuallyFocusedOptionId}>
|
||||
@@ -194,9 +206,9 @@ export const Optimized: StoryFn<OptimizedProps> = ({ count, highlight }) => {
|
||||
key={option}
|
||||
value={option}
|
||||
filterString={highlight ? deferredFilterString : ""}
|
||||
isSelected={selectedItem === option}
|
||||
isSelected={selectedOption === option}
|
||||
hasVirtualFocus={virtuallyFocusedOptionId === option}
|
||||
onSelect={setSelectedItem}
|
||||
onSelect={setSelectedOption}
|
||||
/>
|
||||
))}
|
||||
</Listbox.Options>
|
||||
@@ -246,26 +258,26 @@ const MyMemoizedOption = React.memo(
|
||||
|
||||
export const WithFloating = () => {
|
||||
const [filterString, setFilterString] = useState("");
|
||||
const [selectedItem, setSelectedItem] = useState<MyItem["value"] | null>(
|
||||
null,
|
||||
);
|
||||
const [selectedOption, setSelectedOption] = useState<
|
||||
MyOption["value"] | null
|
||||
>(null);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [virtuallyFocusedOptionId, setVirtuallyFocusedOptionId] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const filteredItems = filterString
|
||||
? filterItems(groupedItems, filterString)
|
||||
: groupedItems;
|
||||
const filteredOptions = filterString
|
||||
? filterOptions(groupedCountries, filterString)
|
||||
: groupedCountries;
|
||||
|
||||
const onSelect = (item: MyItem) => {
|
||||
setSelectedItem(item.value);
|
||||
const onSelect = (option: MyOption) => {
|
||||
setSelectedOption(option.value);
|
||||
//setOpen(false);
|
||||
console.log(item);
|
||||
console.log(option);
|
||||
};
|
||||
|
||||
return (
|
||||
<Floating>
|
||||
<div>Selected: {selectedItem}</div>
|
||||
<div>Selected: {selectedOption}</div>
|
||||
|
||||
<Listbox setVirtuallyFocusedOptionId={setVirtuallyFocusedOptionId}>
|
||||
<Floating.Anchor>
|
||||
@@ -304,9 +316,9 @@ export const WithFloating = () => {
|
||||
width="var(--__axc-floating-anchor-width)"
|
||||
>
|
||||
<Listbox.Options>
|
||||
<RenderItems
|
||||
items={filteredItems}
|
||||
selectedItems={selectedItem ? [selectedItem] : []}
|
||||
<RenderOptions
|
||||
options={filteredOptions}
|
||||
selectedOptions={selectedOption ? [selectedOption] : []}
|
||||
filterString={filterString}
|
||||
virtuallyFocusedOptionId={virtuallyFocusedOptionId}
|
||||
onSelect={onSelect}
|
||||
@@ -321,40 +333,145 @@ export const WithFloating = () => {
|
||||
);
|
||||
};
|
||||
|
||||
type Item = {
|
||||
export const UseCaseTeamkatalogen = () => {
|
||||
const [filterString, setFilterString] = useState("");
|
||||
const [selectedOption, setSelectedOption] = useState<
|
||||
MyOption["value"] | null
|
||||
>(null);
|
||||
const inputRef = React.useRef<HTMLInputElement>(null);
|
||||
const [virtuallyFocusedOptionId, setVirtuallyFocusedOptionId] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const filteredOptions =
|
||||
filterString.length > 2 ? filterOptions(countries, filterString) : null;
|
||||
|
||||
const onSelect = (option: MyOption) => {
|
||||
setSelectedOption(option.value);
|
||||
close();
|
||||
console.log(option);
|
||||
};
|
||||
|
||||
function close() {
|
||||
setVirtuallyFocusedOptionId("");
|
||||
setFilterString("");
|
||||
setOpen(false);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: "500px" }}>
|
||||
<Floating>
|
||||
<BodyShort spacing>
|
||||
Søkefelt der resultatene vises som alternativer i en listbox. Omtrent
|
||||
som dagens Combobox, men der valg fører til navigering og ikke huskes
|
||||
(i feltet).
|
||||
</BodyShort>
|
||||
|
||||
<BodyShort spacing>Valgt: {selectedOption}</BodyShort>
|
||||
|
||||
<Listbox setVirtuallyFocusedOptionId={setVirtuallyFocusedOptionId}>
|
||||
<Floating.Anchor>
|
||||
<Listbox.InputSlot
|
||||
virtuallyFocusedOptionId={virtuallyFocusedOptionId}
|
||||
>
|
||||
<Search
|
||||
label="Søk etter team, seksjon, person eller tagg"
|
||||
placeholder="Søk etter team, seksjon, person eller tagg"
|
||||
variant="simple"
|
||||
ref={inputRef}
|
||||
value={filterString}
|
||||
onChange={(newValue) => {
|
||||
setFilterString(newValue);
|
||||
if (!open && newValue.length > 2) {
|
||||
setOpen(true);
|
||||
}
|
||||
}}
|
||||
onClear={close}
|
||||
onClick={() => setOpen(true)}
|
||||
onFocus={() => setOpen(true)}
|
||||
/>
|
||||
</Listbox.InputSlot>
|
||||
</Floating.Anchor>
|
||||
{open && (
|
||||
<DismissableLayer
|
||||
asChild
|
||||
onDismiss={close}
|
||||
safeZone={{ anchor: inputRef.current }}
|
||||
>
|
||||
<Floating.Content
|
||||
align="start"
|
||||
side="bottom"
|
||||
fallbackPlacements={[]}
|
||||
sideOffset={4}
|
||||
>
|
||||
<Box
|
||||
background="default"
|
||||
borderWidth="1"
|
||||
maxHeight="calc(var(--__axc-floating-available-height) - 4px)"
|
||||
width="var(--__axc-floating-anchor-width)"
|
||||
>
|
||||
{filteredOptions && filteredOptions.length > 0 && (
|
||||
<Listbox.Options>
|
||||
<RenderOptions
|
||||
options={filteredOptions}
|
||||
selectedOptions={[]}
|
||||
filterString={filterString}
|
||||
virtuallyFocusedOptionId={virtuallyFocusedOptionId}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
</Listbox.Options>
|
||||
)}
|
||||
{filteredOptions && filteredOptions.length === 0 && (
|
||||
<Box padding="space-20" role="status">
|
||||
Ingen resultater
|
||||
</Box>
|
||||
)}
|
||||
{!filteredOptions && (
|
||||
<Box padding="space-20">Skriv minst 3 tegn for å søke</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Floating.Content>
|
||||
</DismissableLayer>
|
||||
)}
|
||||
</Listbox>
|
||||
</Floating>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
type Option = {
|
||||
label: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
type Group<T extends Item> = {
|
||||
type Group<T extends Option> = {
|
||||
label: string;
|
||||
id: string;
|
||||
items: T[];
|
||||
options: T[];
|
||||
};
|
||||
|
||||
function filterItems<T extends (Item | Group<Item>)[]>(
|
||||
items: T,
|
||||
function filterOptions<T extends (Option | Group<Option>)[]>(
|
||||
options: T,
|
||||
filterString: string,
|
||||
): T {
|
||||
const filterStringLowerCase = filterString.toLocaleLowerCase();
|
||||
|
||||
return items
|
||||
.map((itemOrGroup) => {
|
||||
if ("items" in itemOrGroup) {
|
||||
const matchingItems = itemOrGroup.items.filter((item) =>
|
||||
item.label.toLocaleLowerCase().includes(filterStringLowerCase),
|
||||
return options
|
||||
.map((optionOrGroup) => {
|
||||
if ("options" in optionOrGroup) {
|
||||
const matchingOptions = optionOrGroup.options.filter((option) =>
|
||||
option.label.toLocaleLowerCase().includes(filterStringLowerCase),
|
||||
);
|
||||
if (matchingItems.length > 0) {
|
||||
return { ...itemOrGroup, items: matchingItems };
|
||||
if (matchingOptions.length > 0) {
|
||||
return { ...optionOrGroup, options: matchingOptions };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (
|
||||
itemOrGroup.label.toLocaleLowerCase().includes(filterStringLowerCase)
|
||||
optionOrGroup.label.toLocaleLowerCase().includes(filterStringLowerCase)
|
||||
) {
|
||||
return itemOrGroup;
|
||||
return optionOrGroup;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
.filter((item) => item !== null) as T;
|
||||
.filter((option) => option !== null) as T;
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
import type { PartialTranslations } from "./i18n.types";
|
||||
import type { Component, ComponentTranslation } from "./i18n.types";
|
||||
|
||||
// Based on https://github.com/Shopify/polaris/blob/main/polaris-react/src/utilities/get.ts
|
||||
|
||||
export function get(
|
||||
export function get<T extends Component>(
|
||||
keypath: string,
|
||||
objs: (PartialTranslations | undefined)[],
|
||||
objs: (ComponentTranslation<T> | undefined)[],
|
||||
) {
|
||||
const keys = keypath.split(".");
|
||||
|
||||
@@ -13,7 +13,7 @@ export function get(
|
||||
continue;
|
||||
}
|
||||
|
||||
let acc: string | PartialTranslations = obj;
|
||||
let acc: string | ComponentTranslation<T> = obj;
|
||||
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const val = acc[keys[i]];
|
||||
|
||||
@@ -4,7 +4,7 @@ import { bench, describe } from "vitest";
|
||||
import { Provider } from "../../provider";
|
||||
import { get } from "./get";
|
||||
import { useI18n } from "./i18n.hooks";
|
||||
import type { PartialTranslations } from "./i18n.types";
|
||||
import type { ComponentTranslation } from "./i18n.types";
|
||||
import { nb } from "./locales";
|
||||
|
||||
describe("translate function throughput", () => {
|
||||
@@ -54,11 +54,11 @@ describe("translate with multiple context objects", () => {
|
||||
});
|
||||
|
||||
describe("get() function throughput", () => {
|
||||
const objects: (PartialTranslations | undefined)[] = [
|
||||
const objects: (ComponentTranslation<"FileUpload"> | undefined)[] = [
|
||||
undefined,
|
||||
{ FileUpload: { item: { uploading: "First" } } },
|
||||
{ FileUpload: { item: { downloading: "Second" } } },
|
||||
{ FileUpload: nb.FileUpload },
|
||||
{ item: { uploading: "First" } },
|
||||
{ item: { downloading: "Second" } },
|
||||
nb.FileUpload,
|
||||
];
|
||||
|
||||
bench("shallow key hit in first object", () => {
|
||||
@@ -74,6 +74,6 @@ describe("get() function throughput", () => {
|
||||
});
|
||||
|
||||
bench("long key dot notation parsing", () => {
|
||||
get("dropzone.dragAndDropMultiple", [{ FileUpload: nb.FileUpload }]);
|
||||
get("dropzone.dragAndDropMultiple", [nb.FileUpload]);
|
||||
});
|
||||
});
|
||||
@@ -110,6 +110,44 @@ describe("useI18n", () => {
|
||||
const translate = result.current;
|
||||
expect(() => translate("item.uploading", { other: "John" })).toThrowError();
|
||||
});
|
||||
|
||||
test("should use internal defaults without entries in the public locales", () => {
|
||||
const { result } = renderHook(() => useI18n("TEST_DATA"), {
|
||||
wrapper: ({ children }) => <Provider locale={en}>{children}</Provider>,
|
||||
});
|
||||
|
||||
expect(result.current("label")).toBe("Internal grid");
|
||||
expect(result.current("selected", { count: 3 })).toBe("3 rows selected");
|
||||
/* @ts-expect-error Testing nonexistent key */
|
||||
expect(() => result.current("nonexistentKey")).toThrowError();
|
||||
});
|
||||
|
||||
test("should prioritize local internal translations and fall back to internal defaults", () => {
|
||||
const { result } = renderHook(() =>
|
||||
useI18n(
|
||||
"TEST_DATA",
|
||||
undefined,
|
||||
{ label: "First override" },
|
||||
{ label: "Second override" },
|
||||
),
|
||||
);
|
||||
|
||||
expect(result.current("label")).toBe("First override");
|
||||
expect(result.current("selected", { count: 2 })).toBe("2 rows selected");
|
||||
});
|
||||
|
||||
test("should update internal overrides without changing the translate function", () => {
|
||||
const { result, rerender } = renderHook(
|
||||
({ label }) => useI18n("TEST_DATA", { label }),
|
||||
{ initialProps: { label: "Initial label" } },
|
||||
);
|
||||
const translate = result.current;
|
||||
|
||||
rerender({ label: "Updated label" });
|
||||
|
||||
expect(result.current).toBe(translate);
|
||||
expect(translate("label")).toBe("Updated label");
|
||||
});
|
||||
});
|
||||
|
||||
describe("useDateLocale", () => {
|
||||
|
||||
@@ -2,12 +2,8 @@ import type { Locale } from "date-fns";
|
||||
import { useCallback, useRef } from "react";
|
||||
import { useProvider } from "../../provider/Provider";
|
||||
import { get } from "./get";
|
||||
import type {
|
||||
Component,
|
||||
ComponentTranslation,
|
||||
PartialTranslations,
|
||||
TFunction,
|
||||
} from "./i18n.types";
|
||||
import { internalTranslations } from "./i18n.internal";
|
||||
import type { Component, ComponentTranslation, TFunction } from "./i18n.types";
|
||||
|
||||
/* https://regex101.com/r/LYKWi3/1 */
|
||||
const REPLACE_REGEX = /{[^}]*}/g;
|
||||
@@ -17,13 +13,9 @@ function useI18n<T extends Component>(
|
||||
...localTranslations: (ComponentTranslation<T> | undefined)[]
|
||||
) {
|
||||
const context = useProvider();
|
||||
const contextTranslations = context.translations || [];
|
||||
const i18nObjects: (PartialTranslations | undefined)[] = [
|
||||
const i18nObjects: (ComponentTranslation<T> | undefined)[] = [
|
||||
...localTranslations,
|
||||
...(Array.isArray(contextTranslations)
|
||||
? contextTranslations.map((t) => t[componentName])
|
||||
: [contextTranslations[componentName]]),
|
||||
context.locale[componentName],
|
||||
...geti18nObjects(componentName, context),
|
||||
];
|
||||
|
||||
const i18nRef = useRef(i18nObjects);
|
||||
@@ -57,6 +49,31 @@ function useI18n<T extends Component>(
|
||||
return translate;
|
||||
}
|
||||
|
||||
function geti18nObjects(
|
||||
componentName: Component,
|
||||
context: ReturnType<typeof useProvider>,
|
||||
) {
|
||||
if (isInternalComponent(componentName)) {
|
||||
return [internalTranslations[componentName]];
|
||||
}
|
||||
|
||||
const contextTranslations = context.translations || [];
|
||||
const i18nObjects: (ComponentTranslation<Component> | undefined)[] = [
|
||||
...(Array.isArray(contextTranslations)
|
||||
? contextTranslations.map((t) => t[componentName])
|
||||
: [contextTranslations[componentName]]),
|
||||
context.locale[componentName],
|
||||
];
|
||||
|
||||
return i18nObjects;
|
||||
}
|
||||
|
||||
function isInternalComponent(
|
||||
componentName: Component,
|
||||
): componentName is keyof typeof internalTranslations {
|
||||
return componentName in internalTranslations;
|
||||
}
|
||||
|
||||
function useDateLocale() {
|
||||
const context = useProvider();
|
||||
const contextTranslations = context.translations || [];
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
/** Defaults for internal components, independent of the public locales. Cant be overridden by users. */
|
||||
const internalTranslations = {
|
||||
TEST_DATA: {
|
||||
label: "Internal grid",
|
||||
selected: "{count} rows selected",
|
||||
},
|
||||
} satisfies Record<string, Record<string, string | Record<string, string>>>;
|
||||
|
||||
export { internalTranslations };
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { internalTranslations } from "./i18n.internal";
|
||||
import type nb from "./locales/nb";
|
||||
|
||||
/* https://dev.to/pffigueiredo/typescript-utility-keyof-nested-object-2pa3 */
|
||||
@@ -7,8 +8,10 @@ type NestedKeyOf<ObjectType extends object> = {
|
||||
: `${Key}`;
|
||||
}[keyof ObjectType & (string | number)];
|
||||
|
||||
type InternalTranslations = typeof internalTranslations;
|
||||
|
||||
export type TFunction<T extends Component> = (
|
||||
keypath: NestedKeyOf<Translations[T]>,
|
||||
keypath: NestedKeyOf<(Translations & InternalTranslations)[T]>,
|
||||
replacements?: Record<string, string | number>,
|
||||
) => string;
|
||||
|
||||
@@ -21,8 +24,8 @@ export type Translations = typeof nb;
|
||||
|
||||
export type PartialTranslations = RecursivePartial<Translations>;
|
||||
|
||||
export type Component = keyof Translations;
|
||||
export type Component = keyof Translations | keyof InternalTranslations;
|
||||
|
||||
export type ComponentTranslation<T extends Component> = RecursivePartial<
|
||||
Translations[T]
|
||||
(Translations & InternalTranslations)[T]
|
||||
>;
|
||||
@@ -1,5 +1,7 @@
|
||||
# @navikt/ds-tailwind
|
||||
|
||||
## 8.20.0
|
||||
|
||||
## 8.19.2
|
||||
|
||||
## 8.19.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tailwind",
|
||||
"version": "8.19.2",
|
||||
"version": "8.20.0",
|
||||
"description": "Tailwind config based on @navikt/ds-tokens",
|
||||
"author": "Aksel | Nav designsystem team",
|
||||
"keywords": [
|
||||
@@ -28,7 +28,7 @@
|
||||
},
|
||||
"homepage": "https://aksel.nav.no/grunnleggende/kode/tailwind/",
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^8.19.2",
|
||||
"@navikt/ds-tokens": "^8.20.0",
|
||||
"@types/node": "^24.10.9",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tsx": "^4.23.15",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
# @navikt/ds-tokens
|
||||
|
||||
## 8.20.0
|
||||
|
||||
## 8.19.2
|
||||
|
||||
## 8.19.1
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-tokens",
|
||||
"version": "8.19.2",
|
||||
"version": "8.20.0",
|
||||
"description": "Design-tokens for Nav designsystem",
|
||||
"author": "Aksel | Nav designsystem team",
|
||||
"keywords": [
|
||||
|
||||
@@ -1,5 +1,21 @@
|
||||
# Changelog
|
||||
|
||||
## 8.20.0
|
||||
|
||||
### @navikt/ds-css
|
||||
|
||||
- Checkbox: Add `CheckboxCard` and `CheckboxCardGroup` components ([#5364](https://github.com/navikt/aksel/pull/5364))
|
||||
|
||||
* ToggleGroup: Add xsmall as size ([#5411](https://github.com/navikt/aksel/pull/5411))
|
||||
|
||||
### @navikt/ds-react
|
||||
|
||||
- Checkbox: Add `CheckboxCard` and `CheckboxCardGroup` components ([#5364](https://github.com/navikt/aksel/pull/5364))
|
||||
|
||||
* Lookup, Badge: Mark as deprecated when imported from @navikt/ds-react/PREVIEW ([#5401](https://github.com/navikt/aksel/pull/5401))
|
||||
|
||||
* ToggleGroup: Add xsmall as size ([#5411](https://github.com/navikt/aksel/pull/5411))
|
||||
|
||||
## 8.19.2
|
||||
|
||||
### @navikt/ds-css
|
||||
|
||||
@@ -11,11 +11,11 @@
|
||||
"sanity:update": "yarn workspace website update:sanity"
|
||||
},
|
||||
"dependencies": {
|
||||
"@navikt/aksel": "^8.19.2",
|
||||
"@navikt/aksel-icons": "^8.19.2",
|
||||
"@navikt/ds-css": "^8.19.2",
|
||||
"@navikt/ds-react": "^8.19.2",
|
||||
"@navikt/ds-tailwind": "^8.19.2",
|
||||
"@navikt/ds-tokens": "^8.19.2"
|
||||
"@navikt/aksel": "^8.20.0",
|
||||
"@navikt/aksel-icons": "^8.20.0",
|
||||
"@navikt/ds-css": "^8.20.0",
|
||||
"@navikt/ds-react": "^8.20.0",
|
||||
"@navikt/ds-tailwind": "^8.20.0",
|
||||
"@navikt/ds-tokens": "^8.20.0"
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ export const KodeEksempler = defineType({
|
||||
"Mindre padding i forhåndsvisningen, og koden er skjult som standard.",
|
||||
name: "compact",
|
||||
type: "boolean",
|
||||
initialValue: false,
|
||||
}),
|
||||
],
|
||||
preview: {
|
||||
|
||||
@@ -20,7 +20,7 @@ type RouteConfig<T extends RouteItemBase> = {
|
||||
heading: string;
|
||||
description: string;
|
||||
buildAttributes: (item: T) => Record<string, string>;
|
||||
getIntro?: (item: T) => any[] | undefined;
|
||||
getIntro?: (item: T) => string;
|
||||
};
|
||||
|
||||
function createRoute<T extends RouteItemBase>(config: RouteConfig<T>) {
|
||||
@@ -96,7 +96,7 @@ function buildItemMarkdown<T extends RouteItemBase>(
|
||||
return buildMarkdown(
|
||||
open,
|
||||
{ heading: item.heading ?? "" },
|
||||
portableMarkdown(config.getIntro?.(item)),
|
||||
config.getIntro?.(item) ?? null,
|
||||
portableMarkdown(item.content ?? undefined),
|
||||
MetadataSeksjonMarkdown(item.component_metadata),
|
||||
close,
|
||||
|
||||
@@ -4,7 +4,9 @@ import {
|
||||
KOMPONENT_BY_SLUG_MARKDOWN_QUERY,
|
||||
} from "@/app/_sanity/queries";
|
||||
import type { ALL_KOMPONENTS_MARKDOWN_QUERY_RESULT } from "@/app/_sanity/query-types";
|
||||
import { buildMarkdown } from "../helpers/build-markdown";
|
||||
import { createRoute } from "../helpers/create-route";
|
||||
import { portableMarkdown } from "../helpers/portable-markdown";
|
||||
|
||||
type KomponentItem = ALL_KOMPONENTS_MARKDOWN_QUERY_RESULT[number];
|
||||
|
||||
@@ -44,5 +46,27 @@ export default createRoute<KomponentItem>({
|
||||
packages: (item.kodepakker ?? []).join(", ") || "Ukjent",
|
||||
version: PackageJson?.version,
|
||||
}),
|
||||
getIntro: (item) => item.intro?.body,
|
||||
getIntro: (item) => {
|
||||
const useFor = [
|
||||
...(item.status?.internal ? ["Bruk på interne flater"] : []),
|
||||
...(item.intro?.brukes_til ?? []),
|
||||
];
|
||||
const avoidUseFor = item.intro?.brukes_ikke_til ?? [];
|
||||
|
||||
return buildMarkdown(
|
||||
portableMarkdown(item.intro?.body),
|
||||
useFor.length > 0
|
||||
? buildMarkdown(
|
||||
{ heading: "Egnet til:", level: 2 },
|
||||
useFor.map((text) => `- ${text}`).join("\n"),
|
||||
)
|
||||
: null,
|
||||
avoidUseFor.length > 0
|
||||
? buildMarkdown(
|
||||
{ heading: "Uegnet til:", level: 2 },
|
||||
avoidUseFor.map((text) => `- ${text}`).join("\n"),
|
||||
)
|
||||
: null,
|
||||
);
|
||||
},
|
||||
});
|
||||
+12
-11
@@ -19,7 +19,7 @@ type CaseT = {
|
||||
keywords: string[];
|
||||
priority: 0 | 1 | 2 | 3;
|
||||
sender: string;
|
||||
age: Date;
|
||||
created: Date;
|
||||
status:
|
||||
| "Mottatt"
|
||||
| "Under behandling"
|
||||
@@ -29,9 +29,8 @@ type CaseT = {
|
||||
deadline: Date;
|
||||
};
|
||||
|
||||
const now = Date.now();
|
||||
|
||||
function createDemoRows(count: number): CaseT[] {
|
||||
const now = Date.now();
|
||||
/* prettier-ignore */
|
||||
const names = ["Emma","Olivia","Nora","Sofie","Leah","Ella","Frida","Sofia","Ellinor","Astrid","Noah","Jakob","Lucas","Emil","Oskar","William","Elias","Isak","Oliver","Ludvig"];
|
||||
/* prettier-ignore */
|
||||
@@ -59,15 +58,16 @@ function createDemoRows(count: number): CaseT[] {
|
||||
keywords: Array.from(keywords),
|
||||
priority: ([0, 1, 2, 3] as const)[rng(4)],
|
||||
sender: names[rng(names.length)],
|
||||
age: new Date(now - index * daysInMs(1)),
|
||||
created: new Date(now - index * daysInMs(1)),
|
||||
status: statuses[rng(statuses.length)],
|
||||
deadline: new Date(now - daysInMs(8) + (index % 50) * daysInMs(1)),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/* prettier-ignore */
|
||||
function createDemoColumns({ withSorting = false }: { withSorting?: boolean; } = {}): DataGrid.Columns<CaseT> {
|
||||
function createDemoColumns({
|
||||
withSorting = false,
|
||||
}: { withSorting?: boolean } = {}): DataGrid.Columns<CaseT> {
|
||||
return [
|
||||
{
|
||||
id: "caseId",
|
||||
@@ -116,10 +116,10 @@ function createDemoColumns({ withSorting = false }: { withSorting?: boolean; } =
|
||||
isSortable: withSorting,
|
||||
},
|
||||
{
|
||||
id: "age",
|
||||
id: "created",
|
||||
header: "Saksalder",
|
||||
bodyCell: ({ age }) =>
|
||||
`${Math.floor((Date.now() - age.getTime()) / daysInMs(1))}d`,
|
||||
bodyCell: ({ created }) =>
|
||||
`${Math.floor((Date.now() - created.getTime()) / daysInMs(1))}d`,
|
||||
align: "right",
|
||||
width: { defaultValue: 110 },
|
||||
isSortable: withSorting,
|
||||
@@ -161,7 +161,8 @@ function createDemoColumns({ withSorting = false }: { withSorting?: boolean; } =
|
||||
align: "center",
|
||||
width: { autoResizeOnce: true },
|
||||
},
|
||||
];
|
||||
] satisfies (DataGrid.Columns<CaseT>[number] &
|
||||
({ isSortable?: never } | { isSortable: boolean; id: keyof CaseT }))[];
|
||||
}
|
||||
|
||||
const PriorityNames = {
|
||||
@@ -188,7 +189,7 @@ function PriorityTag({ priority }: { priority: CaseT["priority"] }) {
|
||||
}
|
||||
|
||||
function DeadlineCell({ deadline }: { deadline: CaseT["deadline"] }) {
|
||||
const isOverdue = deadline.getTime() < now;
|
||||
const isOverdue = deadline.getTime() < Date.now(); // eslint-disable-line react-hooks/purity
|
||||
|
||||
return (
|
||||
<HGrid
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { useState } from "react";
|
||||
import { CheckboxCard, CheckboxCardGroup } from "@navikt/ds-react/PREVIEW";
|
||||
import { withDsExample } from "@/web/examples/withDsExample";
|
||||
|
||||
const Example = () => {
|
||||
const [value, setValue] = useState(["hyre"]);
|
||||
|
||||
return (
|
||||
<CheckboxCardGroup
|
||||
legend="Hvilke inntekter har du fra fisket?"
|
||||
description="Velg alle som gjelder deg."
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
>
|
||||
<CheckboxCard
|
||||
value="lott"
|
||||
description="Du får betalt ut fra hva fartøyet fanger - ikke fast lønn"
|
||||
>
|
||||
Lott - andel av fangsten
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="hyre"
|
||||
description="Du får fast lønn fra arbeidsgiveren din, som en vanlig ansatt"
|
||||
>
|
||||
Hyre - fast lønn
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="provisjon"
|
||||
description="Du får tillegg basert på salg eller fangstmengde"
|
||||
>
|
||||
Provisjon
|
||||
</CheckboxCard>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default withDsExample(Example);
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args: ExampleArgsT = {
|
||||
index: 5,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { CheckboxCard, CheckboxCardGroup } from "@navikt/ds-react/PREVIEW";
|
||||
import { withDsExample } from "@/web/examples/withDsExample";
|
||||
|
||||
const Example = () => {
|
||||
const handleChange = (val: string[]) => console.info(val);
|
||||
|
||||
return (
|
||||
<CheckboxCardGroup
|
||||
legend="Hvilke inntekter har du fra fisket?"
|
||||
description="Velg alle som gjelder deg."
|
||||
onChange={handleChange}
|
||||
>
|
||||
<CheckboxCard
|
||||
value="lott"
|
||||
description="Du får betalt ut fra hva fartøyet fanger - ikke fast lønn"
|
||||
>
|
||||
Lott - andel av fangsten
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="hyre"
|
||||
description="Du får fast lønn fra arbeidsgiveren din, som en vanlig ansatt"
|
||||
>
|
||||
Hyre - fast lønn
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="provisjon"
|
||||
description="Du får tillegg basert på salg eller fangstmengde"
|
||||
>
|
||||
Provisjon
|
||||
</CheckboxCard>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default withDsExample(Example);
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args: ExampleArgsT = {
|
||||
index: 0,
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { CheckboxCard, CheckboxCardGroup } from "@navikt/ds-react/PREVIEW";
|
||||
import { withDsExample } from "@/web/examples/withDsExample";
|
||||
|
||||
const Example = () => {
|
||||
return (
|
||||
<CheckboxCardGroup
|
||||
legend="Hvilke inntekter har du fra fisket?"
|
||||
description="Velg alle som gjelder deg."
|
||||
defaultValue={["lott", "hyre"]}
|
||||
disabled
|
||||
>
|
||||
<CheckboxCard
|
||||
value="lott"
|
||||
description="Du får betalt ut fra hva fartøyet fanger - ikke fast lønn"
|
||||
>
|
||||
Lott - andel av fangsten
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="hyre"
|
||||
description="Du får fast lønn fra arbeidsgiveren din, som en vanlig ansatt"
|
||||
>
|
||||
Hyre - fast lønn
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="provisjon"
|
||||
description="Du får tillegg basert på salg eller fangstmengde"
|
||||
>
|
||||
Provisjon
|
||||
</CheckboxCard>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default withDsExample(Example);
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args: ExampleArgsT = {
|
||||
index: 99,
|
||||
desc: "Vi fraråder bruk av disabled state. Vurder om du trenger å vise feltet i det hele tatt, om du heller kan bruke `readOnly`, eller bare kan skrive det ut i ren tekst.",
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { useState } from "react";
|
||||
import { CheckboxCard, CheckboxCardGroup } from "@navikt/ds-react/PREVIEW";
|
||||
import { withDsExample } from "@/web/examples/withDsExample";
|
||||
|
||||
const Example = () => {
|
||||
const [value, setValue] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<CheckboxCardGroup
|
||||
legend="Hvilke inntekter har du fra fisket?"
|
||||
description="Velg alle som gjelder deg."
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
error={
|
||||
value.length === 0
|
||||
? "Du må velge minst én inntekt før du kan gå videre."
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
<CheckboxCard
|
||||
value="lott"
|
||||
description="Du får betalt ut fra hva fartøyet fanger - ikke fast lønn"
|
||||
>
|
||||
Lott - andel av fangsten
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="hyre"
|
||||
description="Du får fast lønn fra arbeidsgiveren din, som en vanlig ansatt"
|
||||
>
|
||||
Hyre - fast lønn
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="provisjon"
|
||||
description="Du får tillegg basert på salg eller fangstmengde"
|
||||
>
|
||||
Provisjon
|
||||
</CheckboxCard>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default withDsExample(Example);
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args: ExampleArgsT = {
|
||||
index: 4,
|
||||
};
|
||||
@@ -0,0 +1,40 @@
|
||||
import { CheckboxCard, CheckboxCardGroup } from "@navikt/ds-react/PREVIEW";
|
||||
import { withDsExample } from "@/web/examples/withDsExample";
|
||||
|
||||
const Example = () => {
|
||||
return (
|
||||
<CheckboxCardGroup legend="Hvilke inntekter har du fra fisket?" hideLegend>
|
||||
<CheckboxCard
|
||||
value="lott"
|
||||
description="Du får betalt ut fra hva fartøyet fanger - ikke fast lønn"
|
||||
>
|
||||
Lott - andel av fangsten
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="hyre"
|
||||
description="Du får fast lønn fra arbeidsgiveren din, som en vanlig ansatt"
|
||||
>
|
||||
Hyre - fast lønn
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="provisjon"
|
||||
description="Du får tillegg basert på salg eller fangstmengde"
|
||||
>
|
||||
Provisjon
|
||||
</CheckboxCard>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default withDsExample(Example);
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args: ExampleArgsT = {
|
||||
index: 3,
|
||||
desc: "Legend må være meningsfull selv om den skjules, siden den fortsatt leses av skjermlesere.",
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { HGrid } from "@navikt/ds-react";
|
||||
import { CheckboxCard, CheckboxCardGroup } from "@navikt/ds-react/PREVIEW";
|
||||
import { withDsExample } from "@/web/examples/withDsExample";
|
||||
|
||||
const Example = () => {
|
||||
return (
|
||||
<CheckboxCardGroup legend="Hvordan vil du bli varslet?">
|
||||
<HGrid columns={{ xs: 1, md: 2 }}>
|
||||
<CheckboxCard value="sms">SMS</CheckboxCard>
|
||||
<CheckboxCard value="epost">E-post</CheckboxCard>
|
||||
</HGrid>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default withDsExample(Example, { variant: "full" });
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args: ExampleArgsT = {
|
||||
index: 1,
|
||||
desc: "Bruk `HGrid` for å plassere kortene horisontalt. Egner seg best for få alternativer med korte tekster. Du trenger ikke legge til eget `gap`-prop for å få avstand mellom kortene.",
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
import { CheckboxCard, CheckboxCardGroup } from "@navikt/ds-react/PREVIEW";
|
||||
import { withDsExample } from "@/web/examples/withDsExample";
|
||||
|
||||
const Example = () => {
|
||||
return (
|
||||
<CheckboxCardGroup
|
||||
legend="Hvilke inntekter har du fra fisket?"
|
||||
description="Velg alle som gjelder deg."
|
||||
value={["lott", "hyre"]}
|
||||
readOnly
|
||||
>
|
||||
<CheckboxCard
|
||||
value="lott"
|
||||
description="Du får betalt ut fra hva fartøyet fanger - ikke fast lønn"
|
||||
>
|
||||
Lott - andel av fangsten
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="hyre"
|
||||
description="Du får fast lønn fra arbeidsgiveren din, som en vanlig ansatt"
|
||||
>
|
||||
Hyre - fast lønn
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="provisjon"
|
||||
description="Du får tillegg basert på salg eller fangstmengde"
|
||||
>
|
||||
Provisjon
|
||||
</CheckboxCard>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default withDsExample(Example);
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args: ExampleArgsT = {
|
||||
index: 98,
|
||||
desc: "Readonly-attributtet gjør at valgene ikke kan endres, men brukere vil fortsatt kunne markere og kopiere teksten. Til forskjell fra disabled vil brukere også kunne tabbe til det, og feltet vil inkluderes når skjemaet sendes inn.",
|
||||
};
|
||||
@@ -0,0 +1,43 @@
|
||||
import { CheckboxCard, CheckboxCardGroup } from "@navikt/ds-react/PREVIEW";
|
||||
import { withDsExample } from "@/web/examples/withDsExample";
|
||||
|
||||
const Example = () => {
|
||||
return (
|
||||
<CheckboxCardGroup
|
||||
legend="Hvilke inntekter har du fra fisket?"
|
||||
description="Velg alle som gjelder deg."
|
||||
size="small"
|
||||
>
|
||||
<CheckboxCard
|
||||
value="lott"
|
||||
description="Du får betalt ut fra hva fartøyet fanger - ikke fast lønn"
|
||||
>
|
||||
Lott - andel av fangsten
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="hyre"
|
||||
description="Du får fast lønn fra arbeidsgiveren din, som en vanlig ansatt"
|
||||
>
|
||||
Hyre - fast lønn
|
||||
</CheckboxCard>
|
||||
<CheckboxCard
|
||||
value="provisjon"
|
||||
description="Du får tillegg basert på salg eller fangstmengde"
|
||||
>
|
||||
Provisjon
|
||||
</CheckboxCard>
|
||||
</CheckboxCardGroup>
|
||||
);
|
||||
};
|
||||
|
||||
// EXAMPLES DO NOT INCLUDE CONTENT BELOW THIS LINE
|
||||
export default withDsExample(Example);
|
||||
|
||||
/* Storybook story */
|
||||
export const Demo = {
|
||||
render: Example,
|
||||
};
|
||||
|
||||
export const args: ExampleArgsT = {
|
||||
index: 2,
|
||||
};
|
||||
@@ -11,9 +11,9 @@
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@navikt/aksel-icons": "^8.19.2",
|
||||
"@navikt/ds-css": "^8.19.2",
|
||||
"@navikt/ds-react": "^8.19.2",
|
||||
"@navikt/aksel-icons": "^8.20.0",
|
||||
"@navikt/ds-css": "^8.20.0",
|
||||
"@navikt/ds-react": "^8.20.0",
|
||||
"fuse.js": "7.4.2",
|
||||
"react": "19.3.0",
|
||||
"react-dom": "19.3.0"
|
||||
|
||||
@@ -19,9 +19,9 @@
|
||||
"devDependencies": {
|
||||
"@modelcontextprotocol/inspector": "^2.9.0",
|
||||
"@modelcontextprotocol/sdk": "^1.31.0",
|
||||
"@navikt/aksel": "^8.19.2",
|
||||
"@navikt/aksel-icons": "^8.19.2",
|
||||
"@navikt/ds-tokens": "^8.19.2",
|
||||
"@navikt/aksel": "^8.20.0",
|
||||
"@navikt/aksel-icons": "^8.20.0",
|
||||
"@navikt/ds-tokens": "^8.20.0",
|
||||
"@sanity/client": "7.27.0",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/node": "^24.10.9",
|
||||
|
||||
@@ -20,9 +20,9 @@
|
||||
"typescript": "6.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"@navikt/aksel-icons": "^8.19.2",
|
||||
"@navikt/ds-css": "^8.19.2",
|
||||
"@navikt/ds-react": "^8.19.2",
|
||||
"@navikt/aksel-icons": "^8.20.0",
|
||||
"@navikt/ds-css": "^8.20.0",
|
||||
"@navikt/ds-react": "^8.20.0",
|
||||
"react": "19.3.0"
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -23,7 +23,10 @@ export default defineConfig({
|
||||
enabled: true,
|
||||
headless: true,
|
||||
provider: playwright(),
|
||||
instances: [{ browser: "firefox" }],
|
||||
/* Firefox can't start inside the cplt sandbox used by Copilot CLI */
|
||||
instances: [
|
||||
{ browser: process.env.COPILOT_CLI ? "chromium" : "firefox" },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -5222,7 +5222,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@navikt/aksel-icons@npm:^8.19.2, @navikt/aksel-icons@workspace:@navikt/aksel-icons":
|
||||
"@navikt/aksel-icons@npm:^8.20.0, @navikt/aksel-icons@workspace:@navikt/aksel-icons":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@navikt/aksel-icons@workspace:@navikt/aksel-icons"
|
||||
dependencies:
|
||||
@@ -5236,7 +5236,7 @@ __metadata:
|
||||
copyfiles: "npm:^2.4.1"
|
||||
fast-glob: "npm:3.3.3"
|
||||
hast-util-select: "npm:6.0.4"
|
||||
js-yaml: "npm:^5.4.3"
|
||||
js-yaml: "npm:^5.5.0"
|
||||
lodash: "npm:4.18.1"
|
||||
prettier: "npm:3.9.6"
|
||||
react: "npm:19.3.0"
|
||||
@@ -5253,10 +5253,10 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@navikt/aksel-stylelint@workspace:@navikt/aksel-stylelint"
|
||||
dependencies:
|
||||
"@navikt/ds-css": "npm:^8.19.2"
|
||||
"@navikt/ds-tokens": "npm:^8.19.2"
|
||||
"@navikt/ds-css": "npm:^8.20.0"
|
||||
"@navikt/ds-tokens": "npm:^8.20.0"
|
||||
concurrently: "npm:10.0.4"
|
||||
postcss-selector-parser: "npm:^7.1.6"
|
||||
postcss-selector-parser: "npm:^7.1.7"
|
||||
postcss-value-parser: "npm:^4.2.0"
|
||||
stylelint: "npm:^17.15.0"
|
||||
stylelint-test-rule-node: "npm:^1.2.0"
|
||||
@@ -5265,7 +5265,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@navikt/aksel@npm:^8.19.2, @navikt/aksel@workspace:@navikt/aksel":
|
||||
"@navikt/aksel@npm:^8.20.0, @navikt/aksel@workspace:@navikt/aksel":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@navikt/aksel@workspace:@navikt/aksel"
|
||||
dependencies:
|
||||
@@ -5298,11 +5298,11 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@navikt/ds-css@npm:^8.19.2, @navikt/ds-css@workspace:@navikt/core/css":
|
||||
"@navikt/ds-css@npm:^8.20.0, @navikt/ds-css@workspace:@navikt/core/css":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@navikt/ds-css@workspace:@navikt/core/css"
|
||||
dependencies:
|
||||
"@navikt/ds-tokens": "npm:^8.19.2"
|
||||
"@navikt/ds-tokens": "npm:^8.20.0"
|
||||
browserslist: "npm:^4.28.7"
|
||||
esbuild: "npm:^0.28.2"
|
||||
fast-glob: "npm:3.3.3"
|
||||
@@ -5311,14 +5311,14 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@navikt/ds-react@npm:^8.19.2, @navikt/ds-react@workspace:@navikt/core/react":
|
||||
"@navikt/ds-react@npm:^8.20.0, @navikt/ds-react@workspace:@navikt/core/react":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@navikt/ds-react@workspace:@navikt/core/react"
|
||||
dependencies:
|
||||
"@floating-ui/react": "npm:0.27.20"
|
||||
"@floating-ui/react-dom": "npm:^2.1.9"
|
||||
"@navikt/aksel-icons": "npm:^8.19.2"
|
||||
"@navikt/ds-tokens": "npm:^8.19.2"
|
||||
"@navikt/aksel-icons": "npm:^8.20.0"
|
||||
"@navikt/ds-tokens": "npm:^8.20.0"
|
||||
"@testing-library/dom": "npm:10.4.1"
|
||||
"@testing-library/jest-dom": "npm:^6.6.3"
|
||||
"@testing-library/react": "npm:^16.3.3"
|
||||
@@ -5352,11 +5352,11 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@navikt/ds-tailwind@npm:^8.19.2, @navikt/ds-tailwind@workspace:@navikt/core/tailwind":
|
||||
"@navikt/ds-tailwind@npm:^8.20.0, @navikt/ds-tailwind@workspace:@navikt/core/tailwind":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@navikt/ds-tailwind@workspace:@navikt/core/tailwind"
|
||||
dependencies:
|
||||
"@navikt/ds-tokens": "npm:^8.19.2"
|
||||
"@navikt/ds-tokens": "npm:^8.20.0"
|
||||
"@types/node": "npm:^24.10.9"
|
||||
tailwindcss: "npm:^3.3.3"
|
||||
tsx: "npm:^4.23.15"
|
||||
@@ -5365,7 +5365,7 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@navikt/ds-tokens@npm:^8.19.2, @navikt/ds-tokens@workspace:@navikt/core/tokens":
|
||||
"@navikt/ds-tokens@npm:^8.20.0, @navikt/ds-tokens@workspace:@navikt/core/tokens":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@navikt/ds-tokens@workspace:@navikt/core/tokens"
|
||||
dependencies:
|
||||
@@ -10973,9 +10973,9 @@ __metadata:
|
||||
resolution: "aksel-icons-figma-plugin@workspace:apps/figma-icon-plugin"
|
||||
dependencies:
|
||||
"@figma/plugin-typings": "npm:^1.131.0"
|
||||
"@navikt/aksel-icons": "npm:^8.19.2"
|
||||
"@navikt/ds-css": "npm:^8.19.2"
|
||||
"@navikt/ds-react": "npm:^8.19.2"
|
||||
"@navikt/aksel-icons": "npm:^8.20.0"
|
||||
"@navikt/ds-css": "npm:^8.20.0"
|
||||
"@navikt/ds-react": "npm:^8.20.0"
|
||||
"@rollup/pluginutils": "npm:^5.4.0"
|
||||
"@svgr/core": "npm:^8.1.0"
|
||||
"@types/node": "npm:^24.10.9"
|
||||
@@ -10997,9 +10997,9 @@ __metadata:
|
||||
dependencies:
|
||||
"@modelcontextprotocol/inspector": "npm:^2.9.0"
|
||||
"@modelcontextprotocol/sdk": "npm:^1.31.0"
|
||||
"@navikt/aksel": "npm:^8.19.2"
|
||||
"@navikt/aksel-icons": "npm:^8.19.2"
|
||||
"@navikt/ds-tokens": "npm:^8.19.2"
|
||||
"@navikt/aksel": "npm:^8.20.0"
|
||||
"@navikt/aksel-icons": "npm:^8.20.0"
|
||||
"@navikt/ds-tokens": "npm:^8.20.0"
|
||||
"@sanity/client": "npm:7.27.0"
|
||||
"@types/express": "npm:^5.0.3"
|
||||
"@types/node": "npm:^24.10.9"
|
||||
@@ -11021,9 +11021,9 @@ __metadata:
|
||||
"@babel/preset-env": "npm:^7.29.0"
|
||||
"@babel/preset-react": "npm:^7.28.5"
|
||||
"@babel/preset-typescript": "npm:^7.28.5"
|
||||
"@navikt/aksel-icons": "npm:^8.19.2"
|
||||
"@navikt/ds-css": "npm:^8.19.2"
|
||||
"@navikt/ds-react": "npm:^8.19.2"
|
||||
"@navikt/aksel-icons": "npm:^8.20.0"
|
||||
"@navikt/ds-css": "npm:^8.20.0"
|
||||
"@navikt/ds-react": "npm:^8.20.0"
|
||||
"@types/node": "npm:^24.10.9"
|
||||
babel-loader: "npm:^10.1.1"
|
||||
playroom: "npm:^1.4.0"
|
||||
@@ -11085,12 +11085,12 @@ __metadata:
|
||||
version: 0.0.0-use.local
|
||||
resolution: "aksel.nav.no@workspace:aksel.nav.no"
|
||||
dependencies:
|
||||
"@navikt/aksel": "npm:^8.19.2"
|
||||
"@navikt/aksel-icons": "npm:^8.19.2"
|
||||
"@navikt/ds-css": "npm:^8.19.2"
|
||||
"@navikt/ds-react": "npm:^8.19.2"
|
||||
"@navikt/ds-tailwind": "npm:^8.19.2"
|
||||
"@navikt/ds-tokens": "npm:^8.19.2"
|
||||
"@navikt/aksel": "npm:^8.20.0"
|
||||
"@navikt/aksel-icons": "npm:^8.20.0"
|
||||
"@navikt/ds-css": "npm:^8.20.0"
|
||||
"@navikt/ds-react": "npm:^8.20.0"
|
||||
"@navikt/ds-tailwind": "npm:^8.20.0"
|
||||
"@navikt/ds-tokens": "npm:^8.20.0"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
@@ -11291,6 +11291,13 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"argparse@npm:^3.0.3":
|
||||
version: 3.0.3
|
||||
resolution: "argparse@npm:3.0.3"
|
||||
checksum: 10/6834e247fe8a8d21a0061dc2b7134ae79625d9ab795eca043d8ba9434be3bc1e7d049c9a4aa01bd3d6bfc145b29addfb290d17b8b3f7b11710cc52ab13629f31
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"aria-query@npm:5.3.0":
|
||||
version: 5.3.0
|
||||
resolution: "aria-query@npm:5.3.0"
|
||||
@@ -17807,14 +17814,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:^5.4.3":
|
||||
version: 5.4.3
|
||||
resolution: "js-yaml@npm:5.4.3"
|
||||
"js-yaml@npm:^5.5.0":
|
||||
version: 5.5.0
|
||||
resolution: "js-yaml@npm:5.5.0"
|
||||
dependencies:
|
||||
argparse: "npm:^2.0.1"
|
||||
argparse: "npm:^3.0.3"
|
||||
bin:
|
||||
js-yaml: bin/js-yaml.mjs
|
||||
checksum: 10/2409d1238efddc29ab62ddb26d70bc7f2e8b58b84eb8d41d09d50fab86f8670aa2fec5dc6c99ef71939acab5d7c0b3526f96bdd0055042aa0136cca749b20afc
|
||||
checksum: 10/c01359a4fe69a1c032e9e57d7387a1cbdfbbd45bc69924885a3c9a9a020e066627dc125f7b46f7b929186b5a65471ae5fa478d3f773e35f7ccdf57d32fcf369f
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -21534,7 +21541,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-selector-parser@npm:^7.0.0, postcss-selector-parser@npm:^7.1.5, postcss-selector-parser@npm:^7.1.6":
|
||||
"postcss-selector-parser@npm:^7.0.0, postcss-selector-parser@npm:^7.1.5":
|
||||
version: 7.1.6
|
||||
resolution: "postcss-selector-parser@npm:7.1.6"
|
||||
dependencies:
|
||||
@@ -21544,6 +21551,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-selector-parser@npm:^7.1.7":
|
||||
version: 7.1.7
|
||||
resolution: "postcss-selector-parser@npm:7.1.7"
|
||||
dependencies:
|
||||
cssesc: "npm:^3.0.0"
|
||||
util-deprecate: "npm:^1.0.2"
|
||||
checksum: 10/9413749ad38d8e22e4a5e47287e5501493238ab73eea276381476933332604df49468451e99a3d6614463fd3ee96655a80821a93949cfe425ecd6acf210535b0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"postcss-value-parser@npm:^4.0.0, postcss-value-parser@npm:^4.0.2, postcss-value-parser@npm:^4.1.0, postcss-value-parser@npm:^4.2.0":
|
||||
version: 4.2.0
|
||||
resolution: "postcss-value-parser@npm:4.2.0"
|
||||
|
||||
Reference in new issue
Block a user