Compare commits

..
Author SHA1 Message Date
Ken bd27b68072 fix(stylelint): Support stylelint 18 import 2026-11-09 11:34:16 +01:00
213 changed files with 2768 additions and 998 deletions

No files matched your search

-5
View File
@@ -1,5 +0,0 @@
---
"@navikt/ds-react": patch
---
DataGrid: Faster rendering when many details panels are expanded.
-5
View File
@@ -1,5 +0,0 @@
---
"@navikt/ds-react": patch
---
DataGrid.Table: Keyboard navigation no longer rebuilds the internal grid map on every arrow key press, improving responsiveness in large tables.
-5
View File
@@ -1,5 +0,0 @@
---
"@navikt/ds-react": patch
---
Combobox: Ignore option selection when an open Combobox becomes disabled.
-5
View File
@@ -1,5 +0,0 @@
---
"@navikt/ds-react": patch
---
Timeline: Optimize period parsing.
+5
View File
@@ -0,0 +1,5 @@
---
"@navikt/aksel-stylelint": patch
---
Stylelint: `@navikt/aksel-stylelint/recommended` now references its plugin by explicit file path, fixing `Could not find "."` errors with Stylelint 18.
+3 -3
View File
@@ -21,7 +21,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 5, Oxlint 1 (with ESLint plugins as JS plugins), Stylelint 17, Prettier 3 (formatting). Node 22+, Yarn 4.18.0.
- Stack: TypeScript 6, React 19 (dev), Next.js 16, Storybook 10, Vitest 4, Oxlint 1 + 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
@@ -44,7 +44,7 @@
## Commands
- `corepack yarn oxlint --deny-warnings <files...>` / `corepack yarn stylelint <files...>`
- `corepack yarn oxlint --deny-warnings <files...>` / `corepack yarn eslint --max-warnings=0 --no-warn-ignored <files...>` (both: oxlint runs most rules, ESLint the rest) / `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)
@@ -63,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: `oxlint.config.mts`, `stylelint.config.mjs`, `vitest.config.mts`, `.storybook/`
- `.changeset/` - release notes · Root config: `.oxlintrc.json`, `eslint.config.js`, `stylelint.config.mjs`, `biome.json`, `vitest.config.mts`, `.storybook/`
## Skills and agents
+22
View File
@@ -15,13 +15,24 @@ updates:
registries:
- npm-github
groups:
next:
applies-to: version-updates
patterns:
- "@next/eslint-plugin-next"
- "next"
sanity:
applies-to: version-updates
patterns:
- "sanity"
- "sanity-*"
- "@sanity/*"
- "next-sanity"
- "@portabletext/*"
eslint:
applies-to: version-updates
patterns:
- "eslint"
- "@eslint/*"
react:
applies-to: version-updates
patterns:
@@ -37,13 +48,24 @@ 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"
sanity-security:
applies-to: security-updates
patterns:
- "sanity"
- "sanity-*"
- "@sanity/*"
- "next-sanity"
- "@portabletext/*"
eslint-security:
applies-to: security-updates
patterns:
- "eslint"
- "@eslint/*"
react-security:
applies-to: security-updates
patterns:
@@ -1,11 +1,11 @@
---
description: "JS/TS conventions not enforced by 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, import order and `import type` are handled by Prettier. Don't hand-tune them.
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 }`).
+1 -1
View File
@@ -9,7 +9,7 @@ applyTo: "@navikt/aksel/**"
- Every transform is registered in `src/migrations-config.ts` (`value`, `path` without extension, `description`, `ignoredExtensions`). `value` must be unique and is the public CLI name (`v8-accordion-variant`).
- Test with fixtures: `tests/<fixture>.input.js` → `tests/<fixture>.output.js`, run via `check(__dirname, { fixture, migration })` from `src/codemod/utils/check.ts`. Output is compared after Prettier, so whitespace doesn't matter.
- Reuse helpers in `src/codemod/utils/` (imports, props, JSX values, token translation) before writing new AST code.
- Fixtures (`*.input.*`, `*.output.*`) are excluded from Oxlint and TypeScript. Don't "fix" them to match lint rules.
- Fixtures (`*.input.*`, `*.output.*`) are excluded from ESLint and TypeScript. Don't "fix" them to match lint rules.
- `src/version.ts` is generated by `build`. Don't edit it.
- `tests/migrations.test.ts` checks `dist/`. Run `corepack yarn workspace @navikt/aksel build` before `test`, or new transforms fail with "transform-path" errors.
- Consumer docs (`README.md`, website) show `npx @navikt/aksel ...` on purpose. Keep them.
@@ -11,7 +11,7 @@ For changes to component state, callbacks, async behavior, composition, semantic
## Compatibility (why: peer range is `react ^17 || ^18 || ^19`)
- No React 18/19-only APIs. Oxlint `no-restricted-imports` blocks e.g. `React.useId`. Use the internal `useId`, `useClientLayoutEffect`, etc. from `utils-external/`.
- No React 18/19-only APIs. ESLint `no-restricted-imports` blocks e.g. `React.useId`. Use the internal `useId`, `useClientLayoutEffect`, etc. from `utils-external/`.
- Import React explicitly in TSX (`import React from "react"`). `tsconfig` uses classic `jsx: "react"`.
- Component `index.ts` starts with `"use client"`.
+1 -1
View File
@@ -8,7 +8,7 @@ applyTo: "aksel.nav.no/website/**"
- Both routers are in use: `app/` (App Router, `cacheComponents: true` in `next.config.ts`) and `pages/` (examples, templates, legacy). Stay in the router already used nearby. Don't move routes between them.
- In `app/`, dynamic data needs Suspense or `"use cache"`, following nearby code. Cache Components errors at build time are real failures.
- For website build, runtime, or performance issues, use the `website-debugging` skill to establish the cause before changing code.
- `pages/eksempler/**` and `pages/templates/**` are public code snippets. Use the `website-example` skill. Oxlint rules `aksel-local/{import-check,comment-check,args-check}` enforce their shape.
- `pages/eksempler/**` and `pages/templates/**` are public code snippets. Use the `website-example` skill. ESLint rules `aksel-local/{import-check,comment-check,args-check}` enforce their shape.
- Reuse `@/` utils and components nearby before adding abstractions.
- Examples/templates: `corepack yarn storybook:aksel` (no secrets). `corepack yarn dev` only for app behavior; it needs `SANITY_READ_NO_DRAFTS` and `NPM_AUTH_TOKEN`.
- Never run `update:*`, `backup` or other Sanity-writing scripts unless asked. They write to the CMS.
@@ -19,7 +19,7 @@ Validate the smallest relevant surface with repository-pinned tools. Expand only
- Do not add a dependency only to run a one-off check. If the task truly needs new tooling, add and lock it explicitly.
- Public consumer docs may intentionally show `npx @navikt/aksel`. Preserve those examples unless the task changes consumer setup.
- If Corepack/Yarn reports a user-level configuration error, stop and report it. Never edit configuration outside the repository.
- Do not claim type safety from Oxlint or tests. Run a TypeScript-aware command when TypeScript behavior changed.
- Do not claim type safety from ESLint or tests. Run a TypeScript-aware command when TypeScript behavior changed.
## 1. Determine affected projects
@@ -58,7 +58,9 @@ Group only matching files:
```sh
corepack yarn oxlint --deny-warnings <changed .ts/.tsx/.js/.jsx files>
corepack yarn eslint --max-warnings=0 --no-warn-ignored <changed .ts/.tsx/.js/.jsx files>
corepack yarn stylelint <changed .css 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.
+1 -1
View File
@@ -41,7 +41,7 @@ For changes to component state, callbacks, async behavior, composition, semantic
- 🟡 **Suggestion**: should fix. Missing test, docs or changeset, maintainability risk, over-editing.
- 💭 **Nit**: optional. Leave it out when in doubt.
Skip anything Oxlint, Stylelint, 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
+3 -2
View File
@@ -4,7 +4,7 @@ description: >
Add or update a code example or template shown on aksel.nav.no
(aksel.nav.no/website/pages/eksempler/** or pages/templates/**). Use when
asked to create a component example, demo, usage snippet or page template
for the documentation site, or when an example fails the aksel-local Oxlint
for the documentation site, or when an example fails the aksel-local ESLint
rules or validate:eksempler.
argument-hint: "Component group and example name, e.g. button/loading"
---
@@ -55,6 +55,7 @@ Hard rules (enforced by `tooling/eslint` rules `aksel-local/*`):
```sh
corepack yarn oxlint --deny-warnings <file>
corepack yarn eslint --max-warnings=0 --no-warn-ignored <file>
corepack yarn workspace website validate:eksempler
corepack yarn storybook:aksel # visual check at :6007, needs no secrets
```
@@ -63,4 +64,4 @@ Never run `update:eksempler`. It writes to the Sanity CMS. Tell the user that a
## Done when
Oxlint and `validate:eksempler` pass, and the story renders in `storybook:aksel`.
ESLint and `validate:eksempler` pass, and the story renders in `storybook:aksel`.
+1 -1
View File
@@ -17,7 +17,7 @@ permissions:
jobs:
lint:
name: Lint (Oxlint, Stylelint etc.)
name: Lint (Oxlint, ESLint, Stylelint etc.)
runs-on: ubuntu-latest
steps:
- name: Checkout code
+467
View File
@@ -0,0 +1,467 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["react", "import"],
"categories": {
"correctness": "off"
},
"settings": {
"react": {
"version": "19.3.1"
}
},
"env": {
"builtin": true,
"es2026": true,
"browser": true,
"node": true
},
"ignorePatterns": [
"**/public",
"@navikt/aksel/**/*.input.*",
"@navikt/aksel/**/*.output.*",
"**/codemod/**/*.js",
"**/tokens/**/plugin.js",
"**/query-types.ts",
".yarn",
"**/next-env.d.ts",
"@navikt/aksel-icons/src/**",
"@navikt/aksel/src/version.ts",
"@navikt/core/tailwind/{src/tokens.ts,tailwind.config.js,tailwind.darkside.config.js}",
"@navikt/core/tokens/token_docs.js",
"apps/playroom/{playroom.config.js,src/auto-generated-imports.ts}",
"**/.build",
"**/storybook-static",
"**/test-results",
"tooling/analyzer/temp",
"apps/figma-icon-plugin/src/ui/vite-env.d.ts"
],
"rules": {
"constructor-super": "error",
"for-direction": "error",
"getter-return": "error",
"no-async-promise-executor": "error",
"no-case-declarations": "error",
"no-class-assign": "error",
"no-compare-neg-zero": "error",
"no-cond-assign": "error",
"no-const-assign": "error",
"no-constant-binary-expression": "error",
"no-constant-condition": "error",
"no-control-regex": "error",
"no-debugger": "error",
"no-delete-var": "error",
"no-dupe-class-members": "error",
"no-dupe-else-if": "error",
"no-dupe-keys": "error",
"no-duplicate-case": "error",
"no-empty": "error",
"no-empty-character-class": "error",
"no-empty-pattern": "error",
"no-empty-static-block": "error",
"no-ex-assign": "error",
"no-extra-boolean-cast": "error",
"no-fallthrough": "error",
"no-func-assign": "error",
"no-global-assign": "error",
"no-import-assign": "error",
"no-invalid-regexp": "error",
"no-irregular-whitespace": "error",
"no-loss-of-precision": "error",
"no-misleading-character-class": "error",
"no-new-native-nonconstructor": "error",
"no-nonoctal-decimal-escape": "error",
"no-obj-calls": "error",
"no-prototype-builtins": "error",
"no-redeclare": "error",
"no-regex-spaces": "error",
"no-self-assign": "error",
"no-setter-return": "error",
"no-shadow-restricted-names": "error",
"no-sparse-arrays": "error",
"no-this-before-super": "error",
"no-unassigned-vars": "error",
"no-unexpected-multiline": "error",
"no-unreachable": "error",
"no-unsafe-finally": "error",
"no-unsafe-negation": "error",
"no-unsafe-optional-chaining": "error",
"no-unused-labels": "error",
"no-unused-private-class-members": "error",
"no-unused-vars": "error",
"no-useless-backreference": "error",
"no-useless-catch": "error",
"no-useless-escape": "error",
"no-with": "error",
"preserve-caught-error": "error",
"require-yield": "error",
"use-isnan": "error",
"valid-typeof": "error",
"react/jsx-key": "error",
"react/jsx-no-comment-textnodes": "error",
"react/jsx-no-duplicate-props": "error",
"react/jsx-no-target-blank": "error",
"react/jsx-no-undef": "error",
"react/no-children-prop": "error",
"react/no-danger-with-children": "error",
"react/no-direct-mutation-state": "error",
"react/no-find-dom-node": "error",
"react/no-is-mounted": "error",
"react/no-render-return-value": "error",
"react/no-string-refs": "error",
"react/no-unescaped-entities": "error",
"react/no-unknown-property": "error",
"react/react-in-jsx-scope": "error",
"import/default": "error",
"import/no-named-as-default-member": "warn",
"import/no-duplicates": "warn",
"react/jsx-curly-brace-presence": [
"error",
{
"propElementValues": "always"
}
],
"react/rules-of-hooks": "error",
"react/exhaustive-deps": [
"warn",
{
"additionalHooks": "(useClientLayoutEffect)"
}
]
},
"overrides": [
{
"files": ["aksel.nav.no/sanity-studio/**"],
"rules": {
"react/react-in-jsx-scope": "off"
}
},
{
"files": [
"**/*.stories.{ts,tsx,js,jsx,mjs,cjs}",
"**/*.story.{ts,tsx,js,jsx,mjs,cjs}"
],
"rules": {
"storybook/await-interactions": "error",
"storybook/context-in-play-function": "error",
"storybook/default-exports": "error",
"storybook/hierarchy-separator": "warn",
"storybook/no-redundant-story-name": "warn",
"storybook/no-renderer-packages": "error",
"storybook/prefer-pascal-case": "warn",
"storybook/story-exports": "error",
"storybook/use-storybook-expect": "error",
"storybook/use-storybook-testing-library": "error",
"import/no-anonymous-default-export": "off",
"react/rules-of-hooks": "off"
},
"jsPlugins": ["eslint-plugin-storybook"]
},
{
"files": ["**/.storybook/main.{js,cjs,mjs,ts}"],
"rules": {
"storybook/no-uninstalled-addons": "error"
},
"jsPlugins": ["eslint-plugin-storybook"]
},
{
"files": ["**/*.{ts,tsx}"],
"rules": {
"constructor-super": "off",
"getter-return": "off",
"no-class-assign": "off",
"no-const-assign": "off",
"no-dupe-class-members": "off",
"no-dupe-keys": "off",
"no-func-assign": "off",
"no-import-assign": "off",
"no-new-native-nonconstructor": "off",
"no-obj-calls": "off",
"no-redeclare": "off",
"no-setter-return": "off",
"no-this-before-super": "off",
"no-unreachable": "off",
"no-unsafe-negation": "off",
"no-with": "off",
"no-var": "error",
"prefer-const": "error",
"prefer-rest-params": "error",
"prefer-spread": "error",
"no-array-constructor": "error",
"no-unused-expressions": [
"error",
{
"allowShortCircuit": true,
"allowTernary": true
}
],
"typescript/ban-ts-comment": "error",
"typescript/no-duplicate-enum-values": "error",
"typescript/no-empty-object-type": "error",
"typescript/no-explicit-any": "off",
"typescript/no-extra-non-null-assertion": "error",
"typescript/no-misused-new": "error",
"typescript/no-namespace": "error",
"typescript/no-non-null-asserted-optional-chain": "error",
"typescript/no-require-imports": "error",
"typescript/no-this-alias": "error",
"typescript/no-unnecessary-type-constraint": "error",
"typescript/no-unsafe-declaration-merging": "error",
"typescript/no-unsafe-function-type": "error",
"typescript/no-wrapper-object-types": "error",
"typescript/prefer-as-const": "error",
"typescript/prefer-namespace-keyword": "error",
"typescript/triple-slash-reference": "error",
"import/default": "off",
"import/no-named-as-default-member": "off"
},
"plugins": ["typescript"]
},
{
"files": ["**/*.test.*", "**/__tests__/*"],
"rules": {
"testing-library/await-async-events": [
"error",
{
"eventModule": "userEvent"
}
],
"testing-library/await-async-queries": "error",
"testing-library/await-async-utils": "error",
"testing-library/no-await-sync-events": [
"error",
{
"eventModules": ["fire-event"]
}
],
"testing-library/no-await-sync-queries": "error",
"testing-library/no-container": "error",
"testing-library/no-debugging-utils": "warn",
"testing-library/no-dom-import": ["error", "react"],
"testing-library/no-global-regexp-flag-in-query": "error",
"testing-library/no-manual-cleanup": "error",
"testing-library/no-node-access": "error",
"testing-library/no-promise-in-fire-event": "error",
"testing-library/no-render-in-lifecycle": "error",
"testing-library/no-unnecessary-act": "error",
"testing-library/no-wait-for-multiple-assertions": "error",
"testing-library/no-wait-for-side-effects": "error",
"testing-library/no-wait-for-snapshot": "error",
"testing-library/prefer-find-by": "error",
"testing-library/prefer-presence-queries": "error",
"testing-library/prefer-query-by-disappearance": "error",
"testing-library/prefer-screen-queries": "error",
"testing-library/render-result-naming-convention": "error",
"vitest/expect-expect": "error",
"vitest/no-commented-out-tests": "error",
"vitest/no-disabled-tests": "warn",
"vitest/no-focused-tests": "error",
"vitest/no-identical-title": "error",
"vitest/no-import-node-test": "error",
"vitest/no-interpolation-in-snapshots": "error",
"vitest/no-mocks-import": "error",
"vitest/no-standalone-expect": "error",
"vitest/no-unneeded-async-expect-function": "error",
"vitest/prefer-called-exactly-once-with": "error",
"vitest/require-local-test-context-for-concurrent-snapshots": "error",
"vitest/valid-describe-callback": "error",
"vitest/valid-expect": "error",
"vitest/valid-expect-in-promise": "error",
"vitest/valid-title": "error"
},
"jsPlugins": ["eslint-plugin-testing-library"],
"plugins": ["vitest"]
},
{
"files": ["aksel.nav.no/website/**"],
"rules": {
"react/no-unknown-property": [
"error",
{
"ignore": ["jsx", "global"]
}
],
"react/react-in-jsx-scope": "off",
"nextjs/google-font-display": "warn",
"nextjs/google-font-preconnect": "warn",
"nextjs/next-script-for-ga": "warn",
"nextjs/no-async-client-component": "warn",
"nextjs/no-before-interactive-script-outside-document": "warn",
"nextjs/no-css-tags": "warn",
"nextjs/no-head-element": "warn",
"nextjs/no-img-element": "warn",
"nextjs/no-page-custom-font": "warn",
"nextjs/no-styled-jsx-in-document": "warn",
"nextjs/no-sync-scripts": "warn",
"nextjs/no-title-in-document-head": "warn",
"nextjs/no-typos": "warn",
"nextjs/no-unwanted-polyfillio": "warn",
"nextjs/inline-script-id": "error",
"nextjs/no-assign-module-variable": "error",
"nextjs/no-document-import-in-page": "error",
"nextjs/no-duplicate-head": "error",
"nextjs/no-head-import-in-document": "error",
"nextjs/no-script-component-in-head": "error"
},
"plugins": ["nextjs"]
},
{
"files": ["aksel.nav.no/website/pages/eksempler/**"],
"rules": {
"nextjs/no-img-element": "off"
},
"plugins": ["nextjs"]
},
{
"files": [
"aksel.nav.no/website/pages/eksempler/**/*.tsx",
"aksel.nav.no/website/pages/templates/**/*.tsx"
],
"rules": {
"aksel-local/args-check": ["error"],
"aksel-local/comment-check": ["error"]
},
"jsPlugins": ["eslint-plugin-aksel-local"]
},
{
"files": [
"**/examples/__parts*/*.tsx",
"**/pages/templates/**/*.tsx",
"**/pages/eksempler/**/*.tsx"
],
"rules": {
"aksel-local/import-check": ["error"]
},
"jsPlugins": ["eslint-plugin-aksel-local"]
},
{
"files": ["**/examples/__parts-inline/*.tsx"],
"rules": {
"arrow-body-style": ["error", "never"],
"func-style": ["error", "expression"],
"import/no-named-export": "error"
}
},
{
"files": ["@navikt/**"],
"rules": {
"no-restricted-imports": [
"error",
{
"paths": [
{
"name": "react",
"importNames": [
"useId",
"useInsertionEffect",
"useSyncExternalStore",
"useDeferredValue",
"useTransition",
"startTransition"
],
"message": "React 18+ API not allowed. We currently only support React-features accesible in React 17. To add new features, we will need to update peerDependencies (breaking change)."
},
{
"name": "react",
"importNames": [
"useOptimistic",
"useActionState",
"use",
"cache",
"useEffectEvent",
"ViewTransition",
"addTransitionType"
],
"message": "React 19+ API not allowed. We currently only support React-features accesible in React 17. To add new features, we will need to update peerDependencies (breaking change)."
},
{
"name": "react-dom/client",
"importNames": ["createRoot", "hydrateRoot"],
"message": "React 18+ API not allowed (targeting React 17)."
},
{
"name": "react-dom",
"importNames": ["browser"],
"message": "React 19+ API not allowed (targeting React 17)."
}
],
"patterns": [
{
"group": ["fs", "path"],
"message": "Use `node:` prefix when importing native node modules, e.g. `import path from \"node:path\"`."
},
{
"group": ["**/esm/**", "**/cjs/**"],
"message": "Do not import from build output directories. Import from source instead."
}
]
}
],
"no-restricted-properties": [
"error",
{
"object": "React",
"property": "useId",
"message": "React 18+ API not allowed (targeting React 17)."
},
{
"object": "React",
"property": "useInsertionEffect",
"message": "React 18+ API not allowed (targeting React 17)."
},
{
"object": "React",
"property": "useSyncExternalStore",
"message": "React 18+ API not allowed (targeting React 17)."
},
{
"object": "React",
"property": "useDeferredValue",
"message": "React 18+ API not allowed (targeting React 17)."
},
{
"object": "React",
"property": "useTransition",
"message": "React 18+ API not allowed (targeting React 17)."
},
{
"object": "React",
"property": "startTransition",
"message": "React 18+ API not allowed (targeting React 17)."
},
{
"object": "React",
"property": "useOptimistic",
"message": "React 19+ API not allowed (targeting React 17)."
},
{
"object": "React",
"property": "useActionState",
"message": "React 19+ API not allowed (targeting React 17)."
},
{
"object": "React",
"property": "use",
"message": "React 19+ API not allowed (targeting React 17)."
},
{
"object": "React",
"property": "cache",
"message": "React 19+ API not allowed (targeting React 17)."
},
{
"object": "React",
"property": "useEffectEvent",
"message": "React 19+ API not allowed (targeting React 17)."
}
]
}
},
{
"files": ["@navikt/**/*.stories.tsx"],
"rules": {
"no-restricted-imports": "off",
"no-restricted-properties": "off"
}
}
]
}
+2
View File
@@ -1,7 +1,9 @@
{
"recommendations": [
"oxc.oxc-vscode",
"dbaeumer.vscode-eslint",
"stylelint.vscode-stylelint",
"biomejs.biome",
"esbenp.prettier-vscode",
"denoland.vscode-deno",
"github.vscode-github-actions",
+1
View File
@@ -1,5 +1,6 @@
{
"cssVariables.lookupFiles": ["@navikt/core/tokens/dist/tokens.css"],
"eslint.problems.shortenToSingleLine": true,
"editor.defaultFormatter": "esbenp.prettier-vscode",
"files.watcherExclude": {
"**/.yarn/**": true,
-1
View File
@@ -1,4 +1,3 @@
// oxlint-disable-next-line typescript/no-require-imports
const template = require("./config/template");
module.exports = {
+1 -1
View File
@@ -1,7 +1,7 @@
/**
* Fikser https://github.com/navikt/aksel/issues/1758
*/
// oxlint-disable-next-line typescript/no-require-imports
const fs = require("node:fs");
const basePath = "./dist/react/esm";
-1
View File
@@ -1,4 +1,3 @@
// oxlint-disable typescript/no-require-imports
const fastglob = require("fast-glob");
const kebabCase = require("lodash/kebabCase");
const path = require("node:path");
@@ -1,4 +1,3 @@
// oxlint-disable-next-line typescript/no-require-imports
const path = require("node:path");
const defaultIndexTemplate = (paths) => {
+1 -1
View File
@@ -1,4 +1,3 @@
// oxlint-disable typescript/no-require-imports
const fastglob = require("fast-glob");
const path = require("node:path");
const { load, JSON_SCHEMA } = require("js-yaml");
@@ -16,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
View File
@@ -1,4 +1,3 @@
// oxlint-disable typescript/no-require-imports
const fastglob = require("fast-glob");
const path = require("node:path");
const {
-1
View File
@@ -1,4 +1,3 @@
// oxlint-disable typescript/no-require-imports
const AdmZip = require("adm-zip");
const {
existsSync,
+2 -2
View File
@@ -88,11 +88,11 @@
"hast-util-select": "6.0.4",
"js-yaml": "^5.5.0",
"lodash": "4.18.1",
"prettier": "3.9.10",
"prettier": "3.9.6",
"react": "19.3.1",
"rehype-parse": "9.0.1",
"svgo": "3.3.5",
"tsc-alias": "1.9.10",
"tsc-alias": "1.9.7",
"tsx": "^4.23.15",
"typescript": "6.0.3",
"unified": "11.0.5",
+3 -3
View File
@@ -1,4 +1,4 @@
/* oxlint-disable react/rules-of-hooks -- `useId` is invariant at runtime */
/* 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
@@ -14,7 +14,7 @@ function useGlobalId(idOverride?: string): string | undefined {
// We can't use it server-side.
// If you want to use random values please consider the Birthday Problem: https://en.wikipedia.org/wiki/Birthday_problem
globalId += 1;
setDefaultId(`aksel-icon-${globalId}`);
setDefaultId(`aksel-icon-${globalId}`); // eslint-disable-line react-hooks/set-state-in-effect
}
}, [defaultId]);
return id;
@@ -34,6 +34,6 @@ export function useId(idOverride?: string): string | undefined {
const reactId = maybeReactUseId();
return idOverride ?? reactId.replace(/(:)/g, "");
}
// oxlint-disable-next-line react/rules-of-hooks -- `useId` is invariant at runtime.
// eslint-disable-next-line react-hooks/rules-of-hooks -- `useId` is invariant at runtime.
return useGlobalId(idOverride) ?? "";
}
+1 -1
View File
@@ -1,5 +1,5 @@
export default {
plugins: ["."],
plugins: ["./index.js"],
rules: {
"aksel/design-token-exists": true,
"aksel/no-internal-tokens": true,
@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return (
<SpeechBubble illustration={<Illustration />}>
@@ -1,3 +1,4 @@
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return (
<SpeechBubble avatar={<Illustration />}>
@@ -1,5 +1,6 @@
import { SpeechBubble, Button } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return (
<SpeechBubble
@@ -1,5 +1,6 @@
import { Chat, Button } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return (
<Chat
@@ -1,5 +1,6 @@
import { Chat, Button } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return (
<Chat
@@ -1,5 +1,6 @@
import { Chat, Button } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return (
<Chat
@@ -1,4 +1,5 @@
import { SpeechBubble, Button } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return (
<div>
@@ -1,4 +1,5 @@
import { Chat, Button } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return (
<div>
@@ -1,4 +1,5 @@
import { SpeechBubble as DsBubble, Button } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return (
<div>
@@ -1,4 +1,5 @@
import { Chat as DsBubble, Button } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return (
<div>
@@ -1,5 +1,6 @@
import { CopyToClipboard as DsCpyButton } from "@navikt/ds-react-internal";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return <DsCpyButton></DsCpyButton>;
};
@@ -1,5 +1,6 @@
import { CopyButton } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return <CopyButton />;
};
@@ -1,5 +1,6 @@
import { CopyToClipboard } from "@navikt/ds-react-internal";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return (
<CopyToClipboard
@@ -1,5 +1,6 @@
import { CopyButton } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return (
<CopyButton copyText="Text to copy" size="medium" text="text">
@@ -1,5 +1,6 @@
import { CopyToClipboard } from "@navikt/ds-react-internal";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return <CopyToClipboard></CopyToClipboard>;
};
@@ -1,5 +1,6 @@
import { CopyButton } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return <CopyButton />;
};
@@ -1,5 +1,6 @@
import { CopyToClipboard } from "@navikt/ds-react-internal";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return (
<CopyToClipboard
@@ -1,5 +1,6 @@
import { CopyButton } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return <CopyButton copyText="Text to copy" size="medium" text="text" />;
};
@@ -1,5 +1,6 @@
import { CopyButton } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return <CopyButton copyText="Text to copy" size="medium" text={props.text} />;
};
@@ -1,5 +1,6 @@
import { CopyButton } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return <CopyButton copyText="Text to copy" size="medium" text={props.text} />;
};
@@ -1,6 +1,7 @@
import { CopyToClipboard, Dropdown } from "@navikt/ds-react-internal";
import { Button } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return <CopyToClipboard></CopyToClipboard>;
};
@@ -1,6 +1,7 @@
import { Dropdown } from "@navikt/ds-react-internal";
import { Button, CopyButton } from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const Page = () => {
return <CopyButton />;
};
@@ -6,6 +6,7 @@ import {
UNSAFE_useRangeDatepicker,
} from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const UseDatepicker = () => {
const { datepickerProps, inputProps } = UNSAFE_useDatepicker({
fromDate: new Date("Aug 23 2019"),
@@ -6,6 +6,7 @@ import {
useRangeDatepicker,
} from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const UseDatepicker = () => {
const { datepickerProps, inputProps } = useDatepicker({
fromDate: new Date("Aug 23 2019"),
@@ -6,6 +6,7 @@ import {
useRangeDatepicker,
} from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const UseDatepicker = () => {
const { datepickerProps, inputProps } = useDatepicker({
fromDate: new Date("Aug 23 2019"),
@@ -6,6 +6,7 @@ import {
useRangeDatepicker,
} from "@navikt/ds-react";
/* eslint-disable react/jsx-no-undef */
export const UseDatepicker = () => {
const { datepickerProps, inputProps } = useDatepicker({
fromDate: new Date("Aug 23 2019"),
@@ -37,7 +37,7 @@ describe("getJSXLiteralValue", () => {
});
test("returns null for TemplateLiteral with expressions", () => {
// oxlint-disable-next-line 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 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 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
View File
@@ -1,4 +1,3 @@
// oxlint-disable-next-line typescript/no-require-imports
const packageJson = require("../package.json");
/* Make package-version accessible for github-workflow */
+2
View File
@@ -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;
}
+1 -1
View File
@@ -811,7 +811,7 @@
"react-dom": "19.3.1",
"rimraf": "6.1.3",
"swr": "^2.4.2",
"tsc-alias": "1.9.10",
"tsc-alias": "1.9.7",
"tsx": "^4.23.15",
"typescript": "6.0.3",
"vitest": "5.0.5"
@@ -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}
+2 -2
View File
@@ -107,14 +107,14 @@ function normalizeCount(value: number, fallback: number) {
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback;
}
// oxlint-disable-next-line typescript/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Badge {
export type Props = BadgeProps;
}
/** @deprecated Badge is no longer in preview. Import it from `@navikt/ds-react` instead. */
const BadgeDeprecated = Badge;
// oxlint-disable-next-line typescript/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace BadgeDeprecated {
export type Props = BadgeProps;
}
@@ -43,7 +43,6 @@ const ChipsRemovable = forwardRef<HTMLButtonElement, ChipsRemovableProps>(
data-color={color ?? variantToColor(variant)}
{...rest}
ref={ref}
// oxlint-disable-next-line react/button-has-type
type={type}
className={cl(
"aksel-chips__chip aksel-chips__removable aksel-chips--icon-right",
@@ -109,17 +109,16 @@ const DataGridPreferencesRoot = forwardRef<
* with column labels from `columnDefinitions`. Falls back to all columns
* visible in definition order when `columnDisplay` is unset.
*/
const columnDisplay = tableSettings?.columnDisplay;
const columnEntries = useMemo((): DataGridPreferencesColumnDisplay[] => {
const display =
columnDisplay ??
tableSettings?.columnDisplay ??
columnDefinitions.map((col) => ({ id: col.id, visible: true }));
return display.flatMap(({ id, visible }) => {
const label = columnDefinitionMap.get(id);
return label ? [{ id, label, visible }] : [];
});
}, [columnDefinitionMap, columnDisplay, columnDefinitions]);
}, [columnDefinitionMap, tableSettings?.columnDisplay, columnDefinitions]);
const handleColumnsChange = useCallback(
(columns: DataGridPreferencesColumnDisplay[]) => {
@@ -222,7 +221,7 @@ function isFieldVisible(
return fields?.[key] !== false;
}
// oxlint-disable-next-line typescript/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace DataGridPreferencesRoot {
export type Props = DataGridPreferencesProps;
}
@@ -138,16 +138,16 @@ const DataGrid = DataGridInternal as unknown as DataGridComponent;
DataGrid.Table = DataGridTable;
DataGrid.Preferences = DataGridPreferences;
// oxlint-disable-next-line typescript/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace DataGrid {
export type Props<T = unknown> = DataGridProps<T>;
export type Columns<T = unknown> = ColumnDefinitions<T>;
export type Selection<T = unknown> = SelectionProps<T>;
export type Settings = DataGridSettings;
// oxlint-disable-next-line typescript/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Table {
// oxlint-disable-next-line 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;
@@ -157,9 +157,9 @@ export namespace DataGrid {
export type DetailsPanel<T = unknown> = DataGridTable.DetailsPanel<T>;
}
// oxlint-disable-next-line typescript/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace Preferences {
// oxlint-disable-next-line 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,
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from "@storybook/react-vite";
import React from "react";
import { expect, fn, userEvent, within } from "storybook/test";
import { expect, userEvent, within } from "storybook/test";
import { DataGrid } from "../../data-grid";
import type { ColumnDefinitions } from "../table/root/DataGridTable.types";
import { DataGridTable } from "../table/root/DataGridTableRoot";
@@ -228,78 +228,3 @@ export const SelectAllIncludesHiddenDescendantsForCollapsedParents: Story = {
expect(getCheckboxes(canvasElement)[3].checked).toBe(false);
},
};
const detailsPanelData: TestRow[] = [
{ id: "a", name: "Alpha" },
{ id: "b", name: "Beta" },
{ id: "c", name: "Gamma" },
];
function ControlledDetailsPanel({
onExpandedRowIdsChange,
}: {
onExpandedRowIdsChange: (ids: string[]) => void;
}) {
const [expandedRowIds, setExpandedRowIds] = React.useState(["a"]);
return (
<DataGrid
columns={columns}
data={detailsPanelData}
getRowId={(row) => row.id}
>
<DataGrid.Table<TestRow>
detailsPanel={{
getContent: (row) => `Detaljer for ${row.name}`,
isRowExpandable: (row) => row.id !== "c",
expandedRowIds,
onExpandedRowIdsChange: (ids) => {
onExpandedRowIdsChange(ids);
setExpandedRowIds(ids);
},
showExpandAll: true,
}}
/>
</DataGrid>
);
}
export const ControlledDetailsPanelExpansion: StoryObj<{
onExpandedRowIdsChange: ReturnType<typeof fn>;
}> = {
args: {
onExpandedRowIdsChange: fn(),
},
render: (props) => <ControlledDetailsPanel {...props} />,
play: async ({ canvasElement, args }) => {
args.onExpandedRowIdsChange.mockClear();
const canvas = within(canvasElement);
expect(canvas.getByText("Detaljer for Alpha")).toBeInTheDocument();
expect(canvas.queryByText("Detaljer for Beta")).not.toBeInTheDocument();
expect(
canvas.getByRole("button", { name: "Vis alle rader" }),
).toHaveAttribute("aria-expanded", "false");
await userEvent.click(canvas.getByRole("button", { name: "Vis detaljer" }));
expect(args.onExpandedRowIdsChange).toHaveBeenLastCalledWith(["a", "b"]);
expect(canvas.getByText("Detaljer for Beta")).toBeInTheDocument();
await userEvent.click(
canvas.getByRole("button", { name: "Skjul alle rader" }),
);
expect(args.onExpandedRowIdsChange).toHaveBeenLastCalledWith([]);
expect(canvas.queryByText("Detaljer for Alpha")).not.toBeInTheDocument();
expect(canvas.queryByText("Detaljer for Beta")).not.toBeInTheDocument();
await userEvent.click(
canvas.getByRole("button", { name: "Vis alle rader" }),
);
expect(args.onExpandedRowIdsChange).toHaveBeenLastCalledWith(["a", "b"]);
expect(canvas.queryByText("Detaljer for Gamma")).not.toBeInTheDocument();
expect(args.onExpandedRowIdsChange).toHaveBeenCalledTimes(3);
},
};
@@ -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) {
@@ -150,7 +151,7 @@ function useTableColumnResize({
setClampedWidth(pxToRelativeWidth(newColumnWidth));
}
},
[autoResizeOnce], // oxlint-disable-line react/exhaustive-deps
[autoResizeOnce], // eslint-disable-line react-hooks/exhaustive-deps
);
const handleOnClick: DOMAttributes<HTMLButtonElement>["onClick"] =
@@ -5,9 +5,7 @@ import { type GridCache, buildTableGridMap } from "../helpers/table-grid-nav";
/**
* Manages the table grid cache and observes DOM changes that require grid rebuilding.
* Watches for structural changes (rows/cells added/removed) and attribute changes
* (colspan, rowspan, hidden) that affect grid layout.
* Inline `style` is intentionally not observed: focus navigation writes
* scroll-margin on every move, and the grid never reads styles.
* (colspan, rowspan, hidden, style) that affect grid layout and focusability.
*/
function useGridCache(tableRef: HTMLTableElement | null, enabled: boolean) {
const gridCacheRef = useRef<GridCache>({
@@ -36,7 +34,7 @@ function useGridCache(tableRef: HTMLTableElement | null, enabled: boolean) {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ["colspan", "rowspan", "hidden"],
attributeFilter: ["colspan", "rowspan", "hidden", "style"],
});
return () => {
@@ -76,15 +76,10 @@ function DataTableDetailsPanelProvider<T>({
showExpandAll = false,
} = detailsPanel || {};
const controlledExpandedIds = React.useMemo(
() => (expandedRowIds ? new Set(expandedRowIds) : undefined),
[expandedRowIds],
);
const [expandedIds, setExpandedIds] = useControllableState<Set<string>>({
value: controlledExpandedIds,
defaultValue: () => new Set(defaultExpandedRowIds),
onChange: (ids) => onExpandedRowIdsChange?.(Array.from(ids)),
const [expandedIds, setExpandedIds] = useControllableState({
value: expandedRowIds,
defaultValue: defaultExpandedRowIds,
onChange: onExpandedRowIdsChange,
});
/* TODO: False is just fallback until auto and root is merged */
@@ -121,7 +116,8 @@ function DataTableDetailsPanelProvider<T>({
);
const isExpanded = useCallback(
(id: string) => isDetailsPanelExpandableById(id) && expandedIds.has(id),
(id: string) =>
isDetailsPanelExpandableById(id) && expandedIds.includes(id),
[expandedIds, isDetailsPanelExpandableById],
);
@@ -131,35 +127,21 @@ function DataTableDetailsPanelProvider<T>({
return;
}
setExpandedIds((currentExpandedIds) => {
const nextExpandedIds = new Set(currentExpandedIds);
if (nextExpandedIds.has(id)) {
nextExpandedIds.delete(id);
} else {
nextExpandedIds.add(id);
}
return nextExpandedIds;
});
setExpandedIds((currentExpandedIds) =>
currentExpandedIds.includes(id)
? currentExpandedIds.filter((expandedId) => expandedId !== id)
: [...currentExpandedIds, id],
);
},
[isDetailsPanelExpandableById, setExpandedIds],
);
const isAllExpanded = React.useMemo(() => {
if (expandableIds.size === 0) {
return false;
}
for (const id of expandableIds) {
if (!expandedIds.has(id)) {
return false;
}
}
return true;
}, [expandableIds, expandedIds]);
const isAllExpanded =
expandableIds.size > 0 &&
Array.from(expandableIds).every((key) => expandedIds.includes(key));
const toggleAll = useCallback(() => {
setExpandedIds(isAllExpanded ? new Set() : new Set(expandableIds));
setExpandedIds(isAllExpanded ? [] : Array.from(expandableIds));
}, [expandableIds, isAllExpanded, setExpandedIds]);
return (
@@ -599,7 +599,7 @@ const DataGridTable = DataGridTableInternal as <RowT>(
props: DataGridTableProps<RowT> & React.RefAttributes<HTMLTableElement>,
) => React.ReactElement | null;
// oxlint-disable-next-line typescript/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace DataGridTable {
export type Props<T = unknown> = DataGridTableProps<T>;
export type Sorting = TableSortOptions;
@@ -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);
@@ -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;
@@ -112,7 +112,6 @@ export const ControlledMonthPreservesFocus: Story = {
);
return (
// oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
<fieldset
onKeyDown={(event) => {
if (event.key.toLowerCase() === "j") {
@@ -49,7 +49,6 @@ const DatePickerDayButton = ({
"rdp-day_outside": modifiers.outside,
"rdp-day__weekend": modifiers.weekend,
})}
type="button"
>
<div className="rdp-day__inner">{children}</div>
</button>
@@ -142,7 +142,6 @@ export const useDatepicker = (
locale: _locale,
required,
defaultSelected: _defaultSelected,
// oxlint-disable-next-line react/purity -- Defaults `today` to the current date.
today = new Date(),
fromDate,
toDate,
@@ -220,7 +220,6 @@ export const useRangeDatepicker = (
const {
locale: _locale,
defaultSelected: _defaultSelected,
// oxlint-disable-next-line react/purity -- Defaults `today` to the current date.
today = new Date(),
fromDate,
toDate,
@@ -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">
@@ -32,7 +32,7 @@ const CheckboxCard = forwardRef<HTMLInputElement, CheckboxCardProps>(
},
);
// oxlint-disable-next-line typescript/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace CheckboxCard {
export type Props = CheckboxCardProps;
}
@@ -28,7 +28,7 @@ const CheckboxCardGroup = forwardRef<
/>
));
// oxlint-disable-next-line typescript/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace CheckboxCardGroup {
export type Props = CheckboxCardGroupProps;
}
@@ -26,7 +26,8 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
props;
return (
// oxlint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- clickable div for checkbox card
// biome-ignore lint/a11y/noStaticElementInteractions: clickable div for checkbox card
// biome-ignore lint/a11y/useKeyWithClickEvents: clickable div for checkbox card
<div
className={cl(className, "aksel-checkbox", `aksel-checkbox--${size}`, {
"aksel-checkbox--error": hasError,
@@ -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"
tabIndex={-1}
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}
@@ -26,9 +26,9 @@ export const InputController = forwardRef<
>
>((props, ref) => {
const {
// oxlint-disable-next-line typescript/no-unused-vars -- Remove when prop has been removed from ComboboxProps.
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Remove when prop has been removed from ComboboxProps.
clearButton,
// oxlint-disable-next-line typescript/no-unused-vars -- Remove when prop has been removed from ComboboxProps.
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Remove when prop has been removed from ComboboxProps.
clearButtonLabel,
toggleListButton = true,
inputClassName,
@@ -54,7 +54,8 @@ export const InputController = forwardRef<
return (
<Floating.Anchor asChild ref={setAnchorRef}>
{/* oxlint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- 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":
@@ -37,12 +37,7 @@ const SelectedOptionsProvider = ({
"allowNewValues" | "isMultiSelect" | "onToggleSelected" | "maxSelected"
> & { options: ComboboxOption[]; selectedOptions?: ComboboxOption[] };
}) => {
const {
clearInput,
focusInput,
setHideCaret,
inputProps: { disabled },
} = useInputContext();
const { clearInput, focusInput, setHideCaret } = useInputContext();
const {
customOptions,
removeCustomOption,
@@ -117,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]);
@@ -126,9 +122,6 @@ const SelectedOptionsProvider = ({
option: ComboboxOption,
event: React.KeyboardEvent | React.PointerEvent,
) => {
if (disabled) {
return;
}
if (isInList(option.value, selectedOptions)) {
removeSelectedOption(option);
} else if (isMultiSelect && isLimitReached) {
@@ -142,7 +135,6 @@ const SelectedOptionsProvider = ({
[
addSelectedOption,
clearInput,
disabled,
focusInput,
removeSelectedOption,
selectedOptions,
@@ -56,26 +56,6 @@ describe("Render combobox", () => {
});
});
test("ignores option selection after the open Combobox becomes disabled", async () => {
const onToggleSelected = vi.fn();
const props = { options, selectedOptions: ["apple"], onToggleSelected };
const { rerender } = render(<App {...props} />);
const input = screen.getByRole("combobox");
await userEvent.click(input);
rerender(<App {...props} disabled />);
expect(input).toBeDisabled();
await userEvent.click(
await screen.findByRole("option", { name: "banana" }),
);
expect(onToggleSelected).not.toHaveBeenCalled();
rerender(<App {...props} />);
await userEvent.click(
await screen.findByRole("option", { name: "banana" }),
);
expect(onToggleSelected).toHaveBeenCalledWith("banana", true, false);
});
describe("Combobox state-handling", () => {
test("Should show loading icon when loading (used for async search)", async () => {
render(<App options={[]} isListOpen isLoading />);
@@ -216,7 +216,7 @@ const ComboboxSingle = forwardRef(
props: ComboboxSingleProps<T> & React.RefAttributes<HTMLButtonElement>,
) => React.ReactElement;
// oxlint-disable-next-line typescript/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ComboboxSingle {
export type Props<
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
@@ -283,7 +283,7 @@ function ComboboxMulti<
);
}
// oxlint-disable-next-line typescript/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace ComboboxMulti {
export type Props<
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
@@ -28,7 +28,6 @@ const ComboboxField = forwardRef<HTMLButtonElement, ComboboxFieldProps>(
const [ariaLabel, setAriaLabel] = useState<string>();
useEffect(() => {
if (label) {
// oxlint-disable-next-line react/set-state-in-effect -- Briefly sets and resets the label as part of the VoiceOver workaround.
setAriaLabel(label);
setTimeout(() => setAriaLabel(undefined), 100);
}
@@ -86,7 +86,7 @@ function announce(message: string, politeness: Politeness = "polite") {
}
// If the message is the same as the current text,
// append a non-breaking space to force a re-announcement.
target.textContent = message === target.textContent ? `${message} ` : message;
target.textContent = message === target.textContent ? `${message} ` : message; // eslint-disable-line no-irregular-whitespace
scheduleClear(politeness, target);
}
@@ -9,6 +9,7 @@ function highlightSubstring(text: string, substring: string) {
const [rangeStart, rangeEnd] = range;
return (
// Aria-label is used to fix testing-library wrongly evaluating the accessible name of the option when highlighting text
// biome-ignore lint/a11y/useAriaPropsSupportedByRole: Doesn't matter if it doesn't work in normal browsers
<span aria-label={text}>
{text.substring(0, rangeStart)}
<mark>{text.substring(rangeStart, rangeEnd)}</mark>
@@ -1,5 +1,4 @@
import React, { useRef, useState } from "react";
import { useClientLayoutEffect } from "../../../utils-external";
import React, { useLayoutEffect, useRef, useState } from "react";
import { Listbox } from "../../../utils/components/Listbox/root/ListboxRoot";
import { createStrictContext } from "../../../utils/helpers";
import { useComboboxRootContext } from "../root/ComboboxRoot";
@@ -30,7 +29,7 @@ export const ComboboxPopup = ({ children }: ComboboxPopupProps) => {
const rootContext = useComboboxRootContext();
const ref = useRef<HTMLDivElement | null>(null);
useClientLayoutEffect(
useLayoutEffect(
function onOpen() {
const canHover = window.matchMedia("(hover: hover)").matches;
@@ -28,7 +28,6 @@ const ComboboxTrigger = ({
return (
<Floating.Anchor asChild ref={rootContext.triggerRef}>
<Slot
// oxlint-disable-next-line jsx-a11y/role-has-required-aria-props -- aria-controls is not really needed
role="combobox"
aria-expanded={readOnly ? undefined : rootContext.open}
onClick={() => !readOnly && rootContext.setOpen(!rootContext.open)}
@@ -85,6 +85,7 @@ export const ErrorSummary = forwardRef<HTMLDivElement, ErrorSummaryProps>(
const mergedRef = useMergeRefs(ref, wrapperRef);
return (
// biome-ignore lint/a11y/noStaticElementInteractions: Allows focus-calls on div to move focus to heading.
<div
ref={mergedRef}
{...rest}
@@ -90,7 +90,8 @@ const FileUploadDropzone = forwardRef<
{description}
</BodyShort>
)}
{/* oxlint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- Keyboard will access input/button within */}
{/** biome-ignore lint/a11y/noStaticElementInteractions: Assistive technologies will use the input within. */}
{/** biome-ignore lint/a11y/useKeyWithClickEvents: Keyboard will access input/button within */}
<div
className="aksel-dropzone__area"
onDragEnter={dropzoneCtx.onDragEnter}
@@ -1,23 +1,26 @@
import { isAcceptedSize } from "./is-accepted-size";
const createFile = () =>
new File(["a".repeat(4999)], "filename.txt", { type: "text/plain" });
describe("isAcceptedSize", () => {
it("should return true if maxSize is less than or equal to 0", () => {
const file = createFile();
const file = new File([new Array(5000).join("a")], "filename.txt", {
type: "text/plain",
});
expect(isAcceptedSize(file, -1)).toBe(true);
expect(isAcceptedSize(file, 0)).toBe(true);
});
it("should return true if file size is less than or equal to maxSize", () => {
const file = createFile();
const file = new File([new Array(5000).join("a")], "filename.txt", {
type: "text/plain",
});
expect(isAcceptedSize(file, 5000)).toBe(true);
expect(isAcceptedSize(file, 6000)).toBe(true);
});
it("should return false if file size is greater than maxSize", () => {
const file = createFile();
const file = new File([new Array(5000).join("a")], "filename.txt", {
type: "text/plain",
});
expect(isAcceptedSize(file, 4000)).toBe(false);
});
});
@@ -32,7 +32,7 @@ const RadioCard = forwardRef<HTMLInputElement, RadioCardProps>(
},
);
// oxlint-disable-next-line typescript/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace RadioCard {
export type Props = RadioCardProps;
}
@@ -25,7 +25,7 @@ const RadioCardGroup = forwardRef<HTMLFieldSetElement, RadioCardGroupProps>(
),
);
// oxlint-disable-next-line typescript/no-namespace
// eslint-disable-next-line @typescript-eslint/no-namespace
export namespace RadioCardGroup {
export type Props = RadioCardGroupProps;
}
Loaded 100 of 213 files, more files were not shown because too many files have changed in this diff. Show more