mirror of
https://github.com/navikt/aksel.git
synced 2026-11-10 06:44:04 +00:00
Compare commits
15
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ceba3aa4de | ||
|
|
45519d52fc | ||
|
|
05a6734eaa | ||
|
|
2edeaf4446 | ||
|
|
820af7c17c | ||
|
|
137d3c2d51 | ||
|
|
b4f4bb5c11 | ||
|
|
a1317dc3df | ||
|
|
5e90726eb7 | ||
|
|
9bef84205e | ||
|
|
7a49867e77 | ||
|
|
0ee13d82d7 | ||
|
|
18a1eda79c | ||
|
|
12ff585b3e | ||
|
|
3390f0f230 |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/ds-react": patch
|
||||
---
|
||||
|
||||
DataGrid: Faster rendering when many details panels are expanded.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@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.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/ds-react": patch
|
||||
---
|
||||
|
||||
Combobox: Ignore option selection when an open Combobox becomes disabled.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/ds-react": patch
|
||||
---
|
||||
|
||||
Timeline: Optimize period parsing.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@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.
|
||||
@@ -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 4, Oxlint 1 + ESLint 9, Stylelint 17, Biome 2 (lint only), Prettier 3 (formatting). Node 22+, Yarn 4.18.0.
|
||||
- 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.
|
||||
- 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 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 oxlint --deny-warnings <files...>` / `corepack yarn stylelint <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: `.oxlintrc.json`, `eslint.config.js`, `stylelint.config.mjs`, `biome.json`, `vitest.config.mts`, `.storybook/`
|
||||
- `.changeset/` - release notes · Root config: `oxlint.config.mts`, `stylelint.config.mjs`, `vitest.config.mts`, `.storybook/`
|
||||
|
||||
## Skills and agents
|
||||
|
||||
|
||||
@@ -15,24 +15,13 @@ 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:
|
||||
@@ -48,24 +37,13 @@ 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 ESLint/Biome: loops, export style and placement."
|
||||
description: "JS/TS conventions not enforced by Oxlint: loops, export style and placement."
|
||||
applyTo: "**/*.ts,**/*.tsx,**/*.js,**/*.mts,**/*.mjs"
|
||||
---
|
||||
|
||||
# JS/TS conventions
|
||||
|
||||
Formatting, import order and `import type` are handled by Prettier and Biome. Don't hand-tune them.
|
||||
Formatting, import order and `import type` are handled by Prettier. 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 }`).
|
||||
|
||||
@@ -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 ESLint and TypeScript. Don't "fix" them to match lint rules.
|
||||
- Fixtures (`*.input.*`, `*.output.*`) are excluded from Oxlint 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. ESLint `no-restricted-imports` blocks e.g. `React.useId`. Use the internal `useId`, `useClientLayoutEffect`, etc. from `utils-external/`.
|
||||
- No React 18/19-only APIs. Oxlint `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"`.
|
||||
|
||||
|
||||
@@ -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. ESLint 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. Oxlint 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 ESLint or tests. Run a TypeScript-aware command when TypeScript behavior changed.
|
||||
- Do not claim type safety from Oxlint or tests. Run a TypeScript-aware command when TypeScript behavior changed.
|
||||
|
||||
## 1. Determine affected projects
|
||||
|
||||
@@ -58,9 +58,7 @@ 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.
|
||||
|
||||
@@ -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 ESLint, Stylelint, Biome, Prettier or TypeScript already catches. No praise-only or speculative comments. Every finding says **why** it matters and what to do.
|
||||
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.
|
||||
|
||||
## Checklist
|
||||
|
||||
|
||||
@@ -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 ESLint
|
||||
for the documentation site, or when an example fails the aksel-local Oxlint
|
||||
rules or validate:eksempler.
|
||||
argument-hint: "Component group and example name, e.g. button/loading"
|
||||
---
|
||||
@@ -55,7 +55,6 @@ 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
|
||||
```
|
||||
@@ -64,4 +63,4 @@ Never run `update:eksempler`. It writes to the Sanity CMS. Tell the user that a
|
||||
|
||||
## Done when
|
||||
|
||||
ESLint and `validate:eksempler` pass, and the story renders in `storybook:aksel`.
|
||||
Oxlint and `validate:eksempler` pass, and the story renders in `storybook:aksel`.
|
||||
@@ -17,7 +17,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint (Oxlint, ESLint, Stylelint etc.)
|
||||
name: Lint (Oxlint, Stylelint etc.)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
|
||||
-467
@@ -1,467 +0,0 @@
|
||||
{
|
||||
"$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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
-2
@@ -1,9 +1,7 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"oxc.oxc-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"stylelint.vscode-stylelint",
|
||||
"biomejs.biome",
|
||||
"esbenp.prettier-vscode",
|
||||
"denoland.vscode-deno",
|
||||
"github.vscode-github-actions",
|
||||
|
||||
Vendored
-1
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"cssVariables.lookupFiles": ["@navikt/core/tokens/dist/tokens.css"],
|
||||
"eslint.problems.shortenToSingleLine": true,
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"files.watcherExclude": {
|
||||
"**/.yarn/**": true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// oxlint-disable-next-line typescript/no-require-imports
|
||||
const template = require("./config/template");
|
||||
|
||||
module.exports = {
|
||||
|
||||
@@ -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,3 +1,4 @@
|
||||
// oxlint-disable typescript/no-require-imports
|
||||
const fastglob = require("fast-glob");
|
||||
const kebabCase = require("lodash/kebabCase");
|
||||
const path = require("node:path");
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// oxlint-disable-next-line typescript/no-require-imports
|
||||
const path = require("node:path");
|
||||
|
||||
const defaultIndexTemplate = (paths) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// oxlint-disable typescript/no-require-imports
|
||||
const fastglob = require("fast-glob");
|
||||
const path = require("node:path");
|
||||
const { load, JSON_SCHEMA } = require("js-yaml");
|
||||
@@ -15,7 +16,6 @@ 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 +1,4 @@
|
||||
// oxlint-disable typescript/no-require-imports
|
||||
const fastglob = require("fast-glob");
|
||||
const path = require("node:path");
|
||||
const {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// oxlint-disable typescript/no-require-imports
|
||||
const AdmZip = require("adm-zip");
|
||||
const {
|
||||
existsSync,
|
||||
|
||||
@@ -88,11 +88,11 @@
|
||||
"hast-util-select": "6.0.4",
|
||||
"js-yaml": "^5.5.0",
|
||||
"lodash": "4.18.1",
|
||||
"prettier": "3.9.6",
|
||||
"prettier": "3.9.10",
|
||||
"react": "19.3.1",
|
||||
"rehype-parse": "9.0.1",
|
||||
"svgo": "3.3.5",
|
||||
"tsc-alias": "1.9.7",
|
||||
"tsc-alias": "1.9.10",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "6.0.3",
|
||||
"unified": "11.0.5",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/* biome-ignore-all lint/correctness/useHookAtTopLevel: `useId` is invariant at runtime. */
|
||||
/* oxlint-disable react/rules-of-hooks -- `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}`); // eslint-disable-line react-hooks/set-state-in-effect
|
||||
setDefaultId(`aksel-icon-${globalId}`);
|
||||
}
|
||||
}, [defaultId]);
|
||||
return id;
|
||||
@@ -34,6 +34,6 @@ export function useId(idOverride?: string): string | undefined {
|
||||
const reactId = maybeReactUseId();
|
||||
return idOverride ?? reactId.replace(/(:)/g, "");
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/rules-of-hooks -- `useId` is invariant at runtime.
|
||||
// oxlint-disable-next-line react/rules-of-hooks -- `useId` is invariant at runtime.
|
||||
return useGlobalId(idOverride) ?? "";
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
export default {
|
||||
plugins: ["./index.js"],
|
||||
plugins: ["."],
|
||||
rules: {
|
||||
"aksel/design-token-exists": true,
|
||||
"aksel/no-internal-tokens": true,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return (
|
||||
<SpeechBubble illustration={<Illustration />}>
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return (
|
||||
<SpeechBubble avatar={<Illustration />}>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { SpeechBubble, Button } from "@navikt/ds-react";
|
||||
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return (
|
||||
<SpeechBubble
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Chat, Button } from "@navikt/ds-react";
|
||||
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return (
|
||||
<Chat
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Chat, Button } from "@navikt/ds-react";
|
||||
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return (
|
||||
<Chat
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { Chat, Button } from "@navikt/ds-react";
|
||||
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return (
|
||||
<Chat
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { SpeechBubble, Button } from "@navikt/ds-react";
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Chat, Button } from "@navikt/ds-react";
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { SpeechBubble as DsBubble, Button } from "@navikt/ds-react";
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Chat as DsBubble, Button } from "@navikt/ds-react";
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CopyToClipboard as DsCpyButton } from "@navikt/ds-react-internal";
|
||||
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return <DsCpyButton></DsCpyButton>;
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CopyButton } from "@navikt/ds-react";
|
||||
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return <CopyButton />;
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CopyToClipboard } from "@navikt/ds-react-internal";
|
||||
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return (
|
||||
<CopyToClipboard
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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,6 +1,5 @@
|
||||
import { CopyToClipboard } from "@navikt/ds-react-internal";
|
||||
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return <CopyToClipboard></CopyToClipboard>;
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CopyButton } from "@navikt/ds-react";
|
||||
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return <CopyButton />;
|
||||
};
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CopyToClipboard } from "@navikt/ds-react-internal";
|
||||
|
||||
/* eslint-disable react/jsx-no-undef */
|
||||
export const Page = () => {
|
||||
return (
|
||||
<CopyToClipboard
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
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,6 +1,5 @@
|
||||
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,5 @@
|
||||
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,7 +1,6 @@
|
||||
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,7 +1,6 @@
|
||||
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,7 +6,6 @@ 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,7 +6,6 @@ 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,7 +6,6 @@ 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,7 +6,6 @@ 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", () => {
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing only
|
||||
// oxlint-disable-next-line no-template-curly-in-string -- 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);
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing token match inside template string
|
||||
// oxlint-disable-next-line no-template-curly-in-string -- 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};"],
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing token inside template literal
|
||||
// oxlint-disable-next-line no-template-curly-in-string -- Testing token inside template literal
|
||||
["const a = `${ASurfaceDefault}`;", "const a = `${BgDefault}`;"],
|
||||
])("%s", (input, output) => {
|
||||
expect(runJs(`${importLine}\n${input}`)).toBe(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// oxlint-disable-next-line typescript/no-require-imports
|
||||
const packageJson = require("../package.json");
|
||||
|
||||
/* Make package-version accessible for github-workflow */
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -20,7 +19,6 @@
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
|
||||
@@ -811,7 +811,7 @@
|
||||
"react-dom": "19.3.1",
|
||||
"rimraf": "6.1.3",
|
||||
"swr": "^2.4.2",
|
||||
"tsc-alias": "1.9.7",
|
||||
"tsc-alias": "1.9.10",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "6.0.3",
|
||||
"vitest": "5.0.5"
|
||||
|
||||
@@ -16,10 +16,8 @@ const ActionMenuShortcut = ({ children }: ShortcutProps) => {
|
||||
<ActionMenuMarker placement="right">
|
||||
{parsed.map((char, index) => (
|
||||
<span
|
||||
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}`}
|
||||
// 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}`}
|
||||
className="aksel-action-menu__shortcut"
|
||||
>
|
||||
{char}
|
||||
|
||||
@@ -107,14 +107,14 @@ function normalizeCount(value: number, fallback: number) {
|
||||
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : fallback;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// oxlint-disable-next-line typescript/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;
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// oxlint-disable-next-line typescript/no-namespace
|
||||
namespace BadgeDeprecated {
|
||||
export type Props = BadgeProps;
|
||||
}
|
||||
|
||||
@@ -43,6 +43,7 @@ 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,16 +109,17 @@ 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 =
|
||||
tableSettings?.columnDisplay ??
|
||||
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, tableSettings?.columnDisplay, columnDefinitions]);
|
||||
}, [columnDefinitionMap, columnDisplay, columnDefinitions]);
|
||||
|
||||
const handleColumnsChange = useCallback(
|
||||
(columns: DataGridPreferencesColumnDisplay[]) => {
|
||||
@@ -221,7 +222,7 @@ function isFieldVisible(
|
||||
return fields?.[key] !== false;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// oxlint-disable-next-line typescript/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;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// oxlint-disable-next-line typescript/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;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// oxlint-disable-next-line typescript/no-namespace
|
||||
export namespace Table {
|
||||
// biome-ignore lint/suspicious/noShadow: intentional namespace re-export
|
||||
// oxlint-disable-next-line no-shadow -- 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>;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// oxlint-disable-next-line typescript/no-namespace
|
||||
export namespace Preferences {
|
||||
// biome-ignore lint/suspicious/noShadow: intentional namespace re-export
|
||||
// oxlint-disable-next-line no-shadow -- intentional namespace re-export
|
||||
export type Props = DataGridPreferences.Props;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
/** 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, userEvent, within } from "storybook/test";
|
||||
import { expect, fn, 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,3 +228,78 @@ 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,7 +139,6 @@ 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) {
|
||||
@@ -151,7 +150,7 @@ function useTableColumnResize({
|
||||
setClampedWidth(pxToRelativeWidth(newColumnWidth));
|
||||
}
|
||||
},
|
||||
[autoResizeOnce], // eslint-disable-line react-hooks/exhaustive-deps
|
||||
[autoResizeOnce], // oxlint-disable-line react/exhaustive-deps
|
||||
);
|
||||
|
||||
const handleOnClick: DOMAttributes<HTMLButtonElement>["onClick"] =
|
||||
|
||||
@@ -5,7 +5,9 @@ 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, style) that affect grid layout and focusability.
|
||||
* (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.
|
||||
*/
|
||||
function useGridCache(tableRef: HTMLTableElement | null, enabled: boolean) {
|
||||
const gridCacheRef = useRef<GridCache>({
|
||||
@@ -34,7 +36,7 @@ function useGridCache(tableRef: HTMLTableElement | null, enabled: boolean) {
|
||||
subtree: true,
|
||||
childList: true,
|
||||
attributes: true,
|
||||
attributeFilter: ["colspan", "rowspan", "hidden", "style"],
|
||||
attributeFilter: ["colspan", "rowspan", "hidden"],
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
||||
@@ -76,10 +76,15 @@ function DataTableDetailsPanelProvider<T>({
|
||||
showExpandAll = false,
|
||||
} = detailsPanel || {};
|
||||
|
||||
const [expandedIds, setExpandedIds] = useControllableState({
|
||||
value: expandedRowIds,
|
||||
defaultValue: defaultExpandedRowIds,
|
||||
onChange: onExpandedRowIdsChange,
|
||||
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)),
|
||||
});
|
||||
|
||||
/* TODO: False is just fallback until auto and root is merged */
|
||||
@@ -116,8 +121,7 @@ function DataTableDetailsPanelProvider<T>({
|
||||
);
|
||||
|
||||
const isExpanded = useCallback(
|
||||
(id: string) =>
|
||||
isDetailsPanelExpandableById(id) && expandedIds.includes(id),
|
||||
(id: string) => isDetailsPanelExpandableById(id) && expandedIds.has(id),
|
||||
[expandedIds, isDetailsPanelExpandableById],
|
||||
);
|
||||
|
||||
@@ -127,21 +131,35 @@ function DataTableDetailsPanelProvider<T>({
|
||||
return;
|
||||
}
|
||||
|
||||
setExpandedIds((currentExpandedIds) =>
|
||||
currentExpandedIds.includes(id)
|
||||
? currentExpandedIds.filter((expandedId) => expandedId !== id)
|
||||
: [...currentExpandedIds, id],
|
||||
);
|
||||
setExpandedIds((currentExpandedIds) => {
|
||||
const nextExpandedIds = new Set(currentExpandedIds);
|
||||
if (nextExpandedIds.has(id)) {
|
||||
nextExpandedIds.delete(id);
|
||||
} else {
|
||||
nextExpandedIds.add(id);
|
||||
}
|
||||
return nextExpandedIds;
|
||||
});
|
||||
},
|
||||
[isDetailsPanelExpandableById, setExpandedIds],
|
||||
);
|
||||
|
||||
const isAllExpanded =
|
||||
expandableIds.size > 0 &&
|
||||
Array.from(expandableIds).every((key) => expandedIds.includes(key));
|
||||
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 toggleAll = useCallback(() => {
|
||||
setExpandedIds(isAllExpanded ? [] : Array.from(expandableIds));
|
||||
setExpandedIds(isAllExpanded ? new Set() : new Set(expandableIds));
|
||||
}, [expandableIds, isAllExpanded, setExpandedIds]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -599,7 +599,7 @@ const DataGridTable = DataGridTableInternal as <RowT>(
|
||||
props: DataGridTableProps<RowT> & React.RefAttributes<HTMLTableElement>,
|
||||
) => React.ReactElement | null;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// oxlint-disable-next-line typescript/no-namespace
|
||||
export namespace DataGridTable {
|
||||
export type Props<T = unknown> = DataGridTableProps<T>;
|
||||
export type Sorting = TableSortOptions;
|
||||
|
||||
@@ -220,7 +220,6 @@ 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,16 +49,15 @@ 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,6 +112,7 @@ export const ControlledMonthPreservesFocus: Story = {
|
||||
);
|
||||
|
||||
return (
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions
|
||||
<fieldset
|
||||
onKeyDown={(event) => {
|
||||
if (event.key.toLowerCase() === "j") {
|
||||
|
||||
@@ -49,6 +49,7 @@ const DatePickerDayButton = ({
|
||||
"rdp-day_outside": modifiers.outside,
|
||||
"rdp-day__weekend": modifiers.weekend,
|
||||
})}
|
||||
type="button"
|
||||
>
|
||||
<div className="rdp-day__inner">{children}</div>
|
||||
</button>
|
||||
|
||||
@@ -142,6 +142,7 @@ 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,6 +220,7 @@ 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>
|
||||
{/** biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: role="grid" improves a11y reading */}
|
||||
{/* oxlint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role -- 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>(
|
||||
},
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// oxlint-disable-next-line typescript/no-namespace
|
||||
export namespace CheckboxCard {
|
||||
export type Props = CheckboxCardProps;
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ const CheckboxCardGroup = forwardRef<
|
||||
/>
|
||||
));
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// oxlint-disable-next-line typescript/no-namespace
|
||||
export namespace CheckboxCardGroup {
|
||||
export type Props = CheckboxCardGroupProps;
|
||||
}
|
||||
|
||||
@@ -26,8 +26,7 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
props;
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: clickable div for checkbox card
|
||||
// biome-ignore lint/a11y/useKeyWithClickEvents: clickable div for checkbox card
|
||||
// oxlint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- clickable div for checkbox card
|
||||
<div
|
||||
className={cl(className, "aksel-checkbox", `aksel-checkbox--${size}`, {
|
||||
"aksel-checkbox--error": hasError,
|
||||
|
||||
@@ -42,7 +42,6 @@ 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),
|
||||
},
|
||||
)}
|
||||
// biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: List item acts as an option in the combobox.
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role -- List item acts as an option in the combobox
|
||||
role="option"
|
||||
aria-selected={false}
|
||||
>
|
||||
|
||||
@@ -113,7 +113,7 @@ const FilteredOptions = () => {
|
||||
{shouldRenderFilteredOptionsList && (
|
||||
<ul
|
||||
ref={setFilteredOptionsRef}
|
||||
// biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: The <ul> element acts as a listbox in the combobox.
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role -- 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);
|
||||
}
|
||||
}}
|
||||
// biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: List item acts as an option in the combobox.
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role -- 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 {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Remove when prop has been removed from ComboboxProps.
|
||||
// oxlint-disable-next-line typescript/no-unused-vars -- Remove when prop has been removed from ComboboxProps.
|
||||
clearButton,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- Remove when prop has been removed from ComboboxProps.
|
||||
// oxlint-disable-next-line typescript/no-unused-vars -- Remove when prop has been removed from ComboboxProps.
|
||||
clearButtonLabel,
|
||||
toggleListButton = true,
|
||||
inputClassName,
|
||||
@@ -54,8 +54,7 @@ export const InputController = forwardRef<
|
||||
|
||||
return (
|
||||
<Floating.Anchor asChild ref={setAnchorRef}>
|
||||
{/** biome-ignore lint/a11y/noStaticElementInteractions: Acts as wrapper list-toggle and input */}
|
||||
{/** biome-ignore lint/a11y/useKeyWithClickEvents: key-events are handled by input itself */}
|
||||
{/* oxlint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- 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,7 +37,12 @@ const SelectedOptionsProvider = ({
|
||||
"allowNewValues" | "isMultiSelect" | "onToggleSelected" | "maxSelected"
|
||||
> & { options: ComboboxOption[]; selectedOptions?: ComboboxOption[] };
|
||||
}) => {
|
||||
const { clearInput, focusInput, setHideCaret } = useInputContext();
|
||||
const {
|
||||
clearInput,
|
||||
focusInput,
|
||||
setHideCaret,
|
||||
inputProps: { disabled },
|
||||
} = useInputContext();
|
||||
const {
|
||||
customOptions,
|
||||
removeCustomOption,
|
||||
@@ -112,7 +117,6 @@ 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]);
|
||||
@@ -122,6 +126,9 @@ const SelectedOptionsProvider = ({
|
||||
option: ComboboxOption,
|
||||
event: React.KeyboardEvent | React.PointerEvent,
|
||||
) => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
if (isInList(option.value, selectedOptions)) {
|
||||
removeSelectedOption(option);
|
||||
} else if (isMultiSelect && isLimitReached) {
|
||||
@@ -135,6 +142,7 @@ const SelectedOptionsProvider = ({
|
||||
[
|
||||
addSelectedOption,
|
||||
clearInput,
|
||||
disabled,
|
||||
focusInput,
|
||||
removeSelectedOption,
|
||||
selectedOptions,
|
||||
|
||||
@@ -56,6 +56,26 @@ 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;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// oxlint-disable-next-line typescript/no-namespace
|
||||
export namespace ComboboxSingle {
|
||||
export type Props<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
@@ -283,7 +283,7 @@ function ComboboxMulti<
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// oxlint-disable-next-line typescript/no-namespace
|
||||
export namespace ComboboxMulti {
|
||||
export type Props<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
|
||||
@@ -28,6 +28,7 @@ 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; // eslint-disable-line no-irregular-whitespace
|
||||
target.textContent = message === target.textContent ? `${message} ` : message;
|
||||
scheduleClear(politeness, target);
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ 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,4 +1,5 @@
|
||||
import React, { useLayoutEffect, useRef, useState } from "react";
|
||||
import React, { useRef, useState } from "react";
|
||||
import { useClientLayoutEffect } from "../../../utils-external";
|
||||
import { Listbox } from "../../../utils/components/Listbox/root/ListboxRoot";
|
||||
import { createStrictContext } from "../../../utils/helpers";
|
||||
import { useComboboxRootContext } from "../root/ComboboxRoot";
|
||||
@@ -29,7 +30,7 @@ export const ComboboxPopup = ({ children }: ComboboxPopupProps) => {
|
||||
const rootContext = useComboboxRootContext();
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
useLayoutEffect(
|
||||
useClientLayoutEffect(
|
||||
function onOpen() {
|
||||
const canHover = window.matchMedia("(hover: hover)").matches;
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ 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,7 +85,6 @@ 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,8 +90,7 @@ const FileUploadDropzone = forwardRef<
|
||||
{description}
|
||||
</BodyShort>
|
||||
)}
|
||||
{/** biome-ignore lint/a11y/noStaticElementInteractions: Assistive technologies will use the input within. */}
|
||||
{/** biome-ignore lint/a11y/useKeyWithClickEvents: Keyboard will access input/button within */}
|
||||
{/* oxlint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions -- Keyboard will access input/button within */}
|
||||
<div
|
||||
className="aksel-dropzone__area"
|
||||
onDragEnter={dropzoneCtx.onDragEnter}
|
||||
|
||||
@@ -1,26 +1,23 @@
|
||||
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 = new File([new Array(5000).join("a")], "filename.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
const file = createFile();
|
||||
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 = new File([new Array(5000).join("a")], "filename.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
const file = createFile();
|
||||
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 = new File([new Array(5000).join("a")], "filename.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
const file = createFile();
|
||||
expect(isAcceptedSize(file, 4000)).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -32,7 +32,7 @@ const RadioCard = forwardRef<HTMLInputElement, RadioCardProps>(
|
||||
},
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// oxlint-disable-next-line typescript/no-namespace
|
||||
export namespace RadioCard {
|
||||
export type Props = RadioCardProps;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ const RadioCardGroup = forwardRef<HTMLFieldSetElement, RadioCardGroupProps>(
|
||||
),
|
||||
);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
// oxlint-disable-next-line typescript/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
Reference in new issue
Block a user