mirror of
https://github.com/navikt/aksel.git
synced 2026-11-03 19:34:08 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
068fa89ba8 |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/aksel": patch
|
||||
---
|
||||
|
||||
CLI: Update clipboardy to v5.3.2
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/aksel": patch
|
||||
---
|
||||
|
||||
CLI: Update figlet to 1.11.3
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/ds-react": patch
|
||||
---
|
||||
|
||||
DatePicker: Typing a date with an unreasonable year (more than ~150 years from today, or outside `fromDate`/`toDate`) no longer navigates the calendar to that year.
|
||||
@@ -10,8 +10,8 @@
|
||||
|
||||
## Behavior
|
||||
|
||||
- Only respond in same language as the input. Prefer English always.
|
||||
- Use caveman-skill unless asked not to.
|
||||
- Only respond in same language as the input. Prefer english always.
|
||||
- Use caveman-skill unless asked not to
|
||||
- No apologies, no "I think", no change summaries.
|
||||
- Answer-first. 0-2 sentences max.
|
||||
- No thinking aloud; do next edit. Explain only when asked or non-obvious.
|
||||
@@ -22,7 +22,7 @@
|
||||
## Repo summary
|
||||
|
||||
- Nav's design system. Yarn 4 workspaces monorepo: published packages under `@navikt/` + docs site `aksel.nav.no/` + `apps/` + `tooling/`.
|
||||
- Stack: TypeScript 6, React 19 (dev), Next.js 16, Storybook 10, Vitest 4, ESLint 9, Stylelint 17, Oxlint (JS/TS lint), Prettier 3 (formatting). Node 22+, Yarn 4.18.0.
|
||||
- Stack: TypeScript 6, React 19 (dev), Next.js 16, Storybook 10, Vitest 4, ESLint 9, Stylelint 17, Biome 2 (lint only), Prettier 3 (formatting). Node 22+, Yarn 4.18.0.
|
||||
- Published `@navikt/*` packages must stay React 17 compatible (peer `react ^17 || ^18 || ^19`).
|
||||
|
||||
## Start and validate
|
||||
@@ -45,12 +45,11 @@
|
||||
|
||||
## Commands
|
||||
|
||||
- `corepack yarn eslint --max-warnings=0 --no-warn-ignored <files...>` / `corepack yarn stylelint <files...>` / `corepack yarn oxlint --deny-warnings <files...>`
|
||||
- `corepack yarn eslint --max-warnings=0 --no-warn-ignored <files...>` / `corepack yarn stylelint <files...>` / `corepack yarn biome lint --no-errors-on-unmatched <files...>`
|
||||
- `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; 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.
|
||||
- `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)
|
||||
|
||||
## Where to edit
|
||||
|
||||
@@ -64,7 +63,7 @@ Path-specific rules load automatically from `.github/instructions/` (see each `a
|
||||
- `@navikt/aksel/src/` - CLI and codemods
|
||||
- `aksel.nav.no/website/` - Next.js docs site · `aksel.nav.no/sanity-studio/` - CMS schema
|
||||
- `apps/playroom/`, `apps/mcp-server/`, `apps/figma-icon-plugin/` - apps · `tooling/` - ESLint plugin, scripts, changelog, analyzer
|
||||
- `.changeset/` - release notes · Root config: `eslint.config.js`, `stylelint.config.mjs`, `.oxlintrc.json`, `vitest.config.ts`, `.storybook/`
|
||||
- `.changeset/` - release notes · Root config: `eslint.config.js`, `stylelint.config.mjs`, `biome.json`, `vitest.config.ts`, `.storybook/`
|
||||
|
||||
## Skills and agents
|
||||
|
||||
@@ -89,5 +88,3 @@ Path-specific rules load automatically from `.github/instructions/` (see each `a
|
||||
- `node:` imports for Node builtins. Never import from `esm/`/`cjs/` output.
|
||||
- Preserve JSDoc on public props/components when changing public APIs.
|
||||
- Reuse helpers: `cl`, `composeEventHandlers`, `useId`, `omit`, `useClientLayoutEffect`, `useEventListener`.
|
||||
- No tests for trivial logic. Only tests for complex logic that has a higher risk of breaking.
|
||||
- Suppress Oxlint rules with `oxlint-disable-next-line <rule>` / `oxlint-disable <rule>`, never `eslint-disable` (breaks ESLint).
|
||||
@@ -15,12 +15,6 @@ updates:
|
||||
registries:
|
||||
- npm-github
|
||||
groups:
|
||||
next:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "@next/eslint-plugin-next"
|
||||
- "@next/bundle-analyzer"
|
||||
- "next"
|
||||
sanity:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
@@ -49,12 +43,6 @@ updates:
|
||||
- "vitest"
|
||||
- "@vitest/*"
|
||||
# Security updates ignore version-update groups, so mirror them to keep lockstep packages together.
|
||||
next-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "@next/eslint-plugin-next"
|
||||
- "@next/bundle-analyzer"
|
||||
- "next"
|
||||
sanity-security:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
---
|
||||
description: "JS/TS conventions not enforced by ESLint/Oxlint: loops, export style and placement."
|
||||
description: "JS/TS conventions not enforced by ESLint/Biome: loops, export style and placement."
|
||||
applyTo: "**/*.ts,**/*.tsx,**/*.js,**/*.mts,**/*.mjs"
|
||||
---
|
||||
|
||||
# JS/TS conventions
|
||||
|
||||
Formatting and import order are handled by Prettier; linting by ESLint and Oxlint. Don't hand-tune formatting or import order.
|
||||
|
||||
Suppress Oxlint rules with `// oxlint-disable-next-line <rule>` (file-level: `/* oxlint-disable <rule> */`). Never use `eslint-disable` for Oxlint rules; ESLint reports unknown rules and unused directives.
|
||||
Formatting, import order and `import type` are handled by Prettier and Biome. Don't hand-tune them.
|
||||
|
||||
- Use `for...of` instead of `Array.forEach`. It supports `break`/`continue`/`await` and reads top-down.
|
||||
- Use named exports only. Keep type exports separate from value exports (`export { X }` / `export type { XProps }`).
|
||||
|
||||
@@ -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 (Chromium under Copilot CLI, see `vitest.config.ts`). Yarn has `enableScripts: false`, so if the browser is missing, report it instead of installing globally.
|
||||
- Browser tests need Playwright Firefox. Yarn has `enableScripts: false`, so if the browser is missing, report it instead of installing globally.
|
||||
@@ -59,7 +59,7 @@ Group only matching files:
|
||||
```sh
|
||||
corepack yarn eslint --max-warnings=0 --no-warn-ignored <changed .ts/.tsx/.js/.jsx files>
|
||||
corepack yarn stylelint <changed .css files>
|
||||
corepack yarn oxlint --deny-warnings <changed .ts/.tsx/.js/.jsx files>
|
||||
corepack yarn biome lint --no-errors-on-unmatched <changed supported source/config files>
|
||||
```
|
||||
|
||||
Do not pass unsupported file types to make a command appear comprehensive. Documentation-only changes need no code validation unless a documentation check exists.
|
||||
|
||||
@@ -26,7 +26,7 @@ argument-hint: "PR number, branch or files to review"
|
||||
- 🟡 **Suggestion**: should fix. Missing test, docs or changeset, maintainability risk, over-editing.
|
||||
- 💭 **Nit**: optional. Leave it out when in doubt.
|
||||
|
||||
Skip anything ESLint, Stylelint, Oxlint, Prettier or TypeScript already catches. No praise-only or speculative comments. Every finding says **why** it matters and what to do.
|
||||
Skip anything ESLint, Stylelint, Biome, Prettier or TypeScript already catches. No praise-only or speculative comments. Every finding says **why** it matters and what to do.
|
||||
|
||||
## Checklist
|
||||
|
||||
@@ -69,7 +69,7 @@ Skip anything ESLint, Stylelint, Oxlint, Prettier or TypeScript already catches.
|
||||
|
||||
### Tests and docs (🟡)
|
||||
|
||||
- Complex logic with a higher risk of breaking has a story `play` test or Vitest test proving the user-facing contract. Queries use role and accessible name.
|
||||
- A behavior change has a story `play` test or Vitest test proving the user-facing contract. Queries use role and accessible name.
|
||||
- Tests are deterministic: no dependence on timezone, current time or randomness.
|
||||
- Edge cases that are easy to miss: controlled vs. uncontrolled, `ref`/`className`/`...rest` forwarding, `as` prop, empty and very long content.
|
||||
- Website examples (`aksel.nav.no/website/pages/eksempler/`) are updated when usage changes.
|
||||
|
||||
@@ -95,7 +95,7 @@ Move stories to component root dir. If stories import from internal paths, updat
|
||||
|
||||
### 5. Update `index.ts`
|
||||
|
||||
All exports are **named** — no `default` exports anywhere. For compound components, the root file is the single export source: it assembles the compound component, imports every publicly exported sub-component and its prop types, and exports all public values and types together. The component `index.ts` only re-exports these names from `./root/<Component>Root` — never directly from sub-component files or shared type files.
|
||||
All exports are **named** — no `default` exports anywhere. The component `index.ts` re-exports the compound component, its sub-components, and their types from the root file:
|
||||
|
||||
```ts
|
||||
"use client";
|
||||
@@ -111,18 +111,13 @@ export type {
|
||||
} from "./root/<Component>Root";
|
||||
```
|
||||
|
||||
Group values first, then types. If types live in `<Component>.types.ts`, import them into the root file and include them in its type export block. The `index.ts` still re-exports them from the root file.
|
||||
Group values first, then types. If types live in `<Component>.types.ts`, re-export them from there.
|
||||
|
||||
#### Compound root file export pattern
|
||||
|
||||
In `<Component>Root.tsx`, assemble the compound component with `Object.assign` — **do not** use an `as <Component>Component` cast or a dedicated `<Component>Component` interface:
|
||||
|
||||
```tsx
|
||||
import {
|
||||
<Component>Trigger,
|
||||
type <Component>TriggerProps,
|
||||
} from "../trigger/<Component>Trigger";
|
||||
|
||||
const <Component>Root = forwardRef<HTMLDivElement, <Component>Props>((props, ref) => {
|
||||
// ...
|
||||
});
|
||||
@@ -143,8 +138,6 @@ export { <Component>, <Component>Trigger };
|
||||
export type { <Component>Props, <Component>TriggerProps };
|
||||
```
|
||||
|
||||
Follow `accordion/root/AccordionRoot.tsx`: export the imported sub-component values and prop types alongside the compound component at the bottom of the root file. Do not add internal-only sub-components to the public export chain. Sub-components must not import from the root file or component `index.ts`; use shared type/context/helper files to avoid circular dependencies.
|
||||
|
||||
Why `Object.assign` instead of `as`: the cast types the const as an interface that has no runtime `valueDeclaration`, so `react-docgen-typescript` (used by `yarn docgen:meta`) fails to extract props and can't document the component when it is exported via a grouped `export { }` statement. `Object.assign` lets TypeScript infer the intersection type from the value, so metadata generation works with named, bottom-of-file exports.
|
||||
|
||||
Sub-component files follow the same rule — named value export plus named type export at the bottom, no inline `export interface` and no default:
|
||||
@@ -180,8 +173,6 @@ Concretely: grep for `export` lines in both versions and diff them. All componen
|
||||
|
||||
**Package root.** Verify `@navikt/core/react/src/index.ts` still re-exports the component. No changes needed there unless the component's `index.ts` path changed (it shouldn't).
|
||||
|
||||
**Root-only re-exports.** For compound components, verify every value and type export in the component `index.ts` comes from `./root/<Component>Root`, and every forwarded name is exported by that root file.
|
||||
|
||||
**No case-duplicate tracked paths.** This is the most common restructure bug. On case-insensitive filesystems (macOS, Windows) git can end up tracking **two** entries that differ only in casing (e.g. `accordion.stories.tsx` **and** `Accordion.stories.tsx`) while the working tree shows only one file. The stale entry then breaks tests and checkout on other machines. `ls` / file search will NOT reveal it — you must ask git. Run:
|
||||
|
||||
```sh
|
||||
@@ -290,7 +281,6 @@ dialog/
|
||||
|
||||
- **No breaking changes.** Public export names and prop shapes must be identical after restructuring.
|
||||
- **Named exports only.** No `default` exports; group named exports at the bottom of each file (values first, then types).
|
||||
- **Compound exports flow through the root.** Component `index.ts` re-exports public values and types only from `root/<Component>Root.tsx`, not directly from sub-component or shared type files.
|
||||
- **Compound roots use `Object.assign`.** No `as <Component>Component` cast or `<Component>Component` interface — it breaks `yarn docgen:meta` prop extraction.
|
||||
- Component rules (React 17 compatibility, `"use client"`, `forwardRef`/`className`/`...rest`/`as`, tokens) live in `.github/instructions/ds-react-component.instructions.md`. They still apply.
|
||||
- Keep JSDoc on public props and components. The component-level JSDoc block sits directly above the `Object.assign` call; sub-component `@see` tags go on the keys inside the `Object.assign` object literal.
|
||||
@@ -299,6 +289,6 @@ dialog/
|
||||
|
||||
- Canonical example: `@navikt/core/react/src/dialog/`
|
||||
- Package public API: `@navikt/core/react/src/index.ts`
|
||||
- Component export pattern: `@navikt/core/react/src/accordion/root/AccordionRoot.tsx` and `@navikt/core/react/src/accordion/index.ts`
|
||||
- Component `index.ts` pattern: `@navikt/core/react/src/dialog/index.ts`
|
||||
- Context pattern: `@navikt/core/react/src/dialog/root/DialogRoot.context.ts`
|
||||
- Meta: `@navikt/core/react/src/accordion/Accordion.meta.ts`
|
||||
+3
-10
@@ -1,13 +1,6 @@
|
||||
CHANGED=`git diff $1 $2 --stat -- yarn.lock | wc -l`
|
||||
if [ $CHANGED -gt 0 ];
|
||||
then
|
||||
printf '\n\033[1;33m'
|
||||
printf ' ╔══════════════════════════════════════════╗\n'
|
||||
printf ' ║ ║\n'
|
||||
printf ' ║ !! yarn.lock has changed !! ║\n'
|
||||
printf ' ║ ║\n'
|
||||
printf ' ║ Run: yarn install ║\n'
|
||||
printf ' ║ ║\n'
|
||||
printf ' ╚══════════════════════════════════════════╝\n'
|
||||
printf '\033[0m\n'
|
||||
fi
|
||||
echo
|
||||
echo "*** 🟡 yarn.lock has changed 🟡 ***"
|
||||
fi
|
||||
+3
-10
@@ -1,13 +1,6 @@
|
||||
CHANGED=`git diff HEAD@{1} --stat -- yarn.lock | wc -l`
|
||||
if [ $CHANGED -gt 0 ];
|
||||
then
|
||||
printf '\n\033[1;33m'
|
||||
printf ' ╔══════════════════════════════════════════╗\n'
|
||||
printf ' ║ ║\n'
|
||||
printf ' ║ !! yarn.lock has changed !! ║\n'
|
||||
printf ' ║ ║\n'
|
||||
printf ' ║ Run: yarn install ║\n'
|
||||
printf ' ║ ║\n'
|
||||
printf ' ╚══════════════════════════════════════════╝\n'
|
||||
printf '\033[0m\n'
|
||||
fi
|
||||
echo
|
||||
echo "*** 🟡 yarn.lock has changed 🟡 ***"
|
||||
fi
|
||||
+1
-2
@@ -1,2 +1 @@
|
||||
# Runs the local lint-staged checks before committing
|
||||
./node_modules/.bin/lint-staged
|
||||
yarn lint-staged
|
||||
-334
@@ -1,334 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": [
|
||||
"eslint",
|
||||
"typescript",
|
||||
"unicorn",
|
||||
"oxc",
|
||||
"react",
|
||||
"jsx-a11y",
|
||||
"import",
|
||||
"vitest"
|
||||
],
|
||||
"categories": {
|
||||
"correctness": "error"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/lib",
|
||||
"**/public",
|
||||
"**/esm",
|
||||
"**/cjs",
|
||||
"**/dist",
|
||||
"**/.next",
|
||||
"**/.build",
|
||||
"**/coverage",
|
||||
"**/storybook-static",
|
||||
"**/test-results",
|
||||
"**/playwright-report",
|
||||
"**/.sanity/runtime",
|
||||
"**/next-env.d.ts",
|
||||
"**/codemod/**/*.js",
|
||||
"**/transforms/**/*.input.*",
|
||||
"**/transforms/**/*.output.*",
|
||||
"@navikt/aksel/**/*.input.*",
|
||||
"@navikt/aksel/**/*.output.*",
|
||||
"**/modal/helpers/dialog-polyfill.ts",
|
||||
"**/query-types.ts",
|
||||
"**/component-names.generated.ts",
|
||||
"**/tokens/**/plugin.js",
|
||||
"@navikt/aksel-icons/src",
|
||||
"@navikt/aksel/src/version.ts",
|
||||
"@navikt/core/tailwind/src/tokens.ts",
|
||||
"@navikt/core/tailwind/tailwind.config.js",
|
||||
"@navikt/core/tailwind/tailwind.darkside.config.js",
|
||||
"@navikt/core/tokens/token_docs.js",
|
||||
"apps/playroom/playroom.config.js",
|
||||
"apps/playroom/src/auto-generated-imports.ts",
|
||||
"apps/figma-icon-plugin/src/ui/vite-env.d.ts",
|
||||
"tooling/analyzer/temp",
|
||||
".yarn"
|
||||
],
|
||||
"rules": {
|
||||
"jsx-a11y/alt-text": "error",
|
||||
"jsx-a11y/anchor-ambiguous-text": "error",
|
||||
"jsx-a11y/anchor-has-content": "error",
|
||||
"jsx-a11y/anchor-is-valid": "error",
|
||||
"jsx-a11y/aria-activedescendant-has-tabindex": "error",
|
||||
"jsx-a11y/aria-props": "error",
|
||||
"jsx-a11y/aria-proptypes": "error",
|
||||
"jsx-a11y/aria-role": "error",
|
||||
"jsx-a11y/aria-unsupported-elements": "error",
|
||||
"jsx-a11y/autocomplete-valid": "error",
|
||||
"jsx-a11y/click-events-have-key-events": "error",
|
||||
"jsx-a11y/heading-has-content": "error",
|
||||
"jsx-a11y/html-has-lang": "error",
|
||||
"jsx-a11y/iframe-has-title": "error",
|
||||
"jsx-a11y/img-redundant-alt": "error",
|
||||
"jsx-a11y/interactive-supports-focus": "error",
|
||||
"jsx-a11y/label-has-associated-control": "error",
|
||||
"jsx-a11y/lang": "error",
|
||||
"jsx-a11y/media-has-caption": "error",
|
||||
"jsx-a11y/mouse-events-have-key-events": "error",
|
||||
"jsx-a11y/no-access-key": "error",
|
||||
"jsx-a11y/no-aria-hidden-on-focusable": "error",
|
||||
"jsx-a11y/no-autofocus": "error",
|
||||
"jsx-a11y/no-distracting-elements": "error",
|
||||
"jsx-a11y/no-interactive-element-to-noninteractive-role": "error",
|
||||
"jsx-a11y/no-noninteractive-element-to-interactive-role": "error",
|
||||
"jsx-a11y/no-noninteractive-tabindex": "error",
|
||||
"jsx-a11y/no-redundant-roles": "error",
|
||||
"jsx-a11y/no-static-element-interactions": "error",
|
||||
"jsx-a11y/prefer-tag-over-role": "off",
|
||||
"jsx-a11y/role-has-required-aria-props": "error",
|
||||
"jsx-a11y/role-supports-aria-props": "error",
|
||||
"jsx-a11y/scope": "error",
|
||||
"jsx-a11y/tabindex-no-positive": "error",
|
||||
"react/button-has-type": "error",
|
||||
"eslint/no-extra-label": "warn",
|
||||
"eslint/no-lone-blocks": "warn",
|
||||
"eslint/no-sequences": "warn",
|
||||
"eslint/no-unneeded-ternary": "warn",
|
||||
"eslint/no-useless-computed-key": "warn",
|
||||
"eslint/no-useless-constructor": "warn",
|
||||
"eslint/no-useless-rename": "warn",
|
||||
"eslint/prefer-numeric-literals": "warn",
|
||||
"eslint/prefer-regex-literals": "warn",
|
||||
"eslint/prefer-rest-params": "warn",
|
||||
"react/jsx-no-useless-fragment": [
|
||||
"warn",
|
||||
{
|
||||
"allowExpressions": true
|
||||
}
|
||||
],
|
||||
"typescript/no-useless-empty-export": "warn",
|
||||
"unicorn/no-static-only-class": "warn",
|
||||
"unicorn/no-useless-switch-case": "warn",
|
||||
"unicorn/prefer-array-flat": "warn",
|
||||
"unicorn/prefer-array-flat-map": "warn",
|
||||
"unicorn/prefer-array-index-of": "warn",
|
||||
"unicorn/prefer-date-now": "warn",
|
||||
"eslint/no-constructor-return": "error",
|
||||
"eslint/no-inner-declarations": [
|
||||
"error",
|
||||
"both",
|
||||
{
|
||||
"blockScopedFunctions": "allow"
|
||||
}
|
||||
],
|
||||
"eslint/no-use-before-define": [
|
||||
"error",
|
||||
{
|
||||
"functions": false,
|
||||
"classes": false,
|
||||
"variables": false,
|
||||
"enums": false,
|
||||
"typedefs": false,
|
||||
"ignoreTypeReferences": true,
|
||||
"allowNamedExports": true
|
||||
}
|
||||
],
|
||||
"eslint/radix": "warn",
|
||||
"oxc/bad-min-max-func": "error",
|
||||
"react/void-dom-elements-no-children": "error",
|
||||
"import/namespace": "warn",
|
||||
"oxc/no-accumulating-spread": "warn",
|
||||
"eslint/no-eval": "error",
|
||||
"eslint/no-script-url": "error",
|
||||
"react/jsx-no-script-url": "error",
|
||||
"react/no-danger": "error",
|
||||
"eslint/prefer-const": "warn",
|
||||
"eslint/prefer-exponentiation-operator": "warn",
|
||||
"eslint/prefer-template": "warn",
|
||||
"typescript/consistent-type-imports": [
|
||||
"warn",
|
||||
{
|
||||
"disallowTypeAnnotations": false
|
||||
}
|
||||
],
|
||||
"typescript/no-non-null-assertion": "warn",
|
||||
"typescript/prefer-function-type": "warn",
|
||||
"typescript/prefer-literal-enum-member": "warn",
|
||||
"unicorn/prefer-node-protocol": "warn",
|
||||
"eslint/array-callback-return": [
|
||||
"error",
|
||||
{
|
||||
"checkForEach": true,
|
||||
"allowVoid": true
|
||||
}
|
||||
],
|
||||
"eslint/default-case-last": "warn",
|
||||
"eslint/eqeqeq": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"null": "ignore"
|
||||
}
|
||||
],
|
||||
"eslint/no-label-var": "error",
|
||||
"eslint/no-labels": [
|
||||
"warn",
|
||||
{
|
||||
"allowLoop": true
|
||||
}
|
||||
],
|
||||
"eslint/no-proto": "warn",
|
||||
"eslint/no-self-compare": "error",
|
||||
"eslint/no-template-curly-in-string": "warn",
|
||||
"oxc/approx-constant": "warn",
|
||||
"oxc/misrefactored-assign-op": "error",
|
||||
"oxc/no-const-enum": "warn",
|
||||
"react/no-array-index-key": "error",
|
||||
"typescript/adjacent-overload-signatures": "warn",
|
||||
"typescript/no-invalid-void-type": "warn",
|
||||
"unicorn/no-document-cookie": "warn",
|
||||
"unicorn/no-instanceof-array": "warn",
|
||||
"unicorn/no-thenable": "error",
|
||||
"unicorn/prefer-number-properties": "off",
|
||||
"eslint/no-console": [
|
||||
"warn",
|
||||
{
|
||||
"allow": [
|
||||
"info",
|
||||
"warn",
|
||||
"error",
|
||||
"group",
|
||||
"groupEnd",
|
||||
"table",
|
||||
"assert",
|
||||
"countReset",
|
||||
"count",
|
||||
"dir",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"timeStamp"
|
||||
]
|
||||
}
|
||||
],
|
||||
"eslint/no-else-return": [
|
||||
"error",
|
||||
{
|
||||
"allowElseIf": false
|
||||
}
|
||||
],
|
||||
"eslint/no-shadow": [
|
||||
"error",
|
||||
{
|
||||
"ignoreTypeValueShadow": true,
|
||||
"ignoreFunctionTypeParameterNameValueShadow": true
|
||||
}
|
||||
],
|
||||
"eslint/object-shorthand": ["error", "always"],
|
||||
"typescript/array-type": "off",
|
||||
"vitest/require-mock-type-parameters": "off",
|
||||
"vitest/require-to-throw-message": "off",
|
||||
"typescript/no-explicit-any": "off",
|
||||
"unicorn/no-array-for-each": "off",
|
||||
"import/no-cycle": "off",
|
||||
"react/refs": "off",
|
||||
"eslint/constructor-super": "off",
|
||||
"eslint/for-direction": "off",
|
||||
"eslint/getter-return": "off",
|
||||
"eslint/no-async-promise-executor": "off",
|
||||
"eslint/no-class-assign": "off",
|
||||
"eslint/no-compare-neg-zero": "off",
|
||||
"eslint/no-cond-assign": "off",
|
||||
"eslint/no-const-assign": "off",
|
||||
"eslint/no-constant-binary-expression": "off",
|
||||
"eslint/no-constant-condition": "off",
|
||||
"eslint/no-control-regex": "off",
|
||||
"eslint/no-debugger": "off",
|
||||
"eslint/no-delete-var": "off",
|
||||
"eslint/no-dupe-class-members": "off",
|
||||
"eslint/no-dupe-else-if": "off",
|
||||
"eslint/no-dupe-keys": "off",
|
||||
"eslint/no-duplicate-case": "off",
|
||||
"eslint/no-empty-character-class": "off",
|
||||
"eslint/no-empty-pattern": "off",
|
||||
"eslint/no-empty-static-block": "off",
|
||||
"eslint/no-ex-assign": "off",
|
||||
"eslint/no-extra-boolean-cast": "off",
|
||||
"eslint/no-func-assign": "off",
|
||||
"eslint/no-global-assign": "off",
|
||||
"eslint/no-import-assign": "off",
|
||||
"eslint/no-invalid-regexp": "off",
|
||||
"eslint/no-irregular-whitespace": "off",
|
||||
"eslint/no-loss-of-precision": "off",
|
||||
"eslint/no-misleading-character-class": "off",
|
||||
"eslint/no-new-native-nonconstructor": "off",
|
||||
"eslint/no-nonoctal-decimal-escape": "off",
|
||||
"eslint/no-obj-calls": "off",
|
||||
"eslint/no-self-assign": "off",
|
||||
"eslint/no-setter-return": "off",
|
||||
"eslint/no-shadow-restricted-names": "off",
|
||||
"eslint/no-sparse-arrays": "off",
|
||||
"eslint/no-this-before-super": "off",
|
||||
"eslint/no-unassigned-vars": "off",
|
||||
"eslint/no-unreachable": "off",
|
||||
"eslint/no-unsafe-finally": "off",
|
||||
"eslint/no-unsafe-negation": "off",
|
||||
"eslint/no-unsafe-optional-chaining": "off",
|
||||
"eslint/no-unused-expressions": "off",
|
||||
"eslint/no-unused-labels": "off",
|
||||
"eslint/no-unused-private-class-members": "off",
|
||||
"eslint/no-unused-vars": "off",
|
||||
"eslint/no-useless-backreference": "off",
|
||||
"eslint/no-useless-catch": "off",
|
||||
"eslint/no-useless-escape": "off",
|
||||
"eslint/no-with": "off",
|
||||
"eslint/require-yield": "off",
|
||||
"eslint/use-isnan": "off",
|
||||
"eslint/valid-typeof": "off",
|
||||
"import/default": "off",
|
||||
"react/error-boundaries": "off",
|
||||
"react/exhaustive-deps": "off",
|
||||
"react/globals": "off",
|
||||
"react/immutability": "off",
|
||||
"react/incompatible-library": "off",
|
||||
"react/jsx-key": "off",
|
||||
"react/jsx-no-duplicate-props": "off",
|
||||
"react/jsx-no-undef": "off",
|
||||
"react/no-children-prop": "off",
|
||||
"react/no-danger-with-children": "off",
|
||||
"react/no-direct-mutation-state": "off",
|
||||
"react/no-find-dom-node": "off",
|
||||
"react/no-is-mounted": "off",
|
||||
"react/no-render-return-value": "off",
|
||||
"react/no-string-refs": "off",
|
||||
"react/preserve-manual-memoization": "off",
|
||||
"react/purity": "off",
|
||||
"react/set-state-in-effect": "off",
|
||||
"react/set-state-in-render": "off",
|
||||
"react/static-components": "off",
|
||||
"react/use-memo": "off",
|
||||
"react/void-use-memo": "off",
|
||||
"typescript/no-duplicate-enum-values": "off",
|
||||
"typescript/no-extra-non-null-assertion": "off",
|
||||
"typescript/no-misused-new": "off",
|
||||
"typescript/no-non-null-asserted-optional-chain": "off",
|
||||
"typescript/no-this-alias": "off",
|
||||
"typescript/no-unsafe-declaration-merging": "off",
|
||||
"typescript/no-wrapper-object-types": "off",
|
||||
"typescript/prefer-as-const": "off",
|
||||
"typescript/prefer-namespace-keyword": "off",
|
||||
"typescript/triple-slash-reference": "off",
|
||||
"vitest/no-duplicate-hooks": "error",
|
||||
"vitest/expect-expect": "off",
|
||||
"vitest/no-conditional-expect": "off",
|
||||
"vitest/no-disabled-tests": "off",
|
||||
"vitest/no-focused-tests": "off",
|
||||
"vitest/no-standalone-expect": "off",
|
||||
"vitest/require-local-test-context-for-concurrent-snapshots": "off",
|
||||
"vitest/valid-describe-callback": "off",
|
||||
"vitest/valid-expect": "off",
|
||||
"vitest/valid-expect-in-promise": "off",
|
||||
"vitest/valid-title": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.stories.ts", "**/*.stories.tsx", "scripts/*.ts"],
|
||||
"rules": {
|
||||
"eslint/no-console": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+1
-1
@@ -2,7 +2,7 @@
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"stylelint.vscode-stylelint",
|
||||
"oxc.oxc-vscode",
|
||||
"biomejs.biome",
|
||||
"esbenp.prettier-vscode",
|
||||
"denoland.vscode-deno",
|
||||
"github.vscode-github-actions",
|
||||
|
||||
@@ -17,7 +17,6 @@ defaultSemverRangePrefix: ""
|
||||
npmMinimalAgeGate: 7d
|
||||
npmPreapprovedPackages:
|
||||
- "@navikt/*"
|
||||
|
||||
enableTelemetry: false
|
||||
|
||||
yarnPath: .yarn/releases/yarn-4.18.0.cjs
|
||||
@@ -1,19 +1,5 @@
|
||||
# @navikt/aksel-icons
|
||||
|
||||
## 8.20.1
|
||||
|
||||
## 8.20.0
|
||||
|
||||
## 8.19.2
|
||||
|
||||
## 8.19.1
|
||||
|
||||
## 8.19.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Icons: Added new icon MenuHamburgerMagnifyingGlass ([#5368](https://github.com/navikt/aksel/pull/5368))
|
||||
|
||||
## 8.18.0
|
||||
|
||||
## 8.17.2
|
||||
|
||||
@@ -13,7 +13,8 @@ files.forEach((file) => {
|
||||
|
||||
data = data.map((x) => {
|
||||
return x.includes("React.ForwardRefExoticComponent")
|
||||
? `${x.split(":")[0]}: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement> & SVGRProps & React.RefAttributes<SVGSVGElement>>;`
|
||||
? x.split(":")[0] +
|
||||
`: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement> & SVGRProps & React.RefAttributes<SVGSVGElement>>;`
|
||||
: x;
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ ymlList.forEach((file) => {
|
||||
const ymlData = load(fs.readFileSync(`${basePath}/${file}`), {
|
||||
schema: JSON_SCHEMA,
|
||||
});
|
||||
// biome-ignore lint/complexity/useOptionalChain: optional chain not supported in enviroment. Consider revriting to Typescript
|
||||
if (ymlData.keywords && ymlData.keywords.includes("[ignore-docs]")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.5 3.25C17.9518 3.25 20.75 6.04822 20.75 9.5C20.75 11.6221 19.6911 13.4953 18.0742 14.625L20.667 19.6562C20.8567 20.0244 20.7119 20.4773 20.3438 20.667C19.9756 20.8567 19.5227 20.7119 19.333 20.3438L16.751 15.3301C16.0524 15.6 15.2938 15.75 14.5 15.75C11.0482 15.75 8.25 12.9518 8.25 9.5C8.25 6.04822 11.0482 3.25 14.5 3.25ZM15.8994 17.25C15.993 17.25 16.0792 17.3026 16.1221 17.3857L16.6377 18.3857C16.723 18.552 16.6021 18.75 16.415 18.75H3.5C3.08579 18.75 2.75 18.4142 2.75 18C2.75 17.5858 3.08579 17.25 3.5 17.25H15.8994ZM14.5 4.75C11.8766 4.75 9.75 6.87665 9.75 9.5C9.75 12.1234 11.8766 14.25 14.5 14.25C17.1234 14.25 19.25 12.1234 19.25 9.5C19.25 6.87665 17.1234 4.75 14.5 4.75ZM6.75195 11.25C6.86836 11.25 6.96896 11.3306 6.99805 11.4434C7.08182 11.7679 7.1863 12.084 7.30957 12.3906C7.37768 12.56 7.2558 12.75 7.07324 12.75H3.5C3.08579 12.75 2.75 12.4142 2.75 12C2.75 11.5858 3.08579 11.25 3.5 11.25H6.75195ZM7.55664 5.25C7.75557 5.25 7.8765 5.47207 7.77734 5.64453C7.60333 5.94718 7.4499 6.26318 7.31738 6.58984C7.27831 6.68596 7.18482 6.75 7.08105 6.75H3.5C3.08579 6.75 2.75 6.41421 2.75 6C2.75 5.58579 3.08579 5.25 3.5 5.25H7.55664Z" fill="#202733"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.2 KiB |
@@ -1,12 +0,0 @@
|
||||
name: MenuHamburgerMagnifyingGlass
|
||||
category: Interface
|
||||
sub_category: Menu
|
||||
keywords:
|
||||
- Menu
|
||||
- meny
|
||||
- hamburger
|
||||
- search
|
||||
- søk
|
||||
- forstørrelsesglass
|
||||
variant: Stroke
|
||||
created_at: 07.10.2026
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-icons",
|
||||
"version": "8.20.1",
|
||||
"version": "8.18.0",
|
||||
"author": "Aksel | Nav",
|
||||
"homepage": "https://aksel.nav.no/ikoner",
|
||||
"repository": {
|
||||
@@ -78,16 +78,16 @@
|
||||
"@types/lodash": "4.17.25",
|
||||
"@types/react": "19.2.14",
|
||||
"adm-zip": "^0.6.1",
|
||||
"concurrently": "10.0.6",
|
||||
"concurrently": "10.0.4",
|
||||
"copyfiles": "^2.4.1",
|
||||
"fast-glob": "3.3.3",
|
||||
"hast-util-select": "6.0.4",
|
||||
"js-yaml": "^5.5.0",
|
||||
"js-yaml": "^5.4.2",
|
||||
"lodash": "4.18.1",
|
||||
"prettier": "3.9.6",
|
||||
"react": "19.3.1",
|
||||
"react": "19.3.0",
|
||||
"rehype-parse": "9.0.1",
|
||||
"tsc-alias": "1.9.7",
|
||||
"tsc-alias": "1.9.1",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "6.0.3",
|
||||
"unified": "11.0.5",
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* biome-ignore-all lint/correctness/useHookAtTopLevel: `useId` is invariant at runtime. */
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
// https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/useId.ts
|
||||
|
||||
@@ -1,15 +1,5 @@
|
||||
# @navikt/aksel-stylelint
|
||||
|
||||
## 8.20.1
|
||||
|
||||
## 8.20.0
|
||||
|
||||
## 8.19.2
|
||||
|
||||
## 8.19.1
|
||||
|
||||
## 8.19.0
|
||||
|
||||
## 8.18.0
|
||||
|
||||
## 8.17.2
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-stylelint",
|
||||
"version": "8.20.1",
|
||||
"version": "8.18.0",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
@@ -43,10 +43,10 @@
|
||||
"dev": "yarn watch:lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-css": "^8.20.1",
|
||||
"@navikt/ds-tokens": "^8.20.1",
|
||||
"concurrently": "10.0.6",
|
||||
"postcss-selector-parser": "^7.1.7",
|
||||
"@navikt/ds-css": "^8.18.0",
|
||||
"@navikt/ds-tokens": "^8.18.0",
|
||||
"concurrently": "10.0.4",
|
||||
"postcss-selector-parser": "^7.1.6",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"stylelint": "^17.15.0",
|
||||
"stylelint-test-rule-node": "^1.2.0",
|
||||
|
||||
@@ -1,21 +1,5 @@
|
||||
# @navikt/aksel
|
||||
|
||||
## 8.20.1
|
||||
|
||||
## 8.20.0
|
||||
|
||||
## 8.19.2
|
||||
|
||||
## 8.19.1
|
||||
|
||||
## 8.19.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- CLI: Update clipboardy to v5.3.2 ([#5357](https://github.com/navikt/aksel/pull/5357))
|
||||
|
||||
- CLI: Update figlet to 1.11.3 ([#5360](https://github.com/navikt/aksel/pull/5360))
|
||||
|
||||
## 8.18.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel",
|
||||
"version": "8.20.1",
|
||||
"version": "8.18.0",
|
||||
"description": "Aksel command line interface. Codemods and other utilities for Aksel users.",
|
||||
"author": "Aksel | Nav designsystem team",
|
||||
"license": "MIT",
|
||||
@@ -40,13 +40,13 @@
|
||||
"url": "https://github.com/navikt/aksel/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"chalk": "6.0.1",
|
||||
"chalk": "5.6.2",
|
||||
"cli-progress": "3.12.0",
|
||||
"clipboardy": "5.3.2",
|
||||
"commander": "15.0.0",
|
||||
"enquirer": "2.4.1",
|
||||
"fast-glob": "3.3.3",
|
||||
"figlet": "1.12.0",
|
||||
"figlet": "1.11.3",
|
||||
"is-git-clean": "1.1.0",
|
||||
"jscodeshift": "17.4.0",
|
||||
"lodash": "4.18.1"
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("getJSXLiteralValue", () => {
|
||||
});
|
||||
|
||||
test("returns null for TemplateLiteral with expressions", () => {
|
||||
// oxlint-disable-next-line eslint/no-template-curly-in-string -- Testing only
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing only
|
||||
const node = getAttrValue("<Component prop={`hello ${name}`} />");
|
||||
expect(getJSXLiteralValue(node)).toBe(null);
|
||||
});
|
||||
|
||||
@@ -209,7 +209,7 @@ describe("getTokenRegex JS", () => {
|
||||
|
||||
expect("ATextRed").toMatch(regex);
|
||||
expect(" ATextRed ").toMatch(regex);
|
||||
// oxlint-disable-next-line eslint/no-template-curly-in-string -- Testing token match inside template string
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing token match inside template string
|
||||
expect(" background: `${ATextRed}` ").toMatch(regex);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("v8-tokens-js keeps boundaries intact", () => {
|
||||
["const a = ASurfaceDefault;", "const a = BgDefault;"],
|
||||
["const a = [ASurfaceDefault];", "const a = [BgDefault];"],
|
||||
["const a = {...ASurfaceDefault};", "const a = {...BgDefault};"],
|
||||
// oxlint-disable-next-line eslint/no-template-curly-in-string -- Testing token inside template literal
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing token inside template literal
|
||||
["const a = `${ASurfaceDefault}`;", "const a = `${BgDefault}`;"],
|
||||
])("%s", (input, output) => {
|
||||
expect(runJs(`${importLine}\n${input}`)).toBe(
|
||||
|
||||
@@ -1,41 +1,5 @@
|
||||
# @navikt/ds-css
|
||||
|
||||
## 8.20.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Badge: Increase inline padding by 1px for better optical balance ([#5443](https://github.com/navikt/aksel/pull/5443))
|
||||
|
||||
## 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))
|
||||
|
||||
- RadioCard, CheckboxCard: Updated border-color in unchecked error-state, input-color in checked disabled state ([#5425](https://github.com/navikt/aksel/pull/5425))
|
||||
|
||||
## 8.19.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Search: Icon more visible in forced-colors mode ([#5386](https://github.com/navikt/aksel/pull/5386))
|
||||
|
||||
## 8.19.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Accordion: `@media print` now auto-opens every Accordion ([#5363](https://github.com/navikt/aksel/pull/5363))
|
||||
|
||||
- ExpansionCard: `@media print` now auto-opens every ExpansionCard ([#5363](https://github.com/navikt/aksel/pull/5363))
|
||||
|
||||
- ReadMore: `@media print` now auto-opens every ReadMore ([#5363](https://github.com/navikt/aksel/pull/5363))
|
||||
|
||||
## 8.19.0
|
||||
|
||||
## 8.18.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -139,14 +139,14 @@ async function bundle() {
|
||||
|
||||
/* ------------------------------ /global build ----------------------------- */
|
||||
for (const style of BASELINE_CSS_LIST) {
|
||||
const parser = (input: string) => {
|
||||
function parser(input: string) {
|
||||
const parsed = input
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("@import"))
|
||||
.filter((line) => line.includes(style))
|
||||
.join("\n");
|
||||
return `${layerDefinition}\n${parsed}`;
|
||||
};
|
||||
}
|
||||
|
||||
await bundleCSS(parser).then((file) => {
|
||||
writeFile({
|
||||
@@ -208,13 +208,13 @@ async function bundle() {
|
||||
}
|
||||
|
||||
for (const componentLine of componentFiles()) {
|
||||
const parser = (input: string) => {
|
||||
function parser(input: string) {
|
||||
const parsed = input
|
||||
.split("\n")
|
||||
.filter((line) => line === componentLine)
|
||||
.join("\n");
|
||||
return `${layerDefinition}\n${parsed}`;
|
||||
};
|
||||
}
|
||||
|
||||
await bundleCSS(parser).then((file) => {
|
||||
const componentName = componentLine
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "8.20.1",
|
||||
"version": "8.18.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.20.1",
|
||||
"@navikt/ds-tokens": "^8.18.0",
|
||||
"browserslist": "^4.28.7",
|
||||
"esbuild": "^0.28.2",
|
||||
"fast-glob": "3.3.3",
|
||||
|
||||
@@ -123,11 +123,6 @@
|
||||
|
||||
.aksel-accordion--indent > .aksel-accordion__item > .aksel-accordion__content {
|
||||
box-shadow: -2px 0 0 0 var(--ax-border-neutral-subtleA);
|
||||
|
||||
@media print {
|
||||
border-left: 1px solid black;
|
||||
margin-left: calc(var(--ax-space-24) - 2px);
|
||||
}
|
||||
}
|
||||
|
||||
.aksel-accordion__item {
|
||||
@@ -168,16 +163,4 @@
|
||||
border-bottom: 1px solid var(--ax-border-neutral-subtleA);
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
& > .aksel-accordion__content {
|
||||
visibility: visible;
|
||||
|
||||
/* Override grid layout for print. Grid causes element to stretch unexpectedly across printed pages in FF */
|
||||
display: block;
|
||||
margin-block: var(--ax-space-8);
|
||||
margin-block-end: var(--ax-space-24);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,9 +16,7 @@
|
||||
justify-content: center;
|
||||
min-width: var(--__axc-badge-size);
|
||||
height: var(--__axc-badge-size);
|
||||
|
||||
/* 1px optical adjustment inline */
|
||||
padding: var(--ax-space-4) calc(var(--ax-space-4) + var(--ax-space-1));
|
||||
padding: var(--ax-space-4);
|
||||
border-radius: var(--ax-radius-16);
|
||||
background: var(--ax-bg-strong);
|
||||
color: var(--ax-text-contrast);
|
||||
|
||||
@@ -19,10 +19,6 @@
|
||||
box-shadow: 0 0 0 1px var(--ax-border-default);
|
||||
border-color: var(--ax-border-default);
|
||||
}
|
||||
|
||||
@media print {
|
||||
border-color: black;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------ ExpansionCard Small-size ------------------------ */
|
||||
@@ -170,17 +166,6 @@
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@media print {
|
||||
/* Override grid layout for print. Grid causes element to stretch unexpectedly across printed pages in FF */
|
||||
display: block;
|
||||
visibility: visible;
|
||||
padding-block: var(--__axc-expansioncard-padding-block);
|
||||
|
||||
& .aksel-expansioncard__content-inner {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aksel-expansioncard__content-inner {
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
color: var(--ax-text-neutral);
|
||||
width: 100%;
|
||||
min-height: 3rem;
|
||||
display: block;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding: var(--ax-space-8);
|
||||
padding-right: var(--ax-space-40);
|
||||
@@ -81,11 +81,10 @@
|
||||
|
||||
.aksel-combobox2__field-chevron {
|
||||
position: absolute;
|
||||
right: var(--ax-space-8);
|
||||
top: 0;
|
||||
margin-top: 0.75rem; /* We don't use dynamic centering because we don't want it to move if height changes */
|
||||
font-size: 1.5rem;
|
||||
right: var(--ax-space-8);
|
||||
pointer-events: none;
|
||||
margin-top: 12px; /* We don't use dynamic centering because we don't want it to move if height changes */
|
||||
color: var(--ax-text-neutral);
|
||||
|
||||
@media (forced-colors: active) {
|
||||
@@ -93,7 +92,7 @@
|
||||
}
|
||||
|
||||
.aksel-combobox2[data-size="small"] & {
|
||||
margin-top: 0.25rem;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -237,12 +236,6 @@
|
||||
.aksel-combobox2__option:is([aria-selected="true"], [aria-selected="true"][data-virtual-focus="true"])
|
||||
.aksel-listbox__option-inner {
|
||||
background-color: var(--ax-bg-moderate-pressedA);
|
||||
|
||||
@media (forced-colors: active) {
|
||||
background-color: SelectedItem;
|
||||
color: SelectedItemText;
|
||||
forced-color-adjust: none;
|
||||
}
|
||||
}
|
||||
|
||||
.aksel-combobox2__option-checkmark {
|
||||
|
||||
@@ -19,8 +19,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* Checkbox and Radio card */
|
||||
.aksel-checkbox-card,
|
||||
/* Radio card */
|
||||
.aksel-radio-card {
|
||||
width: 100%;
|
||||
align-content: flex-start;
|
||||
@@ -31,30 +30,26 @@
|
||||
border-radius: var(--ax-radius-8);
|
||||
|
||||
&:focus-within::after,
|
||||
&:is(.aksel-checkbox--small, .aksel-radio--small):focus-within::after {
|
||||
&.aksel-radio--small:focus-within::after {
|
||||
inset: 0;
|
||||
border-radius: calc(var(--ax-radius-8) - 1px);
|
||||
outline-offset: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
/* 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) {
|
||||
/* Radio card group */
|
||||
.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-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) {
|
||||
&.aksel-radio-group--small > .aksel-radio-buttons > .aksel-hgrid:not(.aksel-hgrid-gap) {
|
||||
gap: var(--ax-space-8);
|
||||
}
|
||||
}
|
||||
@@ -70,11 +65,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -248,14 +246,8 @@
|
||||
--__axc-radio-checkbox-marker-border: 7px;
|
||||
}
|
||||
|
||||
/* Card border in hover and checked states */
|
||||
.aksel-checkbox-card:is(
|
||||
.aksel-checkbox--error:not(:where(.aksel-checkbox--disabled, .aksel-checkbox--readonly)),
|
||||
: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))
|
||||
),
|
||||
/* Radio card border in hover and checked states */
|
||||
.aksel-radio-card:is(
|
||||
.aksel-radio--error:not(:where(.aksel-radio--disabled, .aksel-radio--readonly)),
|
||||
: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))
|
||||
) {
|
||||
@@ -263,10 +255,7 @@
|
||||
box-shadow: 0 0 0 1px var(--ax-border-default);
|
||||
}
|
||||
|
||||
/* Card background in checked state */
|
||||
.aksel-checkbox-card:has(.aksel-checkbox__input:checked):not(
|
||||
:where(.aksel-checkbox--disabled, .aksel-checkbox--readonly, .aksel-checkbox--error)
|
||||
),
|
||||
/* Radio card background in checked state */
|
||||
.aksel-radio-card:has(> .aksel-radio__input:checked):not(
|
||||
:where(.aksel-radio--disabled, .aksel-radio--readonly, .aksel-radio--error)
|
||||
) {
|
||||
@@ -276,8 +265,6 @@
|
||||
/* 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);
|
||||
@@ -296,16 +283,18 @@
|
||||
opacity: var(--ax-opacity-disabled);
|
||||
}
|
||||
|
||||
/* Card background in readonly state */
|
||||
.aksel-checkbox-card:where(.aksel-checkbox--readonly),
|
||||
/* Radio card background in disabled and readonly states */
|
||||
.aksel-radio-card:where(.aksel-radio--readonly) {
|
||||
background-color: var(--ax-bg-neutral-moderate);
|
||||
}
|
||||
|
||||
.aksel-radio-card.aksel-radio--disabled > .aksel-radio__input:checked {
|
||||
border-color: var(--ax-border-neutral-strong);
|
||||
}
|
||||
|
||||
/* 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;
|
||||
}
|
||||
@@ -313,7 +302,6 @@
|
||||
/* 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;
|
||||
}
|
||||
|
||||
@@ -81,10 +81,6 @@
|
||||
pointer-events: none;
|
||||
font-size: 1.5rem;
|
||||
|
||||
@media (forced-colors: active) {
|
||||
color: CanvasText;
|
||||
}
|
||||
|
||||
.aksel-search--disabled & {
|
||||
opacity: var(--ax-opacity-disabled);
|
||||
}
|
||||
|
||||
@@ -49,9 +49,5 @@
|
||||
outline: 2px solid var(--ax-border-focus);
|
||||
outline-offset: -2px;
|
||||
background-color: var(--ax-bg-neutral-moderate-hoverA);
|
||||
|
||||
@media (forced-colors: active) {
|
||||
outline-color: Highlight;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
/* biome-ignore lint/suspicious/noDuplicateProperties: vh is fallback for browsers that do not support lvh */
|
||||
min-height: 100lvh;
|
||||
}
|
||||
|
||||
@@ -19,6 +20,7 @@
|
||||
|
||||
.aksel-page__content--fullheight {
|
||||
min-height: 100vh;
|
||||
/* biome-ignore lint/suspicious/noDuplicateProperties: vh is fallback for browsers that do not support lvh */
|
||||
min-height: 100lvh;
|
||||
}
|
||||
|
||||
|
||||
@@ -86,10 +86,6 @@
|
||||
|
||||
.aksel-read-more[data-state="closed"] & {
|
||||
display: none;
|
||||
|
||||
@media print {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,18 +87,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
/* 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,69 +1,5 @@
|
||||
# @navikt/ds-react
|
||||
|
||||
## 8.20.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@8.20.1
|
||||
- @navikt/aksel-icons@8.20.1
|
||||
|
||||
## 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))
|
||||
|
||||
- Build: `next build --webpack` now works when package is part of `optimizePackageImports` ([#5422](https://github.com/navikt/aksel/pull/5422))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@8.20.0
|
||||
- @navikt/aksel-icons@8.20.0
|
||||
|
||||
## 8.19.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- TextField: Now inherits `readOnly` from Fieldset. No longer gets read-only styling when `disabled` and `readOnly`. ([#5328](https://github.com/navikt/aksel/pull/5328))
|
||||
|
||||
- Textarea: Now inherits `readOnly` from Fieldset. No longer gets read-only styling when `disabled` and `readOnly`. ([#5328](https://github.com/navikt/aksel/pull/5328))
|
||||
|
||||
- Fieldset: Nested Fieldsets now inherits the parent's `disabled` state. ([#5328](https://github.com/navikt/aksel/pull/5328))
|
||||
|
||||
- Button: `loading={true}` and `disabled={false}` now disable the element. ([#5328](https://github.com/navikt/aksel/pull/5328))
|
||||
|
||||
- Switch: `loading={true}` and `disabled={false}` now disable the element. ([#5328](https://github.com/navikt/aksel/pull/5328))
|
||||
|
||||
- Search.Button: `loading={true}` and `disabled={false}` now disable SearchButton. ([#5328](https://github.com/navikt/aksel/pull/5328))
|
||||
|
||||
- Badge: No longer sets aria-hidden internally automatically. ([#5397](https://github.com/navikt/aksel/pull/5397))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@8.19.2
|
||||
- @navikt/aksel-icons@8.19.2
|
||||
|
||||
## 8.19.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@8.19.1
|
||||
- @navikt/aksel-icons@8.19.1
|
||||
|
||||
## 8.19.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`c5dd309`](https://github.com/navikt/aksel/commit/c5dd3097e51ef2a9f79964248398bd158660bd3b)]:
|
||||
- @navikt/aksel-icons@8.19.0
|
||||
- @navikt/ds-tokens@8.19.0
|
||||
|
||||
## 8.18.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "8.20.1",
|
||||
"version": "8.18.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",
|
||||
@@ -68,22 +68,22 @@
|
||||
},
|
||||
"./PREVIEW/Lookup": {
|
||||
"import": {
|
||||
"types": "./esm/lookup/index-deprecated.d.ts",
|
||||
"default": "./esm/lookup/index-deprecated.js"
|
||||
"types": "./esm/lookup/index.d.ts",
|
||||
"default": "./esm/lookup/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./cjs/lookup/index-deprecated.d.ts",
|
||||
"default": "./cjs/lookup/index-deprecated.js"
|
||||
"types": "./cjs/lookup/index.d.ts",
|
||||
"default": "./cjs/lookup/index.js"
|
||||
}
|
||||
},
|
||||
"./PREVIEW/Badge": {
|
||||
"import": {
|
||||
"types": "./esm/badge/index-deprecated.d.ts",
|
||||
"default": "./esm/badge/index-deprecated.js"
|
||||
"types": "./esm/badge/index.d.ts",
|
||||
"default": "./esm/badge/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./cjs/badge/index-deprecated.d.ts",
|
||||
"default": "./cjs/badge/index-deprecated.js"
|
||||
"types": "./cjs/badge/index.d.ts",
|
||||
"default": "./cjs/badge/index.js"
|
||||
}
|
||||
},
|
||||
"./PREVIEW/RadioCard": {
|
||||
@@ -96,16 +96,6 @@
|
||||
"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"
|
||||
},
|
||||
@@ -788,30 +778,30 @@
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "0.27.20",
|
||||
"@floating-ui/react-dom": "^2.1.9",
|
||||
"@navikt/aksel-icons": "^8.20.1",
|
||||
"@navikt/ds-tokens": "^8.20.1",
|
||||
"@navikt/aksel-icons": "^8.18.0",
|
||||
"@navikt/ds-tokens": "^8.18.0",
|
||||
"date-fns": "^4.0.0",
|
||||
"react-day-picker": "10.0.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "10.4.2",
|
||||
"@testing-library/jest-dom": "^7.0.1",
|
||||
"@testing-library/dom": "10.4.1",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.3.3",
|
||||
"@testing-library/user-event": "^14.6.8",
|
||||
"@testing-library/user-event": "^14.6.7",
|
||||
"@types/jscodeshift": "17.3.0",
|
||||
"@types/node": "^24.10.9",
|
||||
"@types/react": "19.2.14",
|
||||
"@types/react-dom": "19.2.3",
|
||||
"concurrently": "10.0.6",
|
||||
"concurrently": "10.0.4",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"jscodeshift": "17.4.0",
|
||||
"jsdom": "29.1.1",
|
||||
"react": "19.3.1",
|
||||
"react-dom": "19.3.1",
|
||||
"react": "19.3.0",
|
||||
"react-dom": "19.3.0",
|
||||
"rimraf": "6.1.3",
|
||||
"swr": "^2.4.2",
|
||||
"tsc-alias": "1.9.7",
|
||||
"tsc-alias": "1.9.1",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "6.0.3",
|
||||
"vitest": "4.1.11"
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useRef } from "react";
|
||||
import { useModalContext } from "../../modal/root/ModalRoot.context";
|
||||
import { useModalContext } from "../../modal/Modal.context";
|
||||
import { useId } from "../../utils-external";
|
||||
import {
|
||||
Menu,
|
||||
|
||||
@@ -16,8 +16,10 @@ const ActionMenuShortcut = ({ children }: ShortcutProps) => {
|
||||
<ActionMenuMarker placement="right">
|
||||
{parsed.map((char, index) => (
|
||||
<span
|
||||
// oxlint-disable-next-line react/no-array-index-key -- This is a static list of characters, so using the index as a key is acceptable in this case
|
||||
key={`${children}-${index}-${char}`}
|
||||
key={`${children}-${
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: This is a static list of characters, so using the index as a key is acceptable in this case
|
||||
index
|
||||
}-${char}`}
|
||||
className="aksel-action-menu__shortcut"
|
||||
>
|
||||
{char}
|
||||
|
||||
@@ -19,12 +19,12 @@ export default meta;
|
||||
|
||||
type Story = StoryObj<typeof Badge>;
|
||||
|
||||
const statuses: AkselColor[] = [
|
||||
"danger",
|
||||
"success",
|
||||
"info",
|
||||
"warning",
|
||||
"neutral",
|
||||
const statuses: { color: AkselColor; label: string }[] = [
|
||||
{ color: "danger", label: "Avslått" },
|
||||
{ color: "success", label: "Innvilget" },
|
||||
{ color: "info", label: "Under behandling" },
|
||||
{ color: "warning", label: "Mangler dokumentasjon" },
|
||||
{ color: "neutral", label: "Ikke startet" },
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
@@ -37,8 +37,8 @@ export const Default: Story = {
|
||||
export const Dot: Story = {
|
||||
render: () => (
|
||||
<HStack gap="space-16" align="center">
|
||||
{statuses.map((color) => (
|
||||
<Badge key={color} data-color={color} />
|
||||
{statuses.map(({ color, label }) => (
|
||||
<Badge key={color} data-color={color} aria-label={label} />
|
||||
))}
|
||||
</HStack>
|
||||
),
|
||||
@@ -58,13 +58,13 @@ export const Count: Story = {
|
||||
export const Anchored: Story = {
|
||||
render: () => (
|
||||
<HStack gap="space-32" align="center">
|
||||
<Badge data-color="danger" count={42} aria-hidden>
|
||||
<Badge data-color="danger" count={42}>
|
||||
<Button
|
||||
icon={<InboxIcon aria-hidden />}
|
||||
aria-label="Innboks, 42 nye meldinger"
|
||||
/>
|
||||
</Badge>
|
||||
<Badge data-color="danger" aria-hidden>
|
||||
<Badge data-color="danger" aria-label="Nytt varsel">
|
||||
<Button icon={<InboxIcon aria-hidden />} aria-label="Innboks" />
|
||||
</Badge>
|
||||
</HStack>
|
||||
|
||||
@@ -35,8 +35,9 @@ interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
* Use standalone for inline indicators/counts, or pass the element you want to
|
||||
* anchor it to as `children` to pin the badge to one of its corners.
|
||||
*
|
||||
* When anchoring to an interactive element,
|
||||
* set `aria-hidden` on the badge and fold the count into that element's accessible name
|
||||
* Accessibility: A badge with no label is treated as decorative when it is a
|
||||
* dot or anchored to another element. When anchoring to an interactive element,
|
||||
* fold the state into that element's accessible name
|
||||
* (e.g. `aria-label="Inbox, 42 new messages"`).
|
||||
*
|
||||
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/badge)
|
||||
@@ -45,11 +46,11 @@ interface BadgeProps extends HTMLAttributes<HTMLSpanElement> {
|
||||
* @example
|
||||
* ```jsx
|
||||
* // Standalone
|
||||
* <Badge data-color="success" />
|
||||
* <Badge data-color="success" aria-label="Active" />
|
||||
* <Badge data-color="danger" count={42} />
|
||||
*
|
||||
* // Anchored to an element
|
||||
* <Badge data-color="danger" count={42} aria-hidden>
|
||||
* <Badge data-color="danger" count={42}>
|
||||
* <Button icon={<InboxIcon />} aria-label="Inbox, 42 new messages" />
|
||||
* </Badge>
|
||||
* ```
|
||||
@@ -60,6 +61,10 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
||||
children,
|
||||
className,
|
||||
"data-color": color = "danger",
|
||||
"aria-label": ariaLabel,
|
||||
"aria-labelledby": ariaLabelledby,
|
||||
role,
|
||||
"aria-hidden": ariaHidden,
|
||||
count,
|
||||
maxCount = 99,
|
||||
...rest
|
||||
@@ -70,6 +75,10 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
||||
count == null ? undefined : normalizeCount(count, 0);
|
||||
const normalizedMaxCount = normalizeCount(maxCount, 99);
|
||||
const isDot = normalizedCount == null;
|
||||
const hasLabel =
|
||||
Boolean(ariaLabel?.trim()) || Boolean(ariaLabelledby?.trim());
|
||||
const isDecorative =
|
||||
role == null && !hasLabel && (isDot || children != null);
|
||||
|
||||
const badge = (
|
||||
<BodyShort
|
||||
@@ -80,6 +89,10 @@ const Badge = forwardRef<HTMLSpanElement, BadgeProps>(
|
||||
weight="semibold"
|
||||
data-color={color}
|
||||
data-dot={isDot || undefined}
|
||||
aria-label={ariaLabel}
|
||||
aria-labelledby={ariaLabelledby}
|
||||
role={role}
|
||||
aria-hidden={ariaHidden ?? (isDecorative || undefined)}
|
||||
className={cl("aksel-badge", className)}
|
||||
>
|
||||
{isDot
|
||||
@@ -112,11 +125,4 @@ export namespace Badge {
|
||||
export type Props = BadgeProps;
|
||||
}
|
||||
|
||||
/** @deprecated Badge is no longer in preview. Import it from `@navikt/ds-react` instead. */
|
||||
const BadgeDeprecated = Badge;
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
namespace BadgeDeprecated {
|
||||
export type Props = BadgeProps;
|
||||
}
|
||||
|
||||
export { Badge, BadgeDeprecated };
|
||||
export { Badge };
|
||||
@@ -1,2 +0,0 @@
|
||||
"use client";
|
||||
export { BadgeDeprecated as Badge } from "./Badge";
|
||||
@@ -105,9 +105,9 @@ export const Button: OverridableComponent<ButtonProps, HTMLButtonElement> =
|
||||
className={cl(className, "aksel-button", `aksel-button--${size}`, {
|
||||
"aksel-button--loading": loading,
|
||||
"aksel-button--icon-only": !!icon && !children,
|
||||
"aksel-button--disabled": disabled || loading,
|
||||
"aksel-button--disabled": disabled ?? loading,
|
||||
})}
|
||||
disabled={disabled || loading ? true : undefined}
|
||||
disabled={(disabled ?? loading) ? true : undefined}
|
||||
>
|
||||
{icon && iconPosition === "left" && (
|
||||
<span className="aksel-button__icon">{icon}</span>
|
||||
|
||||
+5
-6
@@ -1,8 +1,8 @@
|
||||
import React, { type HTMLAttributes, forwardRef } from "react";
|
||||
import { Detail, Heading, type HeadingProps } from "../../typography";
|
||||
import { cl } from "../../utils/helpers";
|
||||
import { Detail, Heading, type HeadingProps } from "../typography";
|
||||
import { cl } from "../utils/helpers";
|
||||
|
||||
interface ChatBubbleProps extends HTMLAttributes<HTMLDivElement> {
|
||||
export interface ChatBubbleProps extends HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Bubble text.
|
||||
*/
|
||||
@@ -26,7 +26,7 @@ interface ChatBubbleProps extends HTMLAttributes<HTMLDivElement> {
|
||||
toptextHeadingLevel?: Exclude<HeadingProps["level"], "1">;
|
||||
}
|
||||
|
||||
const ChatBubble = forwardRef<HTMLDivElement, ChatBubbleProps>(
|
||||
const Bubble = forwardRef<HTMLDivElement, ChatBubbleProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
@@ -65,5 +65,4 @@ const ChatBubble = forwardRef<HTMLDivElement, ChatBubbleProps>(
|
||||
},
|
||||
);
|
||||
|
||||
export { ChatBubble };
|
||||
export type { ChatBubbleProps };
|
||||
export default Bubble;
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
import { PersonIcon } from "@navikt/aksel-icons";
|
||||
import { VStack } from "../primitives/stack";
|
||||
import { renderStoriesForChromatic } from "../utils/renderStoriesForChromatic";
|
||||
import { Chat, POSITIONS, SIZES, VARIANTS } from "./root/ChatRoot";
|
||||
import Chat, { POSITIONS, SIZES, VARIANTS } from "./Chat";
|
||||
|
||||
export default {
|
||||
title: "ds-react/Chat",
|
||||
|
||||
+38
-33
@@ -1,13 +1,13 @@
|
||||
import React, { type HTMLAttributes, forwardRef } from "react";
|
||||
import type { AkselStatusColorRole } from "@navikt/ds-tokens/types";
|
||||
import type { AkselColor } from "../../types";
|
||||
import { BodyLong, type HeadingProps } from "../../typography";
|
||||
import { cl } from "../../utils/helpers";
|
||||
import { ChatBubble, type ChatBubbleProps } from "../bubble/ChatBubble";
|
||||
import type { AkselColor } from "../types";
|
||||
import { BodyLong, type HeadingProps } from "../typography";
|
||||
import { cl } from "../utils/helpers";
|
||||
import Bubble, { type ChatBubbleProps } from "./Bubble";
|
||||
|
||||
const VARIANTS = ["subtle", "info", "neutral"] as const;
|
||||
const POSITIONS = ["left", "right"] as const;
|
||||
const SIZES = ["medium", "small"] as const;
|
||||
export const VARIANTS = ["subtle", "info", "neutral"] as const;
|
||||
export const POSITIONS = ["left", "right"] as const;
|
||||
export const SIZES = ["medium", "small"] as const;
|
||||
|
||||
interface ChatProps extends HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
@@ -62,7 +62,33 @@ interface ChatProps extends HTMLAttributes<HTMLDivElement> {
|
||||
"data-color"?: Exclude<AkselColor, AkselStatusColorRole>;
|
||||
}
|
||||
|
||||
const ChatRoot = forwardRef<HTMLDivElement, ChatProps>(
|
||||
interface ChatComponent extends React.ForwardRefExoticComponent<
|
||||
ChatProps & React.RefAttributes<HTMLDivElement>
|
||||
> {
|
||||
/**
|
||||
* @see 🏷️ {@link ChatBubbleProps}
|
||||
*/
|
||||
Bubble: typeof Bubble;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component for communicating a dialog between two or more parties.
|
||||
*
|
||||
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/chat)
|
||||
* @see 🏷️ {@link ChatProps}
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <Chat avatar="A" name="Alice" timestamp="01.01.21 14:00">
|
||||
* <Chat.Bubble>Hello!</Chat.Bubble>
|
||||
* <Chat.Bubble>How can I help you?</Chat.Bubble>
|
||||
* </Chat>
|
||||
* <Chat avatar="B" name="Bob" timestamp="01.01.21 14:01" position="right">
|
||||
* <Chat.Bubble>Hi there!</Chat.Bubble>
|
||||
* </Chat>
|
||||
* ```
|
||||
*/
|
||||
export const Chat = forwardRef<HTMLDivElement, ChatProps>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
@@ -117,7 +143,7 @@ const ChatRoot = forwardRef<HTMLDivElement, ChatProps>(
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
) as ChatComponent;
|
||||
|
||||
function variantToColor(variant: ChatProps["variant"]): AkselColor {
|
||||
switch (variant) {
|
||||
@@ -132,29 +158,8 @@ function variantToColor(variant: ChatProps["variant"]): AkselColor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A component for communicating a dialog between two or more parties.
|
||||
*
|
||||
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/chat)
|
||||
* @see 🏷️ {@link ChatProps}
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <Chat avatar="A" name="Alice" timestamp="01.01.21 14:00">
|
||||
* <Chat.Bubble>Hello!</Chat.Bubble>
|
||||
* <Chat.Bubble>How can I help you?</Chat.Bubble>
|
||||
* </Chat>
|
||||
* <Chat avatar="B" name="Bob" timestamp="01.01.21 14:01" position="right">
|
||||
* <Chat.Bubble>Hi there!</Chat.Bubble>
|
||||
* </Chat>
|
||||
* ```
|
||||
*/
|
||||
const Chat = Object.assign(ChatRoot, {
|
||||
/**
|
||||
* @see 🏷️ {@link ChatBubbleProps}
|
||||
*/
|
||||
Bubble: ChatBubble,
|
||||
});
|
||||
Chat.Bubble = Bubble;
|
||||
|
||||
export { Chat, ChatBubble, VARIANTS, POSITIONS, SIZES };
|
||||
export default Chat;
|
||||
export { Bubble as ChatBubble };
|
||||
export type { ChatProps, ChatBubbleProps };
|
||||
@@ -1,3 +1,3 @@
|
||||
"use client";
|
||||
export { Chat, ChatBubble } from "./root/ChatRoot";
|
||||
export type { ChatProps, ChatBubbleProps } from "./root/ChatRoot";
|
||||
export { default as Chat, ChatBubble } from "./Chat";
|
||||
export type { ChatProps, ChatBubbleProps } from "./Chat";
|
||||
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
export { Chips, ChipsRemovable, ChipsToggle } from "./root/ChipsRoot";
|
||||
export type {
|
||||
ChipsProps,
|
||||
ChipsRemovableProps,
|
||||
ChipsToggleProps,
|
||||
} from "./root/ChipsRoot";
|
||||
export { default as Chips, type ChipsProps } from "./root/ChipsRoot";
|
||||
export {
|
||||
ChipsRemovable,
|
||||
type ChipsRemovableProps,
|
||||
} from "./removable/ChipsRemovable";
|
||||
export { ChipsToggle, type ChipsToggleProps } from "./toggle/ChipsToggle";
|
||||
@@ -4,7 +4,7 @@ import type { AkselColor } from "../../types";
|
||||
import { cl, composeEventHandlers } from "../../utils/helpers";
|
||||
import { useI18n } from "../../utils/i18n/i18n.hooks";
|
||||
|
||||
interface ChipsRemovableProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
export interface ChipsRemovableProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: string;
|
||||
/**
|
||||
* @deprecated Use `data-color` prop instead.
|
||||
@@ -22,7 +22,10 @@ interface ChipsRemovableProps extends React.ButtonHTMLAttributes<HTMLButtonEleme
|
||||
"data-color"?: AkselColor;
|
||||
}
|
||||
|
||||
const ChipsRemovable = forwardRef<HTMLButtonElement, ChipsRemovableProps>(
|
||||
export const ChipsRemovable = forwardRef<
|
||||
HTMLButtonElement,
|
||||
ChipsRemovableProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
@@ -43,7 +46,6 @@ const ChipsRemovable = forwardRef<HTMLButtonElement, ChipsRemovableProps>(
|
||||
data-color={color ?? variantToColor(variant)}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
// oxlint-disable-next-line react/button-has-type -- React's button type union is validated by TypeScript and defaults to "button".
|
||||
type={type}
|
||||
className={cl(
|
||||
"aksel-chips__chip aksel-chips__removable aksel-chips--icon-right",
|
||||
@@ -74,5 +76,4 @@ function variantToColor(
|
||||
}
|
||||
}
|
||||
|
||||
export { ChipsRemovable };
|
||||
export type { ChipsRemovableProps };
|
||||
export default ChipsRemovable;
|
||||
@@ -1,10 +1,7 @@
|
||||
import React, { type HTMLAttributes, forwardRef } from "react";
|
||||
import { cl } from "../../utils/helpers";
|
||||
import {
|
||||
ChipsRemovable,
|
||||
type ChipsRemovableProps,
|
||||
} from "../removable/ChipsRemovable";
|
||||
import { ChipsToggle, type ChipsToggleProps } from "../toggle/ChipsToggle";
|
||||
import { ChipsRemovable } from "../removable/ChipsRemovable";
|
||||
import { ChipsToggle } from "../toggle/ChipsToggle";
|
||||
|
||||
interface ChipsProps extends HTMLAttributes<HTMLUListElement> {
|
||||
children: React.ReactNode;
|
||||
@@ -15,24 +12,22 @@ interface ChipsProps extends HTMLAttributes<HTMLUListElement> {
|
||||
size?: "medium" | "small";
|
||||
}
|
||||
|
||||
const ChipsRoot = forwardRef<HTMLUListElement, ChipsProps>(
|
||||
({ className, size = "medium", children, ...rest }, ref) => {
|
||||
return (
|
||||
<ul
|
||||
{...rest}
|
||||
ref={ref}
|
||||
className={cl("aksel-chips", className, `aksel-chips--${size}`, {
|
||||
"aksel-body-short aksel-body-short--small": size === "medium",
|
||||
"aksel-detail aksel-detail--small": size === "small",
|
||||
})}
|
||||
>
|
||||
{React.Children.map(children, (chip, index) => {
|
||||
return <li key={chip?.toString() || index}>{chip}</li>;
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
);
|
||||
interface ChipsComponent extends React.ForwardRefExoticComponent<
|
||||
ChipsProps & React.RefAttributes<HTMLUListElement>
|
||||
> {
|
||||
/**
|
||||
* Toggle between selected-states.
|
||||
* @see 🏷️ {@link ToggleChipsProps}
|
||||
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
|
||||
*/
|
||||
Toggle: typeof ChipsToggle;
|
||||
/**
|
||||
* Remove filter or the likes on click.
|
||||
* @see 🏷️ {@link RemovableChipsProps}
|
||||
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
|
||||
*/
|
||||
Removable: typeof ChipsRemovable;
|
||||
}
|
||||
|
||||
/**
|
||||
* A component that displays a list of items as chips.
|
||||
@@ -61,20 +56,27 @@ const ChipsRoot = forwardRef<HTMLUListElement, ChipsProps>(
|
||||
* </Chips>
|
||||
* ```
|
||||
*/
|
||||
const Chips = Object.assign(ChipsRoot, {
|
||||
/**
|
||||
* Toggle between selected-states.
|
||||
* @see 🏷️ {@link ChipsToggleProps}
|
||||
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
|
||||
*/
|
||||
Toggle: ChipsToggle,
|
||||
/**
|
||||
* Remove filter or the likes on click.
|
||||
* @see 🏷️ {@link ChipsRemovableProps}
|
||||
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
|
||||
*/
|
||||
Removable: ChipsRemovable,
|
||||
});
|
||||
export const Chips = forwardRef<HTMLUListElement, ChipsProps>(
|
||||
({ className, size = "medium", children, ...rest }, ref) => {
|
||||
return (
|
||||
<ul
|
||||
{...rest}
|
||||
ref={ref}
|
||||
className={cl("aksel-chips", className, `aksel-chips--${size}`, {
|
||||
"aksel-body-short aksel-body-short--small": size === "medium",
|
||||
"aksel-detail aksel-detail--small": size === "small",
|
||||
})}
|
||||
>
|
||||
{React.Children.map(children, (chip, index) => {
|
||||
return <li key={chip?.toString() || index}>{chip}</li>;
|
||||
})}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
) as ChipsComponent;
|
||||
|
||||
export { Chips, ChipsRemovable, ChipsToggle };
|
||||
export type { ChipsProps, ChipsRemovableProps, ChipsToggleProps };
|
||||
Chips.Toggle = ChipsToggle;
|
||||
Chips.Removable = ChipsRemovable;
|
||||
|
||||
export default Chips;
|
||||
export type { ChipsProps };
|
||||
@@ -3,7 +3,7 @@ import type { AkselColor } from "../../types";
|
||||
import type { OverridableComponent } from "../../utils-external";
|
||||
import { cl } from "../../utils/helpers";
|
||||
|
||||
interface ChipsToggleProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
export interface ChipsToggleProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
children: string;
|
||||
/**
|
||||
* Toggles aria-pressed and visual changes
|
||||
@@ -26,66 +26,68 @@ interface ChipsToggleProps extends React.ButtonHTMLAttributes<HTMLButtonElement>
|
||||
checkmark?: boolean;
|
||||
}
|
||||
|
||||
const ChipsToggle: OverridableComponent<ChipsToggleProps, HTMLButtonElement> =
|
||||
forwardRef(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
selected,
|
||||
variant,
|
||||
checkmark = true,
|
||||
as: Component = "button",
|
||||
"data-color": color,
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<Component
|
||||
data-color={color ?? variantToColor(variant)}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
className={cl("aksel-chips__chip aksel-chips__toggle", className, {
|
||||
"aksel-chips__toggle--with-checkmark": checkmark,
|
||||
})}
|
||||
aria-pressed={selected}
|
||||
data-pressed={selected}
|
||||
>
|
||||
{checkmark && (
|
||||
<svg
|
||||
aria-hidden
|
||||
className="aksel-chips__toggle-icon"
|
||||
width="1.25em"
|
||||
height="1.25em"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
focusable={false}
|
||||
>
|
||||
{selected ? (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 18.125C14.4873 18.125 18.125 14.4873 18.125 10C18.125 5.51269 14.4873 1.875 10 1.875C5.51269 1.875 1.875 5.51269 1.875 10C1.875 14.4873 5.51269 18.125 10 18.125ZM14.128 7.72904C14.3695 7.44357 14.3339 7.01635 14.0485 6.7748C13.763 6.53326 13.3358 6.56886 13.0942 6.85432L8.60428 12.1606L6.41627 9.97263C6.15185 9.70822 5.72315 9.70822 5.45873 9.97263C5.19431 10.2371 5.19431 10.6658 5.45873 10.9302L8.16706 13.6385C8.30095 13.7724 8.48479 13.8441 8.67397 13.8362C8.86316 13.8284 9.0404 13.7416 9.16271 13.5971L14.128 7.72904Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
) : (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 3.125C6.20304 3.125 3.125 6.20304 3.125 10C3.125 13.797 6.20304 16.875 10 16.875C13.797 16.875 16.875 13.797 16.875 10C16.875 6.20304 13.797 3.125 10 3.125ZM1.875 10C1.875 5.51269 5.51269 1.875 10 1.875C14.4873 1.875 18.125 5.51269 18.125 10C18.125 14.4873 14.4873 18.125 10 18.125C5.51269 18.125 1.875 14.4873 1.875 10Z"
|
||||
/* After removing old fallbacks, change to currentColor */
|
||||
fill="var(--ax-text-default)"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
)}
|
||||
<span className="aksel-chips__chip-text">{children}</span>
|
||||
</Component>
|
||||
);
|
||||
export const ChipsToggle: OverridableComponent<
|
||||
ChipsToggleProps,
|
||||
HTMLButtonElement
|
||||
> = forwardRef(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
selected,
|
||||
variant,
|
||||
checkmark = true,
|
||||
as: Component = "button",
|
||||
"data-color": color,
|
||||
...rest
|
||||
},
|
||||
);
|
||||
ref,
|
||||
) => {
|
||||
return (
|
||||
<Component
|
||||
data-color={color ?? variantToColor(variant)}
|
||||
{...rest}
|
||||
ref={ref}
|
||||
className={cl("aksel-chips__chip aksel-chips__toggle", className, {
|
||||
"aksel-chips__toggle--with-checkmark": checkmark,
|
||||
})}
|
||||
aria-pressed={selected}
|
||||
data-pressed={selected}
|
||||
>
|
||||
{checkmark && (
|
||||
<svg
|
||||
aria-hidden
|
||||
className="aksel-chips__toggle-icon"
|
||||
width="1.25em"
|
||||
height="1.25em"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
focusable={false}
|
||||
>
|
||||
{selected ? (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 18.125C14.4873 18.125 18.125 14.4873 18.125 10C18.125 5.51269 14.4873 1.875 10 1.875C5.51269 1.875 1.875 5.51269 1.875 10C1.875 14.4873 5.51269 18.125 10 18.125ZM14.128 7.72904C14.3695 7.44357 14.3339 7.01635 14.0485 6.7748C13.763 6.53326 13.3358 6.56886 13.0942 6.85432L8.60428 12.1606L6.41627 9.97263C6.15185 9.70822 5.72315 9.70822 5.45873 9.97263C5.19431 10.2371 5.19431 10.6658 5.45873 10.9302L8.16706 13.6385C8.30095 13.7724 8.48479 13.8441 8.67397 13.8362C8.86316 13.8284 9.0404 13.7416 9.16271 13.5971L14.128 7.72904Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
) : (
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M10 3.125C6.20304 3.125 3.125 6.20304 3.125 10C3.125 13.797 6.20304 16.875 10 16.875C13.797 16.875 16.875 13.797 16.875 10C16.875 6.20304 13.797 3.125 10 3.125ZM1.875 10C1.875 5.51269 5.51269 1.875 10 1.875C14.4873 1.875 18.125 5.51269 18.125 10C18.125 14.4873 14.4873 18.125 10 18.125C5.51269 18.125 1.875 14.4873 1.875 10Z"
|
||||
/* After removing old fallbacks, change to currentColor */
|
||||
fill="var(--ax-text-default)"
|
||||
/>
|
||||
)}
|
||||
</svg>
|
||||
)}
|
||||
<span className="aksel-chips__chip-text">{children}</span>
|
||||
</Component>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function variantToColor(
|
||||
variant?: ChipsToggleProps["variant"],
|
||||
@@ -100,5 +102,4 @@ function variantToColor(
|
||||
}
|
||||
}
|
||||
|
||||
export { ChipsToggle };
|
||||
export type { ChipsToggleProps };
|
||||
export default ChipsToggle;
|
||||
@@ -149,7 +149,7 @@ export namespace DataGrid {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Table {
|
||||
// oxlint-disable-next-line eslint/no-shadow -- intentional namespace re-export
|
||||
// biome-ignore lint/suspicious/noShadow: intentional namespace re-export
|
||||
export type Props<T = unknown> = DataGridTable.Props<T>;
|
||||
export type Sorting = DataGridTable.Sorting;
|
||||
export type SortEntry = DataGridTable.SortEntry;
|
||||
@@ -161,7 +161,7 @@ export namespace DataGrid {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Preferences {
|
||||
// oxlint-disable-next-line eslint/no-shadow -- intentional namespace re-export
|
||||
// biome-ignore lint/suspicious/noShadow: intentional namespace re-export
|
||||
export type Props = DataGridPreferences.Props;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** biome-ignore-all lint/correctness/useHookAtTopLevel: False positive because of the way forwardRef() is added */
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
|
||||
@@ -139,6 +139,7 @@ function useTableColumnResize({
|
||||
[resizeMin, resizeMax, setWidth],
|
||||
);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We only want to run this on mount and when autoResizeOnce changes
|
||||
useEffect(
|
||||
function autoResizeColumn() {
|
||||
if (!autoResizeOnce) {
|
||||
|
||||
@@ -86,9 +86,7 @@ const AutoSuggest = forwardRef<HTMLInputElement, AutoSuggestProps>(
|
||||
<Floating>
|
||||
<Listbox setVirtuallyFocusedOptionId={setVirtuallyFocusedOptionId}>
|
||||
<Floating.Anchor>
|
||||
<Listbox.InputSlot
|
||||
virtuallyFocusedOptionId={virtuallyFocusedOptionId}
|
||||
>
|
||||
<Listbox.InputSlot>
|
||||
<Search
|
||||
label="Tabellsøk"
|
||||
variant="simple"
|
||||
|
||||
@@ -220,6 +220,7 @@ export const TokenFilter = forwardRef<HTMLDivElement, TokenFilterProps>(
|
||||
);
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/useSemanticElements: search-tag is too new (baseline 2023)
|
||||
<div
|
||||
ref={ref}
|
||||
className={cl("aksel-property-filter", className)}
|
||||
|
||||
@@ -61,7 +61,7 @@ function createGroups<T>(
|
||||
}
|
||||
|
||||
/* Custom groups first, then default group if it has items */
|
||||
const groups: OptionGroup<T>[] = Object.values(customGroups);
|
||||
const groups: OptionGroup<T>[] = [...Object.values(customGroups)];
|
||||
|
||||
if (defaultGroup.options.length > 0) {
|
||||
groups.push(defaultGroup);
|
||||
|
||||
@@ -3,7 +3,7 @@ import { isSameDay } from "date-fns";
|
||||
import React, { useState } from "react";
|
||||
import { Button } from "../button";
|
||||
import { Dialog } from "../dialog";
|
||||
import { Modal } from "../modal";
|
||||
import Modal from "../modal/Modal";
|
||||
import { Box } from "../primitives/box";
|
||||
import { HGrid } from "../primitives/grid";
|
||||
import { HStack, Spacer, VStack } from "../primitives/stack";
|
||||
@@ -49,15 +49,16 @@ export const Default: StoryObj<DefaultStoryProps> = {
|
||||
toDate: new Date("Aug 23 2029"),
|
||||
});
|
||||
|
||||
const newProps =
|
||||
!props.inputfield || props.mode === "multiple"
|
||||
const newProps = {
|
||||
...(!props.inputfield || props.mode === "multiple"
|
||||
? {
|
||||
open,
|
||||
onClose: () => setOpen(false),
|
||||
fromDate: new Date("Aug 23 2020"),
|
||||
toDate: new Date("Aug 23 2029"),
|
||||
}
|
||||
: {};
|
||||
: {}),
|
||||
};
|
||||
|
||||
const Comp = !props.standalone ? DatePicker : DatePicker.Standalone;
|
||||
|
||||
|
||||
@@ -564,6 +564,89 @@ export const HookFallbackToDate: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
const navigationToday = new Date("Jun 1 2025");
|
||||
|
||||
export const HookIgnoresUnreasonableYear: Story = {
|
||||
render: () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
today: navigationToday,
|
||||
});
|
||||
|
||||
return (
|
||||
<DatePicker {...datepickerProps}>
|
||||
<DatePicker.Input {...inputProps} label="Velg dato" />
|
||||
</DatePicker>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.type(canvas.getByLabelText("Velg dato"), "07.09.4582");
|
||||
await userEvent.click(canvas.getByTitle("Åpne datovelger"));
|
||||
|
||||
const dialog = canvas.getByRole("dialog");
|
||||
expect(
|
||||
within(dialog).getByText(
|
||||
format(navigationToday, "LLLL y", { locale: nb }),
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const HookNavigatesToReasonableYear: Story = {
|
||||
render: () => {
|
||||
const { datepickerProps, inputProps } = useDatepicker({
|
||||
today: navigationToday,
|
||||
});
|
||||
|
||||
return (
|
||||
<DatePicker {...datepickerProps}>
|
||||
<DatePicker.Input {...inputProps} label="Velg dato" />
|
||||
</DatePicker>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.type(canvas.getByLabelText("Velg dato"), "07.09.2030");
|
||||
await userEvent.click(canvas.getByTitle("Åpne datovelger"));
|
||||
|
||||
const dialog = canvas.getByRole("dialog");
|
||||
expect(within(dialog).getByText("september 2030")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const RangeHookIgnoresUnreasonableYear: Story = {
|
||||
render: () => {
|
||||
const { datepickerProps, fromInputProps, toInputProps } =
|
||||
useRangeDatepicker({ today: navigationToday });
|
||||
|
||||
return (
|
||||
<DatePicker {...datepickerProps} open>
|
||||
<HStack gap="space-16">
|
||||
<DatePicker.Input {...fromInputProps} label="Fra" />
|
||||
<DatePicker.Input {...toInputProps} label="Til" />
|
||||
</HStack>
|
||||
</DatePicker>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
await userEvent.click(canvas.getByLabelText("Fra"));
|
||||
await userEvent.paste("07.09.4582");
|
||||
await userEvent.click(canvas.getByLabelText("Til"));
|
||||
await userEvent.paste("08.09.4582");
|
||||
|
||||
const dialog = canvas.getByRole("dialog");
|
||||
expect(
|
||||
within(dialog).getByText(
|
||||
format(navigationToday, "LLLL y", { locale: nb }),
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
|
||||
export const SelectSameRangedDate: Story = {
|
||||
render: () => {
|
||||
const { datepickerProps, toInputProps, fromInputProps } =
|
||||
|
||||
@@ -28,7 +28,6 @@ const DatePickerDayButton = ({
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
{...rest}
|
||||
ref={ref}
|
||||
aria-hidden={day.outside}
|
||||
|
||||
@@ -104,7 +104,6 @@ const ReactDayPicker = ({
|
||||
selected={selected}
|
||||
classNames={LegacyClassNames}
|
||||
components={{
|
||||
// oxlint-disable-next-line react/jsx-no-useless-fragment -- react-day-picker requires an Element return type, so null is not allowed.
|
||||
MonthCaption: () => <></>,
|
||||
DayButton: useCallback(
|
||||
(props) => <DatePickerDayButton {...props} locale={locale} />,
|
||||
|
||||
@@ -3,7 +3,12 @@ import React, { useCallback, useState } from "react";
|
||||
import { type DayEventHandler, dateMatchModifiers } from "react-day-picker";
|
||||
import { getLocaleFromString } from "../../utils/components/date/Date.locale";
|
||||
import type { DateInputProps } from "../../utils/components/date/input/DateInput";
|
||||
import { formatDateForInput, isValidDate, parseDate } from "../../utils/date";
|
||||
import {
|
||||
formatDateForInput,
|
||||
isNavigableYear,
|
||||
isValidDate,
|
||||
parseDate,
|
||||
} from "../../utils/date";
|
||||
import { focusElement } from "../../utils/helpers/focus";
|
||||
import { useDateLocale } from "../../utils/i18n/i18n.hooks";
|
||||
import type { DatePickerProps } from "./DatePickerRoot";
|
||||
@@ -173,10 +178,18 @@ export const useDatepicker = (
|
||||
(newOpen: boolean) => {
|
||||
setOpen(newOpen);
|
||||
if (newOpen) {
|
||||
setMonth(selectedDay ?? defaultSelected ?? defaultMonth ?? today);
|
||||
const navigableSelectedDay =
|
||||
selectedDay &&
|
||||
isNavigableYear({ day: selectedDay, today, fromDate, toDate })
|
||||
? selectedDay
|
||||
: undefined;
|
||||
|
||||
setMonth(
|
||||
navigableSelectedDay ?? defaultSelected ?? defaultMonth ?? today,
|
||||
);
|
||||
}
|
||||
},
|
||||
[defaultMonth, defaultSelected, selectedDay, today],
|
||||
[defaultMonth, defaultSelected, fromDate, selectedDay, toDate, today],
|
||||
);
|
||||
|
||||
const updateDate = (date?: Date) => {
|
||||
@@ -221,7 +234,10 @@ export const useDatepicker = (
|
||||
const isAfter =
|
||||
toDate && day && differenceInCalendarDays(day, toDate) > 0;
|
||||
|
||||
!isBefore && !isAfter && setMonth(day);
|
||||
!isBefore &&
|
||||
!isAfter &&
|
||||
isNavigableYear({ day, today, fromDate, toDate }) &&
|
||||
setMonth(day);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -313,7 +329,9 @@ export const useDatepicker = (
|
||||
}
|
||||
updateDate(day);
|
||||
updateValidation();
|
||||
setMonth(defaultMonth ?? day);
|
||||
if (defaultMonth || isNavigableYear({ day, today, fromDate, toDate })) {
|
||||
setMonth(defaultMonth ?? day);
|
||||
}
|
||||
};
|
||||
|
||||
const datepickerProps = {
|
||||
|
||||
@@ -4,7 +4,12 @@ import { dateMatchModifiers } from "react-day-picker";
|
||||
import { getLocaleFromString } from "../../utils/components/date/Date.locale";
|
||||
import type { DateRange } from "../../utils/components/date/Date.typeutils";
|
||||
import type { DateInputProps } from "../../utils/components/date/input/DateInput";
|
||||
import { formatDateForInput, isValidDate, parseDate } from "../../utils/date";
|
||||
import {
|
||||
formatDateForInput,
|
||||
isNavigableYear,
|
||||
isValidDate,
|
||||
parseDate,
|
||||
} from "../../utils/date";
|
||||
import { focusElement } from "../../utils/helpers/focus";
|
||||
import { useDateLocale } from "../../utils/i18n/i18n.hooks";
|
||||
import type { DatePickerProps } from "./DatePickerRoot";
|
||||
@@ -316,6 +321,11 @@ export const useRangeDatepicker = (
|
||||
setValidation(initialValidation(range, opt));
|
||||
};
|
||||
|
||||
/* Avoid navigating the calendar to unreasonable years, e.g. while typing */
|
||||
const navigateToMonth = (day: Date) => {
|
||||
isNavigableYear({ day, today, fromDate, toDate }) && setMonth(day);
|
||||
};
|
||||
|
||||
const handleFocus = (e, src: RangeT) => {
|
||||
if (e.target.readOnly) {
|
||||
return;
|
||||
@@ -339,7 +349,7 @@ export const useRangeDatepicker = (
|
||||
const isAfter =
|
||||
toDate && day && differenceInCalendarDays(day, toDate) > 0;
|
||||
|
||||
!isBefore && !isAfter && setMonth(day);
|
||||
!isBefore && !isAfter && navigateToMonth(day);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -438,7 +448,7 @@ export const useRangeDatepicker = (
|
||||
) {
|
||||
updateRange({ to: day, from: day });
|
||||
setToInputValue(formatDateForInput(day, locale, "date", inputFormat));
|
||||
setMonth(day);
|
||||
navigateToMonth(day);
|
||||
updateValidation();
|
||||
return;
|
||||
}
|
||||
@@ -453,14 +463,14 @@ export const useRangeDatepicker = (
|
||||
);
|
||||
if (validateDay(toDay)) {
|
||||
updateRange({ from: day, to: toDay });
|
||||
setMonth(day);
|
||||
navigateToMonth(day);
|
||||
updateValidation();
|
||||
return;
|
||||
}
|
||||
}
|
||||
updateRange({ ...selectedRange, from: day });
|
||||
updateValidation({}, validation.to);
|
||||
setMonth(day);
|
||||
navigateToMonth(day);
|
||||
};
|
||||
|
||||
const toChange = (
|
||||
@@ -508,7 +518,7 @@ export const useRangeDatepicker = (
|
||||
}
|
||||
updateRange({ from: selectedRange?.from, to: day });
|
||||
updateValidation(validation.from, {});
|
||||
setMonth(day);
|
||||
navigateToMonth(day);
|
||||
};
|
||||
|
||||
/* live-update datepicker based on changes in inputfields */
|
||||
|
||||
@@ -23,7 +23,7 @@ const DatePickerWeekRow = ({
|
||||
|
||||
return (
|
||||
<Show below="sm" asChild>
|
||||
{/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role -- role="grid" improves a11y reading */}
|
||||
{/** biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: role="grid" improves a11y reading */}
|
||||
<table className="rdp-table" role="grid">
|
||||
<tbody className="rdp-tbody">
|
||||
<tr className="rdp-row aksel-date__week-row">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { useModalContext } from "../../modal/root/ModalRoot.context";
|
||||
import { useModalContext } from "../../modal/Modal.context";
|
||||
import { Portal, type PortalProps } from "../../portal";
|
||||
import { DialogBackdropInternal } from "../backdrop/DialogBackdropInternal";
|
||||
import { useDialogContext } from "../root/DialogRoot.context";
|
||||
|
||||
+65
-66
@@ -1,24 +1,51 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import type { AkselColor } from "../../types";
|
||||
import { cl } from "../../utils/helpers";
|
||||
import { useControllableState } from "../../utils/hooks";
|
||||
import {
|
||||
ExpansionCardContent,
|
||||
import type { AkselColor } from "../types";
|
||||
import type { OverridableComponent } from "../utils-external";
|
||||
import { cl } from "../utils/helpers";
|
||||
import { useControllableState } from "../utils/hooks";
|
||||
import ExpansionCardContent, {
|
||||
type ExpansionCardContentProps,
|
||||
} from "../content/ExpansionCardContent";
|
||||
} from "./ExpansionCardContent";
|
||||
import {
|
||||
ExpansionCardDescription,
|
||||
type ExpansionCardDescriptionProps,
|
||||
} from "../description/ExpansionCardDescription";
|
||||
import {
|
||||
ExpansionCardHeader,
|
||||
} from "./ExpansionCardDescription";
|
||||
import ExpansionCardHeader, {
|
||||
type ExpansionCardHeaderProps,
|
||||
} from "../header/ExpansionCardHeader";
|
||||
} from "./ExpansionCardHeader";
|
||||
import {
|
||||
ExpansionCardTitle,
|
||||
type ExpansionCardTitleProps,
|
||||
} from "../title/ExpansionCardTitle";
|
||||
import { ExpansionCardContext } from "./ExpansionCardRoot.context";
|
||||
} from "./ExpansionCardTitle";
|
||||
import { ExpansionCardContext } from "./context";
|
||||
|
||||
interface ExpansionCardComponent extends React.ForwardRefExoticComponent<
|
||||
ExpansionCardProps & React.RefAttributes<HTMLDivElement>
|
||||
> {
|
||||
/**
|
||||
* @see 🏷️ {@link ExpansionCardHeaderProps}
|
||||
*/
|
||||
Header: React.ForwardRefExoticComponent<
|
||||
ExpansionCardHeaderProps & React.RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
/**
|
||||
* @see 🏷️ {@link ExpansionCardTitleProps}
|
||||
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
|
||||
*/
|
||||
Title: OverridableComponent<ExpansionCardTitleProps, HTMLHeadingElement>;
|
||||
/**
|
||||
* @see 🏷️ {@link ExpansionCardDescriptionProps}
|
||||
*/
|
||||
Description: React.ForwardRefExoticComponent<
|
||||
ExpansionCardDescriptionProps & React.RefAttributes<HTMLParagraphElement>
|
||||
>;
|
||||
/**
|
||||
* @see 🏷️ {@link ExpansionCardContentProps}
|
||||
*/
|
||||
Content: React.ForwardRefExoticComponent<
|
||||
ExpansionCardContentProps & React.RefAttributes<HTMLDivElement>
|
||||
>;
|
||||
}
|
||||
|
||||
interface ExpansionCardCommonProps extends Omit<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
@@ -66,10 +93,28 @@ type ExpansionCardConditionalProps =
|
||||
"aria-labelledby": string;
|
||||
};
|
||||
|
||||
type ExpansionCardProps = ExpansionCardCommonProps &
|
||||
export type ExpansionCardProps = ExpansionCardCommonProps &
|
||||
ExpansionCardConditionalProps;
|
||||
|
||||
const ExpansionCardRoot = forwardRef<HTMLDivElement, ExpansionCardProps>(
|
||||
/**
|
||||
* A component that displays an expandable card.
|
||||
*
|
||||
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/expansioncard)
|
||||
* @see 🏷️ {@link ExpansionCardProps}
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <ExpansionCard aria-label="Utbetaling av sykepenger">
|
||||
* <ExpansionCard.Header>
|
||||
* <ExpansionCard.Title>Utbetaling av sykepenger</ExpansionCard.Title>
|
||||
* </ExpansionCard.Header>
|
||||
* <ExpansionCard.Content>
|
||||
* <Innhold />
|
||||
* </ExpansionCard.Content>
|
||||
* </ExpansionCard>
|
||||
* ```
|
||||
*/
|
||||
export const ExpansionCard = forwardRef<HTMLDivElement, ExpansionCardProps>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
@@ -109,57 +154,11 @@ const ExpansionCardRoot = forwardRef<HTMLDivElement, ExpansionCardProps>(
|
||||
</ExpansionCardContext.Provider>
|
||||
);
|
||||
},
|
||||
);
|
||||
) as ExpansionCardComponent;
|
||||
|
||||
/**
|
||||
* A component that displays an expandable card.
|
||||
*
|
||||
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/expansioncard)
|
||||
* @see 🏷️ {@link ExpansionCardProps}
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <ExpansionCard aria-label="Utbetaling av sykepenger">
|
||||
* <ExpansionCard.Header>
|
||||
* <ExpansionCard.Title>Utbetaling av sykepenger</ExpansionCard.Title>
|
||||
* </ExpansionCard.Header>
|
||||
* <ExpansionCard.Content>
|
||||
* <Innhold />
|
||||
* </ExpansionCard.Content>
|
||||
* </ExpansionCard>
|
||||
* ```
|
||||
*/
|
||||
const ExpansionCard = Object.assign(ExpansionCardRoot, {
|
||||
/**
|
||||
* @see 🏷️ {@link ExpansionCardHeaderProps}
|
||||
*/
|
||||
Header: ExpansionCardHeader,
|
||||
/**
|
||||
* @see 🏷️ {@link ExpansionCardTitleProps}
|
||||
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
|
||||
*/
|
||||
Title: ExpansionCardTitle,
|
||||
/**
|
||||
* @see 🏷️ {@link ExpansionCardDescriptionProps}
|
||||
*/
|
||||
Description: ExpansionCardDescription,
|
||||
/**
|
||||
* @see 🏷️ {@link ExpansionCardContentProps}
|
||||
*/
|
||||
Content: ExpansionCardContent,
|
||||
});
|
||||
ExpansionCard.Header = ExpansionCardHeader;
|
||||
ExpansionCard.Content = ExpansionCardContent;
|
||||
ExpansionCard.Title = ExpansionCardTitle;
|
||||
ExpansionCard.Description = ExpansionCardDescription;
|
||||
|
||||
export {
|
||||
ExpansionCard,
|
||||
ExpansionCardHeader,
|
||||
ExpansionCardTitle,
|
||||
ExpansionCardDescription,
|
||||
ExpansionCardContent,
|
||||
};
|
||||
export type {
|
||||
ExpansionCardProps,
|
||||
ExpansionCardHeaderProps,
|
||||
ExpansionCardTitleProps,
|
||||
ExpansionCardDescriptionProps,
|
||||
ExpansionCardContentProps,
|
||||
};
|
||||
export default ExpansionCard;
|
||||
+6
-7
@@ -1,10 +1,10 @@
|
||||
import React, { forwardRef, useContext } from "react";
|
||||
import { useThemeInternal } from "../../theme/Theme";
|
||||
import { BodyLong } from "../../typography";
|
||||
import { cl } from "../../utils/helpers";
|
||||
import { ExpansionCardContext } from "../root/ExpansionCardRoot.context";
|
||||
import { useThemeInternal } from "../theme/Theme";
|
||||
import { BodyLong } from "../typography";
|
||||
import { cl } from "../utils/helpers";
|
||||
import { ExpansionCardContext } from "./context";
|
||||
|
||||
interface ExpansionCardContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
export interface ExpansionCardContentProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -44,5 +44,4 @@ const ExpansionCardContent = forwardRef<
|
||||
);
|
||||
});
|
||||
|
||||
export { ExpansionCardContent };
|
||||
export type { ExpansionCardContentProps };
|
||||
export default ExpansionCardContent;
|
||||
+6
-7
@@ -1,13 +1,13 @@
|
||||
import React, { forwardRef, useContext } from "react";
|
||||
import { BodyLong } from "../../typography";
|
||||
import { cl } from "../../utils/helpers";
|
||||
import { ExpansionCardContext } from "../root/ExpansionCardRoot.context";
|
||||
import { BodyLong } from "../typography";
|
||||
import { cl } from "../utils/helpers";
|
||||
import { ExpansionCardContext } from "./context";
|
||||
|
||||
interface ExpansionCardDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
||||
export interface ExpansionCardDescriptionProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
const ExpansionCardDescription = forwardRef<
|
||||
export const ExpansionCardDescription = forwardRef<
|
||||
HTMLParagraphElement,
|
||||
ExpansionCardDescriptionProps
|
||||
>(({ className, ...rest }, ref) => {
|
||||
@@ -31,5 +31,4 @@ const ExpansionCardDescription = forwardRef<
|
||||
);
|
||||
});
|
||||
|
||||
export { ExpansionCardDescription };
|
||||
export type { ExpansionCardDescriptionProps };
|
||||
export default ExpansionCardDescription;
|
||||
+5
-6
@@ -1,10 +1,10 @@
|
||||
import React, { forwardRef, useContext } from "react";
|
||||
import { ChevronDownIcon } from "@navikt/aksel-icons";
|
||||
import { cl } from "../../utils/helpers";
|
||||
import { useI18n } from "../../utils/i18n/i18n.hooks";
|
||||
import { ExpansionCardContext } from "../root/ExpansionCardRoot.context";
|
||||
import { cl } from "../utils/helpers";
|
||||
import { useI18n } from "../utils/i18n/i18n.hooks";
|
||||
import { ExpansionCardContext } from "./context";
|
||||
|
||||
interface ExpansionCardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
export interface ExpansionCardHeaderProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -46,5 +46,4 @@ const ExpansionCardHeader = forwardRef<
|
||||
);
|
||||
});
|
||||
|
||||
export { ExpansionCardHeader };
|
||||
export type { ExpansionCardHeaderProps };
|
||||
export default ExpansionCardHeader;
|
||||
+5
-6
@@ -1,8 +1,8 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import type { OverridableComponent } from "../../utils-external";
|
||||
import { cl } from "../../utils/helpers";
|
||||
import type { OverridableComponent } from "../utils-external";
|
||||
import { cl } from "../utils/helpers";
|
||||
|
||||
interface ExpansionCardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
export interface ExpansionCardTitleProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* Changes text-sizing
|
||||
@@ -11,7 +11,7 @@ interface ExpansionCardTitleProps extends React.HTMLAttributes<HTMLHeadingElemen
|
||||
size?: "large" | "medium" | "small";
|
||||
}
|
||||
|
||||
const ExpansionCardTitle: OverridableComponent<
|
||||
export const ExpansionCardTitle: OverridableComponent<
|
||||
ExpansionCardTitleProps,
|
||||
HTMLHeadingElement
|
||||
> = forwardRef(
|
||||
@@ -32,5 +32,4 @@ const ExpansionCardTitle: OverridableComponent<
|
||||
},
|
||||
);
|
||||
|
||||
export { ExpansionCardTitle };
|
||||
export type { ExpansionCardTitleProps };
|
||||
export default ExpansionCardTitle;
|
||||
@@ -0,0 +1,13 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
export type ExpansionCardContextProps = {
|
||||
open: boolean;
|
||||
toggleOpen: () => void;
|
||||
size: "medium" | "small";
|
||||
};
|
||||
|
||||
export const ExpansionCardContext = createContext<ExpansionCardContextProps>({
|
||||
open: false,
|
||||
toggleOpen: () => {},
|
||||
size: "medium",
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
export {
|
||||
ExpansionCard,
|
||||
ExpansionCardContent,
|
||||
ExpansionCardHeader,
|
||||
ExpansionCardTitle,
|
||||
ExpansionCardDescription,
|
||||
} from "./root/ExpansionCardRoot";
|
||||
export type { ExpansionCardProps } from "./root/ExpansionCardRoot";
|
||||
type ExpansionCardProps,
|
||||
default as ExpansionCard,
|
||||
} from "./ExpansionCard";
|
||||
export { default as ExpansionCardContent } from "./ExpansionCardContent";
|
||||
export { default as ExpansionCardHeader } from "./ExpansionCardHeader";
|
||||
export { default as ExpansionCardTitle } from "./ExpansionCardTitle";
|
||||
export { default as ExpansionCardDescription } from "./ExpansionCardDescription";
|
||||
@@ -1,16 +0,0 @@
|
||||
import { createContext } from "react";
|
||||
|
||||
type ExpansionCardContextProps = {
|
||||
open: boolean;
|
||||
toggleOpen: () => void;
|
||||
size: "medium" | "small";
|
||||
};
|
||||
|
||||
const ExpansionCardContext = createContext<ExpansionCardContextProps>({
|
||||
open: false,
|
||||
toggleOpen: () => {},
|
||||
size: "medium",
|
||||
});
|
||||
|
||||
export { ExpansionCardContext };
|
||||
export type { ExpansionCardContextProps };
|
||||
@@ -107,18 +107,6 @@ function DisabledFormElements() {
|
||||
<TextField label="Second field" />
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset legend="Nested Fieldset (outer disabled)" disabled>
|
||||
<Fieldset legend="Inner fieldset">
|
||||
<TextField label="Nested field" />
|
||||
<Switch>Nested switch</Switch>
|
||||
</Fieldset>
|
||||
</Fieldset>
|
||||
|
||||
<Fieldset legend="Fieldset (readOnly)" readOnly>
|
||||
<TextField label="TextField" defaultValue="Read-only value" />
|
||||
<Textarea label="Textarea" defaultValue="Read-only value" />
|
||||
</Fieldset>
|
||||
|
||||
<FileUpload.Dropzone
|
||||
label="FileUpload Dropzone"
|
||||
onSelect={console.log}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
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 };
|
||||
@@ -1,129 +0,0 @@
|
||||
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" };
|
||||
@@ -1,41 +0,0 @@
|
||||
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 };
|
||||
@@ -1,37 +0,0 @@
|
||||
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 };
|
||||
@@ -1,3 +0,0 @@
|
||||
"use client";
|
||||
export { CheckboxCard } from "./CheckboxCard";
|
||||
export { CheckboxCardGroup } from "./CheckboxCardGroup";
|
||||
@@ -88,22 +88,3 @@ 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,32 +1,21 @@
|
||||
import React, { forwardRef, useRef } from "react";
|
||||
import React, { forwardRef } from "react";
|
||||
import { BodyShort } from "../../typography";
|
||||
import { omit, useId } from "../../utils-external";
|
||||
import { cl, createStrictContext } from "../../utils/helpers";
|
||||
import { useMergeRefs } from "../../utils/hooks";
|
||||
import { cl } from "../../utils/helpers";
|
||||
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 (
|
||||
// oxlint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- clickable div for checkbox card
|
||||
<div
|
||||
className={cl(className, "aksel-checkbox", `aksel-checkbox--${size}`, {
|
||||
"aksel-checkbox--error": hasError,
|
||||
@@ -34,26 +23,9 @@ 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={mergedRefs}
|
||||
ref={forwardedRef}
|
||||
{...omit(props, [
|
||||
"children",
|
||||
"size",
|
||||
@@ -100,4 +72,3 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
);
|
||||
|
||||
export default Checkbox;
|
||||
export { CheckboxCardContextProvider };
|
||||
@@ -19,7 +19,7 @@ export interface CheckboxProps
|
||||
*/
|
||||
children: React.ReactNode;
|
||||
/**
|
||||
* Hides the label and description visually while keeping them available to screen readers.
|
||||
* Visually hides label and description, and makes it viewable for screen-readers only.
|
||||
*/
|
||||
hideLabel?: boolean;
|
||||
/**
|
||||
|
||||
@@ -42,6 +42,7 @@ const ComboboxWrapper = ({
|
||||
}
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: Wrapper for combobox input and chips.
|
||||
<div
|
||||
ref={wrapperRef}
|
||||
className={cl(
|
||||
|
||||
@@ -52,7 +52,7 @@ const AddNewOption = () => {
|
||||
activeDescendantId === filteredOptionsUtil.getAddNewOptionId(id),
|
||||
},
|
||||
)}
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role -- List item acts as an option in the combobox.
|
||||
// biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: List item acts as an option in the combobox.
|
||||
role="option"
|
||||
aria-selected={false}
|
||||
>
|
||||
|
||||
@@ -113,7 +113,7 @@ const FilteredOptions = () => {
|
||||
{shouldRenderFilteredOptionsList && (
|
||||
<ul
|
||||
ref={setFilteredOptionsRef}
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role -- The <ul> element acts as a listbox in the combobox.
|
||||
// biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: The <ul> element acts as a listbox in the combobox.
|
||||
role="listbox"
|
||||
className="aksel-combobox__list-options"
|
||||
>
|
||||
|
||||
@@ -76,7 +76,7 @@ const FilteredOptionsItem = ({ option }: { option: ComboboxOption }) => {
|
||||
toggleIsListOpen(false);
|
||||
}
|
||||
}}
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role -- List item acts as an option in the combobox.
|
||||
// biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: List item acts as an option in the combobox.
|
||||
role="option"
|
||||
aria-selected={isInList(option.value, selectedOptions)}
|
||||
aria-disabled={isDisabled(option) || undefined}
|
||||
|
||||
@@ -54,7 +54,8 @@ export const InputController = forwardRef<
|
||||
|
||||
return (
|
||||
<Floating.Anchor asChild ref={setAnchorRef}>
|
||||
{/* oxlint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- Acts as wrapper list-toggle and input; key-events are handled by input itself */}
|
||||
{/** biome-ignore lint/a11y/noStaticElementInteractions: Acts as wrapper list-toggle and input */}
|
||||
{/** biome-ignore lint/a11y/useKeyWithClickEvents: key-events are handled by input itself */}
|
||||
<div
|
||||
className={cl("aksel-combobox__wrapper-inner aksel-text-field__input", {
|
||||
"aksel-combobox__wrapper-inner--virtually-unfocused":
|
||||
|
||||
@@ -112,6 +112,7 @@ const SelectedOptionsProvider = ({
|
||||
const newHideCaret =
|
||||
isLimitReached || (!isMultiSelect && selectedOptions.length > 0);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We explicitly want to run this effect when selectedOptions changes to match the view with the selected options.
|
||||
useEffect(() => {
|
||||
setHideCaret(newHideCaret);
|
||||
}, [newHideCaret, selectedOptions, setHideCaret]);
|
||||
|
||||
@@ -60,7 +60,8 @@ export interface ComboboxProps
|
||||
*/
|
||||
filteredOptions?: string[] | ComboboxOption[];
|
||||
/**
|
||||
* Hides the label and description visually while keeping them available to screen readers.
|
||||
* Optionally hide the label visually.
|
||||
* Not recommended, but can be considered for e.g. search fields in the top menu.
|
||||
*/
|
||||
hideLabel?: boolean;
|
||||
/**
|
||||
|
||||
@@ -21,19 +21,20 @@ import { ComboboxTrigger } from "./trigger/ComboboxTrigger";
|
||||
interface ComboboxProps<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
>
|
||||
extends Omit<ComboboxRootProps<T>, "children">, Omit<FormFieldProps, "size"> {
|
||||
/**
|
||||
* Label
|
||||
*/
|
||||
extends
|
||||
Omit<
|
||||
ComboboxRootProps<T>,
|
||||
"children" | "selectedOptions" | "onToggleOption"
|
||||
>,
|
||||
Partial<Pick<ComboboxRootProps<T>, "selectedOptions" | "onToggleOption">>,
|
||||
Omit<FormFieldProps, "size"> {
|
||||
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<
|
||||
@@ -42,6 +43,8 @@ function Combobox<
|
||||
label,
|
||||
hideLabel,
|
||||
name,
|
||||
defaultSelectedOptions,
|
||||
onSelectedOptionsChange,
|
||||
size: sizeProp,
|
||||
error,
|
||||
errorId: errorIdProp,
|
||||
@@ -49,7 +52,9 @@ function Combobox<
|
||||
description,
|
||||
id,
|
||||
readOnly: readOnlyProp,
|
||||
multiple,
|
||||
multiselect = false, // Default value should be synced with ComboboxRoot
|
||||
selectedOptions: selectedOptionsProp,
|
||||
onToggleOption: onToggleOptionProp,
|
||||
...rest
|
||||
}: ComboboxProps<T>) {
|
||||
const {
|
||||
@@ -73,11 +78,35 @@ 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}
|
||||
multiple={multiple}
|
||||
multiselect={multiselect}
|
||||
selectedOptions={selectedOptions}
|
||||
onToggleOption={onToggleOption}
|
||||
{...rest}
|
||||
>
|
||||
<ComboboxLabel
|
||||
@@ -117,8 +146,8 @@ function Combobox<
|
||||
</div>
|
||||
|
||||
{name &&
|
||||
rest.value.map((val) => (
|
||||
<input key={val} type="hidden" name={name} value={val} />
|
||||
selectedOptions.map((value) => (
|
||||
<input key={value} type="hidden" name={name} value={value} />
|
||||
))}
|
||||
|
||||
<ComboboxOverlay
|
||||
@@ -140,126 +169,5 @@ function Combobox<
|
||||
);
|
||||
}
|
||||
|
||||
// ----------- 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 };
|
||||
export { Combobox };
|
||||
export type { ComboboxProps };
|
||||
@@ -3,7 +3,8 @@ import React, { useMemo, useState } from "react";
|
||||
import { Button } from "../../button";
|
||||
import { Dialog } from "../../dialog";
|
||||
import { HStack, VStack } from "../../primitives/stack";
|
||||
import { ComboboxMulti, ComboboxSingle } from "./Combobox";
|
||||
import { Select } from "../select";
|
||||
import { Combobox, type ComboboxProps } from "./Combobox";
|
||||
import { ComboboxField } from "./field/ComboboxField";
|
||||
import { ComboboxFilter } from "./filter/ComboboxFilter";
|
||||
import { ComboboxLabel } from "./label/ComboboxLabel";
|
||||
@@ -17,9 +18,9 @@ import {
|
||||
} from "./root/ComboboxRoot";
|
||||
import { ComboboxTrigger } from "./trigger/ComboboxTrigger";
|
||||
|
||||
const meta: Meta<typeof ComboboxSingle> = {
|
||||
const meta: Meta<typeof Combobox> = {
|
||||
title: "ds-react/Combobox2",
|
||||
component: ComboboxSingle,
|
||||
component: Combobox,
|
||||
parameters: {
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
@@ -76,12 +77,13 @@ const groupedCountries: (MyGroup | MyOption)[] = [
|
||||
];
|
||||
|
||||
type DefaultProps = Pick<
|
||||
ComboboxSingle.Props<MyOption>,
|
||||
ComboboxProps<MyOption>,
|
||||
| "label"
|
||||
| "description"
|
||||
| "hideLabel"
|
||||
| "size"
|
||||
| "defaultOpen"
|
||||
| "multiselect"
|
||||
| "id"
|
||||
| "readOnly"
|
||||
| "disabled"
|
||||
@@ -89,13 +91,14 @@ type DefaultProps = Pick<
|
||||
>;
|
||||
|
||||
export const Default: StoryFn<DefaultProps> = (props) => {
|
||||
return <ComboboxSingle options={countries} {...props} />;
|
||||
return <Combobox defaultOpen 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: "",
|
||||
@@ -110,21 +113,17 @@ 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<ComboboxSingle.Props<T>>) {
|
||||
>(props: Partial<ComboboxProps<T>>) {
|
||||
return (
|
||||
<ComboboxSingle
|
||||
<Combobox
|
||||
options={countries as T[]}
|
||||
defaultValue="opt-1"
|
||||
defaultSelectedOptions={["opt-1"]}
|
||||
label="Velg land"
|
||||
description="Landet hvor du er født."
|
||||
multiselect={false}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
@@ -134,23 +133,20 @@ export const Small = () => <BasicCombobox size="small" error="Error" />;
|
||||
|
||||
export const Hidelabel = () => <BasicCombobox hideLabel />;
|
||||
|
||||
export const Readonly = () => <BasicCombobox readOnly error="Test" />;
|
||||
export const Readonly = () => <BasicCombobox readOnly />;
|
||||
|
||||
export const Disabled = () => <BasicCombobox disabled error="Test" />;
|
||||
export const Disabled = () => <BasicCombobox disabled />;
|
||||
|
||||
export const ErrorStory = () => (
|
||||
<VStack gap="space-16">
|
||||
<BasicCombobox error="Du må velge et land." defaultValue={undefined} />
|
||||
<BasicCombobox error="Ugyldig land." />
|
||||
</VStack>
|
||||
);
|
||||
export const ErrorStory = () => <BasicCombobox error="Du må velge et land." />;
|
||||
ErrorStory.storyName = "Error";
|
||||
|
||||
export const DefaultOpen = () => <BasicCombobox defaultOpen />;
|
||||
DefaultOpen.storyName = "DefaultOpen";
|
||||
|
||||
export const DefaultValue = () => <BasicCombobox defaultValue="opt-1" />;
|
||||
DefaultValue.storyName = "DefaultValue";
|
||||
export const DefaultSelectedOptions = () => (
|
||||
<BasicCombobox defaultSelectedOptions={["opt-1"]} />
|
||||
);
|
||||
DefaultSelectedOptions.storyName = "DefaultSelectedOptions";
|
||||
|
||||
export const Name = () => (
|
||||
<form
|
||||
@@ -168,19 +164,50 @@ export const Name = () => (
|
||||
);
|
||||
|
||||
export const Controlled = () => {
|
||||
const [value, setValue] = useState<MyOption["value"] | undefined>("opt-1");
|
||||
const [selectedOptions, setSelectedOptions] = useState<MyOption["value"][]>([
|
||||
"opt-1",
|
||||
]);
|
||||
|
||||
return (
|
||||
<VStack gap="space-8" minWidth="250px">
|
||||
<ComboboxSingle
|
||||
<Combobox
|
||||
options={countries}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
selectedOptions={selectedOptions}
|
||||
onToggleOption={(option, newSelected) => {
|
||||
setSelectedOptions((prev) =>
|
||||
newSelected
|
||||
? [...prev, option.value]
|
||||
: prev.filter((v) => v !== option.value),
|
||||
);
|
||||
}}
|
||||
label="Velg land"
|
||||
/>
|
||||
Selected: {value}
|
||||
Selected: {selectedOptions.join(", ")}
|
||||
<div>
|
||||
<button type="button" onClick={() => setValue(undefined)}>
|
||||
<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([])}>
|
||||
Clear
|
||||
</button>
|
||||
</div>
|
||||
@@ -211,7 +238,7 @@ export const ManyOptions: StoryFn<ManyOptionsProps> = ({ count }) => {
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "400px", width: "250px" }}>
|
||||
<ComboboxSingle options={manyOptions} label="Test" />
|
||||
<Combobox options={manyOptions} label="Test" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -246,7 +273,7 @@ export const LongLabels = () => (
|
||||
],
|
||||
},
|
||||
]}
|
||||
value={["opt-1"]}
|
||||
selectedOptions={["opt-1"]}
|
||||
onToggleOption={() => {}}
|
||||
>
|
||||
<ComboboxField />
|
||||
@@ -258,13 +285,15 @@ export const LongLabels = () => (
|
||||
);
|
||||
|
||||
export const Composition = () => {
|
||||
const [value, setSelectedOptions] = useState<MyOption["value"][]>(["opt-1"]);
|
||||
const [selectedOptions, setSelectedOptions] = useState<MyOption["value"][]>([
|
||||
"opt-1",
|
||||
]);
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState<MyOption>(countries[0]);
|
||||
|
||||
const rootProps = {
|
||||
options: countries,
|
||||
value,
|
||||
selectedOptions,
|
||||
onToggleOption: (option) => {
|
||||
setSelectedOptions((prev) =>
|
||||
prev.includes(option.value)
|
||||
@@ -289,7 +318,7 @@ export const Composition = () => {
|
||||
</ComboboxRoot>
|
||||
|
||||
<ComboboxRoot {...rootProps}>
|
||||
<ComboboxTrigger openWhenStartsTyping={false}>
|
||||
<ComboboxTrigger>
|
||||
<Button>Uten input</Button>
|
||||
</ComboboxTrigger>
|
||||
<ComboboxOverlay>
|
||||
@@ -301,11 +330,11 @@ export const Composition = () => {
|
||||
|
||||
<ComboboxRoot
|
||||
options={countries}
|
||||
value={[selectedOption.value]}
|
||||
selectedOptions={[selectedOption.value]}
|
||||
onToggleOption={setSelectedOption}
|
||||
multiple={false}
|
||||
multiselect={false}
|
||||
>
|
||||
<ComboboxTrigger openWhenStartsTyping={false}>
|
||||
<ComboboxTrigger>
|
||||
<Button>Single select</Button>
|
||||
</ComboboxTrigger>
|
||||
<ComboboxOverlay>
|
||||
@@ -332,9 +361,9 @@ export const Composition = () => {
|
||||
|
||||
<ComboboxRoot
|
||||
options={countries}
|
||||
value={[selectedOption.value]}
|
||||
selectedOptions={[selectedOption.value]}
|
||||
onToggleOption={setSelectedOption}
|
||||
multiple={false}
|
||||
multiselect={false}
|
||||
>
|
||||
<ComboboxLabel htmlFor="demo2">Velg land (single select)</ComboboxLabel>
|
||||
<ComboboxTrigger id="demo2">
|
||||
@@ -349,7 +378,7 @@ export const Composition = () => {
|
||||
</ComboboxRoot>
|
||||
|
||||
<ComboboxRoot {...rootProps}>
|
||||
<ComboboxLabel htmlFor="demo3">Velg land (multiple)</ComboboxLabel>
|
||||
<ComboboxLabel htmlFor="demo3">Velg land (multiselect)</ComboboxLabel>
|
||||
<ComboboxTrigger id="demo3">
|
||||
<ComboboxField />
|
||||
</ComboboxTrigger>
|
||||
@@ -389,45 +418,77 @@ export const InDialog = () => {
|
||||
);
|
||||
};
|
||||
|
||||
function WithValidation() {
|
||||
const [error, setError] = useState("Du må velge et land");
|
||||
export const Testing = () => {
|
||||
return (
|
||||
<BasicCombobox
|
||||
options={nordicCountries}
|
||||
defaultValue={undefined}
|
||||
error={error}
|
||||
onChange={(newVal) => {
|
||||
if (!newVal) {
|
||||
setError("Du må velge et land");
|
||||
return;
|
||||
}
|
||||
setError(newVal === "opt-5" ? "" : "Du må velge Island");
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export const ScreenreaderTesting = () => (
|
||||
<div>
|
||||
<h2>Med grupper</h2>
|
||||
<BasicCombobox options={groupedCountries} defaultValue={undefined} />
|
||||
/* TODO:
|
||||
- Mobil: Vurder å ikke fokusere søkefelt automatisk.
|
||||
- Mobil: Vurder å ikke ha virtuelt fokus i det hele tatt hvis man ikke har tastatur
|
||||
Kan kanskje bruke media query, ev. ikke gi noe fokus før man begynner å trykke (som dagens CB. Sjekk hvordan SR takler at ingenting har virtuelt fokus.)
|
||||
Sjekk om det er mulig å flytte virtuelt fokus uten tastatur (typ med skjermleser).
|
||||
- Vurder om fokus skal låses til søkefelt (mest aktuelt ved multiselect).
|
||||
- Åpne på pil ned (og ev. opp)?
|
||||
- Vurder funksjoner fra gamle CB (ikke brukt: dropp, brukt lite: muliggjør med komposisjon, brukt mye: bygg inn støtte)
|
||||
- allowNewValues (er dette ofte egentlig Autocomplete?)
|
||||
- isLoading
|
||||
- maxSelected
|
||||
- Skal den hete noe annet enn Combobox?
|
||||
- Vurder om Label og Description (og error?) skal være sub-komponenter eller ikke.
|
||||
- Følge Combobox-pattern (mer)? Kan ikke følge det slavisk uansett.
|
||||
Pil opp og ned velger
|
||||
Ikke loop
|
||||
- A11y-sjekk (skjermleser, zoom, høykontrast...)
|
||||
- Beslutningsloggen?
|
||||
|
||||
<h2>Uten grupper</h2>
|
||||
<BasicCombobox options={nordicCountries} defaultValue={undefined} />
|
||||
|
||||
<h2>Label skjult visuelt</h2>
|
||||
<BasicCombobox options={nordicCountries} hideLabel />
|
||||
Utfordringer med komposisjon:
|
||||
- Vanskelig å bruke
|
||||
- Utfordrende for oss å endre (ref FormSummary)
|
||||
- Context er litt magisk/uoversiktlig
|
||||
- Description: Hvordan skal root/trigger vite at aria-describedby skal settes?
|
||||
Forslag: Tilby enkeltkomponent for de vanligste tilfellene, men også subkomponentene for fleksibilitet.
|
||||
Kan ev. ha slot/render-props for enkelte ting.
|
||||
Kan ev. bruke children for å kunne bytte ut/skreddersy innholdet i popup.
|
||||
|
||||
<h2>Med feilmelding</h2>
|
||||
<WithValidation />
|
||||
|
||||
<h2>Readonly med verdi</h2>
|
||||
<BasicCombobox readOnly />
|
||||
|
||||
<h2>Readonly uten verdi</h2>
|
||||
<BasicCombobox readOnly defaultValue={undefined} />
|
||||
|
||||
<h2>Disabled</h2>
|
||||
<BasicCombobox disabled />
|
||||
</div>
|
||||
);
|
||||
*/
|
||||
@@ -1,7 +1,7 @@
|
||||
import type { Meta } from "@storybook/react-vite";
|
||||
import React, { useState } from "react";
|
||||
import { expect, userEvent, within } from "storybook/test";
|
||||
import { ComboboxSingle } from "./Combobox";
|
||||
import { Combobox, type ComboboxProps } from "./Combobox";
|
||||
import { ComboboxFilter } from "./filter/ComboboxFilter";
|
||||
import { ComboboxList } from "./list/ComboboxList";
|
||||
import { ComboboxPopup } from "./popup/ComboboxPopup";
|
||||
@@ -59,14 +59,20 @@ const groupedOptions = [
|
||||
|
||||
function BaseCombobox<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
>(props: Partial<ComboboxSingle.Props<T>>) {
|
||||
const [value, setValue] = useState<string | undefined>("option-1");
|
||||
>(props: Partial<ComboboxProps<T>>) {
|
||||
const [selectedOptions, setSelectedOptions] = useState(["option-1"]);
|
||||
|
||||
return (
|
||||
<ComboboxSingle
|
||||
<Combobox
|
||||
options={countries as T[]}
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
selectedOptions={selectedOptions}
|
||||
onToggleOption={(option, newSelected) => {
|
||||
setSelectedOptions((prev) =>
|
||||
newSelected
|
||||
? [...prev, option.value]
|
||||
: prev.filter((v) => v !== option.value),
|
||||
);
|
||||
}}
|
||||
label="Velg land"
|
||||
description="Landet hvor du er født."
|
||||
{...props}
|
||||
@@ -93,7 +99,7 @@ const optionMemoTestRenderFn = (
|
||||
return optOrGroup.label;
|
||||
};
|
||||
export const OptionMemoization = () => {
|
||||
const [value, setValue] = useState(["option-1"]);
|
||||
const [selectedOptions, setSelectedOptions] = useState(["option-1"]);
|
||||
|
||||
return (
|
||||
<div style={{ minHeight: "450px" }}>
|
||||
@@ -106,9 +112,9 @@ export const OptionMemoization = () => {
|
||||
<ComboboxRoot
|
||||
defaultOpen
|
||||
options={groupedOptions}
|
||||
value={value}
|
||||
selectedOptions={selectedOptions}
|
||||
onToggleOption={(option) => {
|
||||
setValue((prev) =>
|
||||
setSelectedOptions((prev) =>
|
||||
prev.includes(option.value)
|
||||
? prev.filter((v) => v !== option.value)
|
||||
: [...prev, option.value],
|
||||
|
||||
Loaded 100 of 398 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user