mirror of
https://github.com/navikt/aksel.git
synced 2026-10-28 00:14:06 +00:00
Compare commits
3
Commits
experimental
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcdc1044ae | ||
|
|
1873e0f0a4 | ||
|
|
071ce07d4a |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/ds-react": patch
|
||||
---
|
||||
|
||||
DatePicker: Preserve keyboard focus when controlled month changes replace the focused day.
|
||||
@@ -22,7 +22,7 @@
|
||||
## Repo summary
|
||||
|
||||
- Nav's design system. Yarn 4 workspaces monorepo: published packages under `@navikt/` + docs site `aksel.nav.no/` + `apps/` + `tooling/`.
|
||||
- Stack: TypeScript 6, React 19 (dev), Next.js 16, Storybook 10, Vitest 4, ESLint 9, Stylelint 17, Oxlint (JS/TS lint), Prettier 3 (formatting). Node 22+, Yarn 4.18.0.
|
||||
- Stack: TypeScript 6, React 19 (dev), Next.js 16, Storybook 10, Vitest 4, ESLint 9, Stylelint 17, Biome 2 (lint only), Prettier 3 (formatting). Node 22+, Yarn 4.18.0.
|
||||
- Published `@navikt/*` packages must stay React 17 compatible (peer `react ^17 || ^18 || ^19`).
|
||||
|
||||
## Start and validate
|
||||
@@ -45,7 +45,7 @@
|
||||
|
||||
## Commands
|
||||
|
||||
- `corepack yarn eslint --max-warnings=0 --no-warn-ignored <files...>` / `corepack yarn stylelint <files...>` / `corepack yarn oxlint --deny-warnings <files...>`
|
||||
- `corepack yarn eslint --max-warnings=0 --no-warn-ignored <files...>` / `corepack yarn stylelint <files...>` / `corepack yarn biome lint --no-errors-on-unmatched <files...>`
|
||||
- `corepack yarn tsc --noEmit --incremental false --project <tsconfig>` - targeted type-check
|
||||
- `corepack yarn workspace <name> <script>` - e.g. `@navikt/ds-react test` (Vitest + tsc), `website test`
|
||||
- `corepack yarn lint` / `corepack yarn test` / `corepack yarn boot` - root suites (slow; cross-workspace changes only)
|
||||
@@ -64,7 +64,7 @@ Path-specific rules load automatically from `.github/instructions/` (see each `a
|
||||
- `@navikt/aksel/src/` - CLI and codemods
|
||||
- `aksel.nav.no/website/` - Next.js docs site · `aksel.nav.no/sanity-studio/` - CMS schema
|
||||
- `apps/playroom/`, `apps/mcp-server/`, `apps/figma-icon-plugin/` - apps · `tooling/` - ESLint plugin, scripts, changelog, analyzer
|
||||
- `.changeset/` - release notes · Root config: `eslint.config.js`, `stylelint.config.mjs`, `.oxlintrc.json`, `vitest.config.ts`, `.storybook/`
|
||||
- `.changeset/` - release notes · Root config: `eslint.config.js`, `stylelint.config.mjs`, `biome.json`, `vitest.config.ts`, `.storybook/`
|
||||
|
||||
## Skills and agents
|
||||
|
||||
@@ -90,4 +90,3 @@ Path-specific rules load automatically from `.github/instructions/` (see each `a
|
||||
- Preserve JSDoc on public props/components when changing public APIs.
|
||||
- Reuse helpers: `cl`, `composeEventHandlers`, `useId`, `omit`, `useClientLayoutEffect`, `useEventListener`.
|
||||
- No tests for trivial logic. Only tests for complex logic that has a higher risk of breaking.
|
||||
- Suppress Oxlint rules with `oxlint-disable-next-line <rule>` / `oxlint-disable <rule>`, never `eslint-disable` (breaks ESLint).
|
||||
@@ -1,13 +1,11 @@
|
||||
---
|
||||
description: "JS/TS conventions not enforced by ESLint/Oxlint: loops, export style and placement."
|
||||
description: "JS/TS conventions not enforced by ESLint/Biome: loops, export style and placement."
|
||||
applyTo: "**/*.ts,**/*.tsx,**/*.js,**/*.mts,**/*.mjs"
|
||||
---
|
||||
|
||||
# JS/TS conventions
|
||||
|
||||
Formatting and import order are handled by Prettier; linting by ESLint and Oxlint. Don't hand-tune formatting or import order.
|
||||
|
||||
Suppress Oxlint rules with `// oxlint-disable-next-line <rule>` (file-level: `/* oxlint-disable <rule> */`). Never use `eslint-disable` for Oxlint rules; ESLint reports unknown rules and unused directives.
|
||||
Formatting, import order and `import type` are handled by Prettier and Biome. Don't hand-tune them.
|
||||
|
||||
- Use `for...of` instead of `Array.forEach`. It supports `break`/`continue`/`await` and reads top-down.
|
||||
- Use named exports only. Keep type exports separate from value exports (`export { X }` / `export type { XProps }`).
|
||||
|
||||
@@ -59,7 +59,7 @@ Group only matching files:
|
||||
```sh
|
||||
corepack yarn eslint --max-warnings=0 --no-warn-ignored <changed .ts/.tsx/.js/.jsx files>
|
||||
corepack yarn stylelint <changed .css files>
|
||||
corepack yarn oxlint --deny-warnings <changed .ts/.tsx/.js/.jsx files>
|
||||
corepack yarn biome lint --no-errors-on-unmatched <changed supported source/config files>
|
||||
```
|
||||
|
||||
Do not pass unsupported file types to make a command appear comprehensive. Documentation-only changes need no code validation unless a documentation check exists.
|
||||
|
||||
@@ -26,7 +26,7 @@ argument-hint: "PR number, branch or files to review"
|
||||
- 🟡 **Suggestion**: should fix. Missing test, docs or changeset, maintainability risk, over-editing.
|
||||
- 💭 **Nit**: optional. Leave it out when in doubt.
|
||||
|
||||
Skip anything ESLint, Stylelint, Oxlint, Prettier or TypeScript already catches. No praise-only or speculative comments. Every finding says **why** it matters and what to do.
|
||||
Skip anything ESLint, Stylelint, Biome, Prettier or TypeScript already catches. No praise-only or speculative comments. Every finding says **why** it matters and what to do.
|
||||
|
||||
## Checklist
|
||||
|
||||
|
||||
-334
@@ -1,334 +0,0 @@
|
||||
{
|
||||
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||
"plugins": [
|
||||
"eslint",
|
||||
"typescript",
|
||||
"unicorn",
|
||||
"oxc",
|
||||
"react",
|
||||
"jsx-a11y",
|
||||
"import",
|
||||
"vitest"
|
||||
],
|
||||
"categories": {
|
||||
"correctness": "error"
|
||||
},
|
||||
"ignorePatterns": [
|
||||
"**/lib",
|
||||
"**/public",
|
||||
"**/esm",
|
||||
"**/cjs",
|
||||
"**/dist",
|
||||
"**/.next",
|
||||
"**/.build",
|
||||
"**/coverage",
|
||||
"**/storybook-static",
|
||||
"**/test-results",
|
||||
"**/playwright-report",
|
||||
"**/.sanity/runtime",
|
||||
"**/next-env.d.ts",
|
||||
"**/codemod/**/*.js",
|
||||
"**/transforms/**/*.input.*",
|
||||
"**/transforms/**/*.output.*",
|
||||
"@navikt/aksel/**/*.input.*",
|
||||
"@navikt/aksel/**/*.output.*",
|
||||
"**/modal/helpers/dialog-polyfill.ts",
|
||||
"**/query-types.ts",
|
||||
"**/component-names.generated.ts",
|
||||
"**/tokens/**/plugin.js",
|
||||
"@navikt/aksel-icons/src",
|
||||
"@navikt/aksel/src/version.ts",
|
||||
"@navikt/core/tailwind/src/tokens.ts",
|
||||
"@navikt/core/tailwind/tailwind.config.js",
|
||||
"@navikt/core/tailwind/tailwind.darkside.config.js",
|
||||
"@navikt/core/tokens/token_docs.js",
|
||||
"apps/playroom/playroom.config.js",
|
||||
"apps/playroom/src/auto-generated-imports.ts",
|
||||
"apps/figma-icon-plugin/src/ui/vite-env.d.ts",
|
||||
"tooling/analyzer/temp",
|
||||
".yarn"
|
||||
],
|
||||
"rules": {
|
||||
"jsx-a11y/alt-text": "error",
|
||||
"jsx-a11y/anchor-ambiguous-text": "error",
|
||||
"jsx-a11y/anchor-has-content": "error",
|
||||
"jsx-a11y/anchor-is-valid": "error",
|
||||
"jsx-a11y/aria-activedescendant-has-tabindex": "error",
|
||||
"jsx-a11y/aria-props": "error",
|
||||
"jsx-a11y/aria-proptypes": "error",
|
||||
"jsx-a11y/aria-role": "error",
|
||||
"jsx-a11y/aria-unsupported-elements": "error",
|
||||
"jsx-a11y/autocomplete-valid": "error",
|
||||
"jsx-a11y/click-events-have-key-events": "error",
|
||||
"jsx-a11y/heading-has-content": "error",
|
||||
"jsx-a11y/html-has-lang": "error",
|
||||
"jsx-a11y/iframe-has-title": "error",
|
||||
"jsx-a11y/img-redundant-alt": "error",
|
||||
"jsx-a11y/interactive-supports-focus": "error",
|
||||
"jsx-a11y/label-has-associated-control": "error",
|
||||
"jsx-a11y/lang": "error",
|
||||
"jsx-a11y/media-has-caption": "error",
|
||||
"jsx-a11y/mouse-events-have-key-events": "error",
|
||||
"jsx-a11y/no-access-key": "error",
|
||||
"jsx-a11y/no-aria-hidden-on-focusable": "error",
|
||||
"jsx-a11y/no-autofocus": "error",
|
||||
"jsx-a11y/no-distracting-elements": "error",
|
||||
"jsx-a11y/no-interactive-element-to-noninteractive-role": "error",
|
||||
"jsx-a11y/no-noninteractive-element-to-interactive-role": "error",
|
||||
"jsx-a11y/no-noninteractive-tabindex": "error",
|
||||
"jsx-a11y/no-redundant-roles": "error",
|
||||
"jsx-a11y/no-static-element-interactions": "error",
|
||||
"jsx-a11y/prefer-tag-over-role": "off",
|
||||
"jsx-a11y/role-has-required-aria-props": "error",
|
||||
"jsx-a11y/role-supports-aria-props": "error",
|
||||
"jsx-a11y/scope": "error",
|
||||
"jsx-a11y/tabindex-no-positive": "error",
|
||||
"react/button-has-type": "error",
|
||||
"eslint/no-extra-label": "warn",
|
||||
"eslint/no-lone-blocks": "warn",
|
||||
"eslint/no-sequences": "warn",
|
||||
"eslint/no-unneeded-ternary": "warn",
|
||||
"eslint/no-useless-computed-key": "warn",
|
||||
"eslint/no-useless-constructor": "warn",
|
||||
"eslint/no-useless-rename": "warn",
|
||||
"eslint/prefer-numeric-literals": "warn",
|
||||
"eslint/prefer-regex-literals": "warn",
|
||||
"eslint/prefer-rest-params": "warn",
|
||||
"react/jsx-no-useless-fragment": [
|
||||
"warn",
|
||||
{
|
||||
"allowExpressions": true
|
||||
}
|
||||
],
|
||||
"typescript/no-useless-empty-export": "warn",
|
||||
"unicorn/no-static-only-class": "warn",
|
||||
"unicorn/no-useless-switch-case": "warn",
|
||||
"unicorn/prefer-array-flat": "warn",
|
||||
"unicorn/prefer-array-flat-map": "warn",
|
||||
"unicorn/prefer-array-index-of": "warn",
|
||||
"unicorn/prefer-date-now": "warn",
|
||||
"eslint/no-constructor-return": "error",
|
||||
"eslint/no-inner-declarations": [
|
||||
"error",
|
||||
"both",
|
||||
{
|
||||
"blockScopedFunctions": "allow"
|
||||
}
|
||||
],
|
||||
"eslint/no-use-before-define": [
|
||||
"error",
|
||||
{
|
||||
"functions": false,
|
||||
"classes": false,
|
||||
"variables": false,
|
||||
"enums": false,
|
||||
"typedefs": false,
|
||||
"ignoreTypeReferences": true,
|
||||
"allowNamedExports": true
|
||||
}
|
||||
],
|
||||
"eslint/radix": "warn",
|
||||
"oxc/bad-min-max-func": "error",
|
||||
"react/void-dom-elements-no-children": "error",
|
||||
"import/namespace": "warn",
|
||||
"oxc/no-accumulating-spread": "warn",
|
||||
"eslint/no-eval": "error",
|
||||
"eslint/no-script-url": "error",
|
||||
"react/jsx-no-script-url": "error",
|
||||
"react/no-danger": "error",
|
||||
"eslint/prefer-const": "warn",
|
||||
"eslint/prefer-exponentiation-operator": "warn",
|
||||
"eslint/prefer-template": "warn",
|
||||
"typescript/consistent-type-imports": [
|
||||
"warn",
|
||||
{
|
||||
"disallowTypeAnnotations": false
|
||||
}
|
||||
],
|
||||
"typescript/no-non-null-assertion": "warn",
|
||||
"typescript/prefer-function-type": "warn",
|
||||
"typescript/prefer-literal-enum-member": "warn",
|
||||
"unicorn/prefer-node-protocol": "warn",
|
||||
"eslint/array-callback-return": [
|
||||
"error",
|
||||
{
|
||||
"checkForEach": true,
|
||||
"allowVoid": true
|
||||
}
|
||||
],
|
||||
"eslint/default-case-last": "warn",
|
||||
"eslint/eqeqeq": [
|
||||
"error",
|
||||
"always",
|
||||
{
|
||||
"null": "ignore"
|
||||
}
|
||||
],
|
||||
"eslint/no-label-var": "error",
|
||||
"eslint/no-labels": [
|
||||
"warn",
|
||||
{
|
||||
"allowLoop": true
|
||||
}
|
||||
],
|
||||
"eslint/no-proto": "warn",
|
||||
"eslint/no-self-compare": "error",
|
||||
"eslint/no-template-curly-in-string": "warn",
|
||||
"oxc/approx-constant": "warn",
|
||||
"oxc/misrefactored-assign-op": "error",
|
||||
"oxc/no-const-enum": "warn",
|
||||
"react/no-array-index-key": "error",
|
||||
"typescript/adjacent-overload-signatures": "warn",
|
||||
"typescript/no-invalid-void-type": "warn",
|
||||
"unicorn/no-document-cookie": "warn",
|
||||
"unicorn/no-instanceof-array": "warn",
|
||||
"unicorn/no-thenable": "error",
|
||||
"unicorn/prefer-number-properties": "off",
|
||||
"eslint/no-console": [
|
||||
"warn",
|
||||
{
|
||||
"allow": [
|
||||
"info",
|
||||
"warn",
|
||||
"error",
|
||||
"group",
|
||||
"groupEnd",
|
||||
"table",
|
||||
"assert",
|
||||
"countReset",
|
||||
"count",
|
||||
"dir",
|
||||
"time",
|
||||
"timeEnd",
|
||||
"timeStamp"
|
||||
]
|
||||
}
|
||||
],
|
||||
"eslint/no-else-return": [
|
||||
"error",
|
||||
{
|
||||
"allowElseIf": false
|
||||
}
|
||||
],
|
||||
"eslint/no-shadow": [
|
||||
"error",
|
||||
{
|
||||
"ignoreTypeValueShadow": true,
|
||||
"ignoreFunctionTypeParameterNameValueShadow": true
|
||||
}
|
||||
],
|
||||
"eslint/object-shorthand": ["error", "always"],
|
||||
"typescript/array-type": "off",
|
||||
"vitest/require-mock-type-parameters": "off",
|
||||
"vitest/require-to-throw-message": "off",
|
||||
"typescript/no-explicit-any": "off",
|
||||
"unicorn/no-array-for-each": "off",
|
||||
"import/no-cycle": "off",
|
||||
"react/refs": "off",
|
||||
"eslint/constructor-super": "off",
|
||||
"eslint/for-direction": "off",
|
||||
"eslint/getter-return": "off",
|
||||
"eslint/no-async-promise-executor": "off",
|
||||
"eslint/no-class-assign": "off",
|
||||
"eslint/no-compare-neg-zero": "off",
|
||||
"eslint/no-cond-assign": "off",
|
||||
"eslint/no-const-assign": "off",
|
||||
"eslint/no-constant-binary-expression": "off",
|
||||
"eslint/no-constant-condition": "off",
|
||||
"eslint/no-control-regex": "off",
|
||||
"eslint/no-debugger": "off",
|
||||
"eslint/no-delete-var": "off",
|
||||
"eslint/no-dupe-class-members": "off",
|
||||
"eslint/no-dupe-else-if": "off",
|
||||
"eslint/no-dupe-keys": "off",
|
||||
"eslint/no-duplicate-case": "off",
|
||||
"eslint/no-empty-character-class": "off",
|
||||
"eslint/no-empty-pattern": "off",
|
||||
"eslint/no-empty-static-block": "off",
|
||||
"eslint/no-ex-assign": "off",
|
||||
"eslint/no-extra-boolean-cast": "off",
|
||||
"eslint/no-func-assign": "off",
|
||||
"eslint/no-global-assign": "off",
|
||||
"eslint/no-import-assign": "off",
|
||||
"eslint/no-invalid-regexp": "off",
|
||||
"eslint/no-irregular-whitespace": "off",
|
||||
"eslint/no-loss-of-precision": "off",
|
||||
"eslint/no-misleading-character-class": "off",
|
||||
"eslint/no-new-native-nonconstructor": "off",
|
||||
"eslint/no-nonoctal-decimal-escape": "off",
|
||||
"eslint/no-obj-calls": "off",
|
||||
"eslint/no-self-assign": "off",
|
||||
"eslint/no-setter-return": "off",
|
||||
"eslint/no-shadow-restricted-names": "off",
|
||||
"eslint/no-sparse-arrays": "off",
|
||||
"eslint/no-this-before-super": "off",
|
||||
"eslint/no-unassigned-vars": "off",
|
||||
"eslint/no-unreachable": "off",
|
||||
"eslint/no-unsafe-finally": "off",
|
||||
"eslint/no-unsafe-negation": "off",
|
||||
"eslint/no-unsafe-optional-chaining": "off",
|
||||
"eslint/no-unused-expressions": "off",
|
||||
"eslint/no-unused-labels": "off",
|
||||
"eslint/no-unused-private-class-members": "off",
|
||||
"eslint/no-unused-vars": "off",
|
||||
"eslint/no-useless-backreference": "off",
|
||||
"eslint/no-useless-catch": "off",
|
||||
"eslint/no-useless-escape": "off",
|
||||
"eslint/no-with": "off",
|
||||
"eslint/require-yield": "off",
|
||||
"eslint/use-isnan": "off",
|
||||
"eslint/valid-typeof": "off",
|
||||
"import/default": "off",
|
||||
"react/error-boundaries": "off",
|
||||
"react/exhaustive-deps": "off",
|
||||
"react/globals": "off",
|
||||
"react/immutability": "off",
|
||||
"react/incompatible-library": "off",
|
||||
"react/jsx-key": "off",
|
||||
"react/jsx-no-duplicate-props": "off",
|
||||
"react/jsx-no-undef": "off",
|
||||
"react/no-children-prop": "off",
|
||||
"react/no-danger-with-children": "off",
|
||||
"react/no-direct-mutation-state": "off",
|
||||
"react/no-find-dom-node": "off",
|
||||
"react/no-is-mounted": "off",
|
||||
"react/no-render-return-value": "off",
|
||||
"react/no-string-refs": "off",
|
||||
"react/preserve-manual-memoization": "off",
|
||||
"react/purity": "off",
|
||||
"react/set-state-in-effect": "off",
|
||||
"react/set-state-in-render": "off",
|
||||
"react/static-components": "off",
|
||||
"react/use-memo": "off",
|
||||
"react/void-use-memo": "off",
|
||||
"typescript/no-duplicate-enum-values": "off",
|
||||
"typescript/no-extra-non-null-assertion": "off",
|
||||
"typescript/no-misused-new": "off",
|
||||
"typescript/no-non-null-asserted-optional-chain": "off",
|
||||
"typescript/no-this-alias": "off",
|
||||
"typescript/no-unsafe-declaration-merging": "off",
|
||||
"typescript/no-wrapper-object-types": "off",
|
||||
"typescript/prefer-as-const": "off",
|
||||
"typescript/prefer-namespace-keyword": "off",
|
||||
"typescript/triple-slash-reference": "off",
|
||||
"vitest/no-duplicate-hooks": "error",
|
||||
"vitest/expect-expect": "off",
|
||||
"vitest/no-conditional-expect": "off",
|
||||
"vitest/no-disabled-tests": "off",
|
||||
"vitest/no-focused-tests": "off",
|
||||
"vitest/no-standalone-expect": "off",
|
||||
"vitest/require-local-test-context-for-concurrent-snapshots": "off",
|
||||
"vitest/valid-describe-callback": "off",
|
||||
"vitest/valid-expect": "off",
|
||||
"vitest/valid-expect-in-promise": "off",
|
||||
"vitest/valid-title": "off"
|
||||
},
|
||||
"overrides": [
|
||||
{
|
||||
"files": ["**/*.stories.ts", "**/*.stories.tsx", "scripts/*.ts"],
|
||||
"rules": {
|
||||
"eslint/no-console": "off"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Vendored
+1
-1
@@ -2,7 +2,7 @@
|
||||
"recommendations": [
|
||||
"dbaeumer.vscode-eslint",
|
||||
"stylelint.vscode-stylelint",
|
||||
"oxc.oxc-vscode",
|
||||
"biomejs.biome",
|
||||
"esbenp.prettier-vscode",
|
||||
"denoland.vscode-deno",
|
||||
"github.vscode-github-actions",
|
||||
|
||||
@@ -13,7 +13,8 @@ files.forEach((file) => {
|
||||
|
||||
data = data.map((x) => {
|
||||
return x.includes("React.ForwardRefExoticComponent")
|
||||
? `${x.split(":")[0]}: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement> & SVGRProps & React.RefAttributes<SVGSVGElement>>;`
|
||||
? x.split(":")[0] +
|
||||
`: React.ForwardRefExoticComponent<React.SVGProps<SVGSVGElement> & SVGRProps & React.RefAttributes<SVGSVGElement>>;`
|
||||
: x;
|
||||
});
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ ymlList.forEach((file) => {
|
||||
const ymlData = load(fs.readFileSync(`${basePath}/${file}`), {
|
||||
schema: JSON_SCHEMA,
|
||||
});
|
||||
// biome-ignore lint/complexity/useOptionalChain: optional chain not supported in enviroment. Consider revriting to Typescript
|
||||
if (ymlData.keywords && ymlData.keywords.includes("[ignore-docs]")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* biome-ignore-all lint/correctness/useHookAtTopLevel: `useId` is invariant at runtime. */
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
// https://github.com/mui/material-ui/blob/master/packages/mui-utils/src/useId.ts
|
||||
|
||||
@@ -37,7 +37,7 @@ describe("getJSXLiteralValue", () => {
|
||||
});
|
||||
|
||||
test("returns null for TemplateLiteral with expressions", () => {
|
||||
// oxlint-disable-next-line eslint/no-template-curly-in-string -- Testing only
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing only
|
||||
const node = getAttrValue("<Component prop={`hello ${name}`} />");
|
||||
expect(getJSXLiteralValue(node)).toBe(null);
|
||||
});
|
||||
|
||||
@@ -209,7 +209,7 @@ describe("getTokenRegex JS", () => {
|
||||
|
||||
expect("ATextRed").toMatch(regex);
|
||||
expect(" ATextRed ").toMatch(regex);
|
||||
// oxlint-disable-next-line eslint/no-template-curly-in-string -- Testing token match inside template string
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing token match inside template string
|
||||
expect(" background: `${ATextRed}` ").toMatch(regex);
|
||||
});
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ describe("v8-tokens-js keeps boundaries intact", () => {
|
||||
["const a = ASurfaceDefault;", "const a = BgDefault;"],
|
||||
["const a = [ASurfaceDefault];", "const a = [BgDefault];"],
|
||||
["const a = {...ASurfaceDefault};", "const a = {...BgDefault};"],
|
||||
// oxlint-disable-next-line eslint/no-template-curly-in-string -- Testing token inside template literal
|
||||
// biome-ignore lint/suspicious/noTemplateCurlyInString: Testing token inside template literal
|
||||
["const a = `${ASurfaceDefault}`;", "const a = `${BgDefault}`;"],
|
||||
])("%s", (input, output) => {
|
||||
expect(runJs(`${importLine}\n${input}`)).toBe(
|
||||
|
||||
@@ -139,14 +139,14 @@ async function bundle() {
|
||||
|
||||
/* ------------------------------ /global build ----------------------------- */
|
||||
for (const style of BASELINE_CSS_LIST) {
|
||||
const parser = (input: string) => {
|
||||
function parser(input: string) {
|
||||
const parsed = input
|
||||
.split("\n")
|
||||
.filter((line) => line.startsWith("@import"))
|
||||
.filter((line) => line.includes(style))
|
||||
.join("\n");
|
||||
return `${layerDefinition}\n${parsed}`;
|
||||
};
|
||||
}
|
||||
|
||||
await bundleCSS(parser).then((file) => {
|
||||
writeFile({
|
||||
@@ -208,13 +208,13 @@ async function bundle() {
|
||||
}
|
||||
|
||||
for (const componentLine of componentFiles()) {
|
||||
const parser = (input: string) => {
|
||||
function parser(input: string) {
|
||||
const parsed = input
|
||||
.split("\n")
|
||||
.filter((line) => line === componentLine)
|
||||
.join("\n");
|
||||
return `${layerDefinition}\n${parsed}`;
|
||||
};
|
||||
}
|
||||
|
||||
await bundleCSS(parser).then((file) => {
|
||||
const componentName = componentLine
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -791,7 +791,7 @@
|
||||
"@navikt/aksel-icons": "^8.20.1",
|
||||
"@navikt/ds-tokens": "^8.20.1",
|
||||
"date-fns": "^4.0.0",
|
||||
"react-day-picker": "10.0.1"
|
||||
"react-day-picker": "10.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@testing-library/dom": "10.4.2",
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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 -- React's button type union is validated by TypeScript and defaults to "button".
|
||||
type={type}
|
||||
className={cl(
|
||||
"aksel-chips__chip aksel-chips__removable aksel-chips--icon-right",
|
||||
|
||||
@@ -149,7 +149,7 @@ export namespace DataGrid {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Table {
|
||||
// oxlint-disable-next-line eslint/no-shadow -- intentional namespace re-export
|
||||
// biome-ignore lint/suspicious/noShadow: intentional namespace re-export
|
||||
export type Props<T = unknown> = DataGridTable.Props<T>;
|
||||
export type Sorting = DataGridTable.Sorting;
|
||||
export type SortEntry = DataGridTable.SortEntry;
|
||||
@@ -161,7 +161,7 @@ export namespace DataGrid {
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Preferences {
|
||||
// oxlint-disable-next-line eslint/no-shadow -- intentional namespace re-export
|
||||
// biome-ignore lint/suspicious/noShadow: intentional namespace re-export
|
||||
export type Props = DataGridPreferences.Props;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** biome-ignore-all lint/correctness/useHookAtTopLevel: False positive because of the way forwardRef() is added */
|
||||
import React, {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
|
||||
@@ -139,6 +139,7 @@ function useTableColumnResize({
|
||||
[resizeMin, resizeMax, setWidth],
|
||||
);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We only want to run this on mount and when autoResizeOnce changes
|
||||
useEffect(
|
||||
function autoResizeColumn() {
|
||||
if (!autoResizeOnce) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -103,6 +103,66 @@ export const DropdownCaptionNavigation: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const ControlledMonthPreservesFocus: Story = {
|
||||
tags: ["play-fn", "!skip-test"],
|
||||
render: () => {
|
||||
const [month, setMonth] = React.useState(new Date(2026, 2, 15));
|
||||
const [selected, setSelected] = React.useState<Date | undefined>(
|
||||
new Date(2026, 2, 15),
|
||||
);
|
||||
|
||||
return (
|
||||
<fieldset
|
||||
onKeyDown={(event) => {
|
||||
if (event.key.toLowerCase() === "j") {
|
||||
const nextDate = new Date(2026, 10, 15);
|
||||
setMonth(nextDate);
|
||||
setSelected(nextDate);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<legend>Bytt måned med J</legend>
|
||||
<DatePicker.Standalone
|
||||
locale="nb"
|
||||
month={month}
|
||||
onMonthChange={setMonth}
|
||||
selected={selected}
|
||||
onSelect={setSelected}
|
||||
/>
|
||||
</fieldset>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const selectedDay = canvas.getByRole("button", {
|
||||
name: "søndag 15",
|
||||
pressed: true,
|
||||
});
|
||||
|
||||
await userEvent.click(selectedDay);
|
||||
await expect(selectedDay).toHaveFocus();
|
||||
await userEvent.keyboard("j");
|
||||
|
||||
await expect(canvas.getByText("november 2026")).toBeInTheDocument();
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: "søndag 15", pressed: true }),
|
||||
).toHaveFocus();
|
||||
|
||||
await userEvent.keyboard("{ArrowRight}");
|
||||
await expect(
|
||||
canvas.getByRole("button", { name: "mandag 16" }),
|
||||
).toHaveFocus();
|
||||
|
||||
const nextMonthButton = canvas.getByRole("button", {
|
||||
name: "Gå til neste måned",
|
||||
});
|
||||
await userEvent.click(nextMonthButton);
|
||||
|
||||
await expect(canvas.getByText("desember 2026")).toBeInTheDocument();
|
||||
await expect(nextMonthButton).toHaveFocus();
|
||||
},
|
||||
};
|
||||
|
||||
export const Required: Story = {
|
||||
render: () => {
|
||||
const { datepickerProps } = useDatepicker({
|
||||
|
||||
@@ -28,7 +28,6 @@ const DatePickerDayButton = ({
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
{...rest}
|
||||
ref={ref}
|
||||
aria-hidden={day.outside}
|
||||
|
||||
@@ -104,7 +104,6 @@ const ReactDayPicker = ({
|
||||
selected={selected}
|
||||
classNames={LegacyClassNames}
|
||||
components={{
|
||||
// oxlint-disable-next-line react/jsx-no-useless-fragment -- react-day-picker requires an Element return type, so null is not allowed.
|
||||
MonthCaption: () => <></>,
|
||||
DayButton: useCallback(
|
||||
(props) => <DatePickerDayButton {...props} locale={locale} />,
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -26,7 +26,8 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
|
||||
props;
|
||||
|
||||
return (
|
||||
// oxlint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- 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"
|
||||
className="aksel-combobox__list-options"
|
||||
>
|
||||
|
||||
@@ -76,7 +76,7 @@ const FilteredOptionsItem = ({ option }: { option: ComboboxOption }) => {
|
||||
toggleIsListOpen(false);
|
||||
}
|
||||
}}
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-element-to-interactive-role -- List item acts as an option in the combobox.
|
||||
// biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: List item acts as an option in the combobox.
|
||||
role="option"
|
||||
aria-selected={isInList(option.value, selectedOptions)}
|
||||
aria-disabled={isDisabled(option) || undefined}
|
||||
|
||||
@@ -54,7 +54,8 @@ export const InputController = forwardRef<
|
||||
|
||||
return (
|
||||
<Floating.Anchor asChild ref={setAnchorRef}>
|
||||
{/* oxlint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- Acts as wrapper list-toggle and input; key-events are handled by input itself */}
|
||||
{/** biome-ignore lint/a11y/noStaticElementInteractions: Acts as wrapper list-toggle and input */}
|
||||
{/** biome-ignore lint/a11y/useKeyWithClickEvents: key-events are handled by input itself */}
|
||||
<div
|
||||
className={cl("aksel-combobox__wrapper-inner aksel-text-field__input", {
|
||||
"aksel-combobox__wrapper-inner--virtually-unfocused":
|
||||
|
||||
@@ -112,6 +112,7 @@ const SelectedOptionsProvider = ({
|
||||
const newHideCaret =
|
||||
isLimitReached || (!isMultiSelect && selectedOptions.length > 0);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We explicitly want to run this effect when selectedOptions changes to match the view with the selected options.
|
||||
useEffect(() => {
|
||||
setHideCaret(newHideCaret);
|
||||
}, [newHideCaret, selectedOptions, setHideCaret]);
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
type ResolveOption,
|
||||
useComboboxRootContext,
|
||||
} from "../root/ComboboxRoot";
|
||||
import { useHitsAnnouncer } from "./useHitsAnnouncer";
|
||||
|
||||
interface ComboboxListProps<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
@@ -47,12 +48,13 @@ function ComboboxList<
|
||||
function ComboboxListContent<
|
||||
T extends ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>,
|
||||
>({ children }: ComboboxListProps<T>) {
|
||||
const { virtuallyFocusedOptionValue, filterString, setAnnounceText } =
|
||||
const { virtuallyFocusedOptionValue, filterString } =
|
||||
useComboboxPopupContext();
|
||||
const { setOpen, options, value, onToggleOption, multiple, triggerRef } =
|
||||
useComboboxRootContext();
|
||||
const memoizedOnToggleOption = useEventCallback(onToggleOption);
|
||||
const deferredFilterString = useDeferredValue(filterString);
|
||||
const maybeAnnouceHits = useHitsAnnouncer(deferredFilterString, filterString);
|
||||
|
||||
const localOnToggleOption = useCallback(
|
||||
(option: ComboboxOptionData, isSelected: boolean) => {
|
||||
@@ -73,16 +75,11 @@ function ComboboxListContent<
|
||||
? filterOptions(options, deferredFilterString)
|
||||
: options;
|
||||
|
||||
if (filteredOptions.length === 0) {
|
||||
setAnnounceText("Ingen treff");
|
||||
return (
|
||||
<div className="aksel-combobox2__no-results" role="status">
|
||||
Ingen treff
|
||||
</div>
|
||||
);
|
||||
}
|
||||
maybeAnnouceHits(filteredOptions);
|
||||
|
||||
setAnnounceText("");
|
||||
if (filteredOptions.length === 0) {
|
||||
return <div className="aksel-combobox2__no-results">Ingen treff</div>;
|
||||
}
|
||||
|
||||
return filteredOptions.map((optOrGroup) =>
|
||||
"options" in optOrGroup ? (
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Based on https://github.com/facebook/astryx/blob/main/packages/core/src/hooks/useAnnounce.ts
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
/**
|
||||
* Provides imperative screen-reader announcements via persistently-mounted
|
||||
* polite/assertive live regions. Regions are auto-cleared shortly after
|
||||
* announcing so stale status text does not linger in the accessibility tree.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Announcement urgency:
|
||||
* - `'polite'` (default): announced when the screen reader is idle. Use for
|
||||
* status updates, result counts, "no results", non-urgent confirmations.
|
||||
* - `'assertive'`: interrupts the current announcement. Reserve for errors and
|
||||
* time-sensitive alerts.
|
||||
*/
|
||||
type Politeness = "polite" | "assertive";
|
||||
|
||||
const clearTimers: {
|
||||
[K in Politeness]: ReturnType<typeof setTimeout> | null;
|
||||
} = {
|
||||
polite: null,
|
||||
assertive: null,
|
||||
};
|
||||
|
||||
function cancelScheduledClear(politeness: Politeness) {
|
||||
const timer = clearTimers[politeness];
|
||||
if (timer != null) {
|
||||
clearTimeout(timer);
|
||||
clearTimers[politeness] = null;
|
||||
}
|
||||
}
|
||||
|
||||
function scheduleClear(politeness: Politeness, target: HTMLElement) {
|
||||
cancelScheduledClear(politeness);
|
||||
clearTimers[politeness] = setTimeout(() => {
|
||||
clearTimers[politeness] = null;
|
||||
target.textContent = "";
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
let regions: {
|
||||
polite: HTMLElement;
|
||||
assertive: HTMLElement;
|
||||
} | null = null;
|
||||
|
||||
function createRegion(politeness: Politeness) {
|
||||
const el = document.createElement("div");
|
||||
el.dataset.akselAnnouncer = "";
|
||||
el.className = "aksel-sr-only";
|
||||
el.role = politeness === "assertive" ? "alert" : "status";
|
||||
document.body.appendChild(el);
|
||||
return el;
|
||||
}
|
||||
|
||||
function getRegions() {
|
||||
if (typeof document === "undefined") {
|
||||
return null;
|
||||
}
|
||||
if (regions) {
|
||||
// Re-attach if a region was removed from the DOM (e.g. by a test cleanup).
|
||||
if (!regions.polite.isConnected) {
|
||||
document.body.appendChild(regions.polite);
|
||||
}
|
||||
if (!regions.assertive.isConnected) {
|
||||
document.body.appendChild(regions.assertive);
|
||||
}
|
||||
return regions;
|
||||
}
|
||||
regions = {
|
||||
polite: createRegion("polite"),
|
||||
assertive: createRegion("assertive"),
|
||||
};
|
||||
return regions;
|
||||
}
|
||||
|
||||
function announce(message: string, politeness: Politeness = "polite") {
|
||||
const r = getRegions();
|
||||
if (!r) {
|
||||
return;
|
||||
}
|
||||
const target = r[politeness];
|
||||
if (!message) {
|
||||
cancelScheduledClear(politeness);
|
||||
target.textContent = "";
|
||||
return;
|
||||
}
|
||||
// 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
|
||||
scheduleClear(politeness, target);
|
||||
}
|
||||
|
||||
/**
|
||||
* Speaks a message through a persistently-mounted, visually-hidden live region.
|
||||
*
|
||||
* The polite and assertive regions are created once and kept mounted.
|
||||
*
|
||||
* Each message is automatically cleared a couple of seconds after being
|
||||
* announced, so users browsing the DOM later do not encounter stale status
|
||||
* text. Announcing again before the clear simply resets the countdown.
|
||||
*/
|
||||
function useAnnounce() {
|
||||
getRegions(); // Ensure regions are created on beforehand
|
||||
// NOTE: Before using this hook several places, we should give each instance their own live regions to avoid conflicts.
|
||||
return announce;
|
||||
}
|
||||
|
||||
export { useAnnounce };
|
||||
@@ -0,0 +1,62 @@
|
||||
import { useEffect, useMemo, useRef } from "react";
|
||||
import { debounce } from "../../../utils-external";
|
||||
import type {
|
||||
ComboboxGroupData,
|
||||
ComboboxOptionData,
|
||||
} from "../root/ComboboxRoot";
|
||||
import { useAnnounce } from "./useAnnounce";
|
||||
|
||||
function getNumberOfOptions<T extends ComboboxOptionData>(
|
||||
options: (T | ComboboxGroupData<T>)[],
|
||||
) {
|
||||
return options.reduce((count, optOrGroup) => {
|
||||
if ("options" in optOrGroup) {
|
||||
return count + optOrGroup.options.length;
|
||||
}
|
||||
return count + 1;
|
||||
}, 0);
|
||||
}
|
||||
|
||||
function useHitsAnnouncer(deferredFilterString: string, filterString: string) {
|
||||
const shouldAnnounceRef = useRef(false);
|
||||
const announce = useAnnounce();
|
||||
const announceHits = useMemo(
|
||||
() => debounce((hits: number) => announce(`${hits} treff`), 2000),
|
||||
[announce],
|
||||
);
|
||||
const announceNoHits = useMemo(
|
||||
() => debounce(() => announce(`Ingen treff`), 500),
|
||||
[announce],
|
||||
);
|
||||
useEffect(
|
||||
() =>
|
||||
function cleanUp() {
|
||||
announceHits.clear();
|
||||
announceNoHits.clear();
|
||||
},
|
||||
[announceHits, announceNoHits],
|
||||
);
|
||||
|
||||
return (
|
||||
filteredOptions: (
|
||||
ComboboxOptionData | ComboboxGroupData<ComboboxOptionData>
|
||||
)[],
|
||||
) => {
|
||||
// The options are filtered based on deferredFilterString, so we need to wait for it to settle.
|
||||
if (deferredFilterString !== filterString) {
|
||||
shouldAnnounceRef.current = true;
|
||||
} else if (shouldAnnounceRef.current) {
|
||||
if (filteredOptions.length === 0) {
|
||||
announceHits.clear();
|
||||
announceNoHits();
|
||||
} else {
|
||||
const hits = getNumberOfOptions(filteredOptions);
|
||||
announceNoHits.clear();
|
||||
announceHits(hits);
|
||||
}
|
||||
shouldAnnounceRef.current = false;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export { useHitsAnnouncer };
|
||||
@@ -65,6 +65,7 @@ const highlightSubstring = (text: string, substring: string) => {
|
||||
const end = text.substring(indexOfHighlightedText + substring.length);
|
||||
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 aria-label doesn't work
|
||||
<span aria-label={text}>
|
||||
{start}
|
||||
{highlight && <mark>{highlight}</mark>}
|
||||
|
||||
@@ -8,7 +8,6 @@ interface ComboboxPopupContextProps {
|
||||
setVirtuallyFocusedOptionValue: (value: string) => void;
|
||||
filterString: string;
|
||||
setFilterString: (value: string) => void;
|
||||
setAnnounceText: (text: string) => void;
|
||||
}
|
||||
|
||||
export const {
|
||||
@@ -29,7 +28,6 @@ export const ComboboxPopup = ({ children }: ComboboxPopupProps) => {
|
||||
const [filterString, setFilterString] = useState("");
|
||||
const rootContext = useComboboxRootContext();
|
||||
const ref = useRef<HTMLDivElement | null>(null);
|
||||
const [announceText, setAnnounceText] = useState("");
|
||||
|
||||
useEffect(
|
||||
function onOpen() {
|
||||
@@ -89,12 +87,8 @@ export const ComboboxPopup = ({ children }: ComboboxPopupProps) => {
|
||||
setFilterString(newString);
|
||||
setVirtuallyFocusedOptionValue("");
|
||||
}}
|
||||
setAnnounceText={setAnnounceText}
|
||||
>
|
||||
{children}
|
||||
<div className="aksel-sr-only" role="status">
|
||||
{announceText}
|
||||
</div>
|
||||
</ComboboxPopupContextProvider>
|
||||
</Listbox>
|
||||
);
|
||||
|
||||
@@ -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 can be supplied through rest or the slotted trigger.
|
||||
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/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- Assistive technologies and keyboard users access the 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}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { isAcceptedSize } from "./is-accepted-size";
|
||||
|
||||
describe("isAcceptedSize", () => {
|
||||
it("should return true if maxSize is less than or equal to 0", () => {
|
||||
const file = new File(["a".repeat(4999)], "filename.txt", {
|
||||
const file = new File([new Array(5000).join("a")], "filename.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
expect(isAcceptedSize(file, -1)).toBe(true);
|
||||
@@ -10,7 +10,7 @@ describe("isAcceptedSize", () => {
|
||||
});
|
||||
|
||||
it("should return true if file size is less than or equal to maxSize", () => {
|
||||
const file = new File(["a".repeat(4999)], "filename.txt", {
|
||||
const file = new File([new Array(5000).join("a")], "filename.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
expect(isAcceptedSize(file, 5000)).toBe(true);
|
||||
@@ -18,7 +18,7 @@ describe("isAcceptedSize", () => {
|
||||
});
|
||||
|
||||
it("should return false if file size is greater than maxSize", () => {
|
||||
const file = new File(["a".repeat(4999)], "filename.txt", {
|
||||
const file = new File([new Array(5000).join("a")], "filename.txt", {
|
||||
type: "text/plain",
|
||||
});
|
||||
expect(isAcceptedSize(file, 4000)).toBe(false);
|
||||
|
||||
@@ -22,7 +22,8 @@ export const Radio = forwardRef<HTMLInputElement, RadioProps>(
|
||||
const { className, description, children } = props;
|
||||
|
||||
return (
|
||||
// oxlint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events -- clickable div for radio card
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: clickable div for radio card
|
||||
// biome-ignore lint/a11y/useKeyWithClickEvents: clickable div for radio card
|
||||
<div
|
||||
className={cl(className, "aksel-radio", `aksel-radio--${size}`, {
|
||||
"aksel-radio--error": hasError,
|
||||
|
||||
@@ -158,7 +158,7 @@ export const Search = forwardRef<HTMLInputElement, SearchProps>(
|
||||
clearButton && !inputProps.disabled && (value ?? internalValue);
|
||||
|
||||
return (
|
||||
// oxlint-disable-next-line jsx-a11y/no-static-element-interactions -- Escape key handler for clearing input
|
||||
// biome-ignore lint/a11y/noStaticElementInteractions: Escape key handler for clearing input
|
||||
<div
|
||||
onKeyDown={(event) => {
|
||||
if (event.key !== "Escape") {
|
||||
|
||||
@@ -80,7 +80,7 @@ export const useFormField = (
|
||||
const showErrorMsg =
|
||||
!disabled && !readOnly && !!error && typeof error !== "boolean";
|
||||
|
||||
const ariaInvalid = hasError ? { "aria-invalid": true } : {};
|
||||
const ariaInvalid = { ...(hasError ? { "aria-invalid": true } : {}) };
|
||||
|
||||
if ((props as any)?.required) {
|
||||
consoleWarning(
|
||||
|
||||
@@ -52,15 +52,15 @@ export const Variants: StoryFn = () => {
|
||||
<VStack gap="space-16">
|
||||
{variants.map((variant, i) => (
|
||||
<Alert key={variant} variant={variant}>
|
||||
{"Id elit esse enim reprehenderit enim nisi veniam nostrud. ".repeat(
|
||||
i + 1,
|
||||
{new Array(i + 1).fill(
|
||||
"Id elit esse enim reprehenderit enim nisi veniam nostrud. ",
|
||||
)}
|
||||
</Alert>
|
||||
))}
|
||||
{variants.map((variant, i) => (
|
||||
<Alert key={variant} variant={variant} size="small">
|
||||
{"Id elit esse enim reprehenderit enim nisi veniam nostrud. ".repeat(
|
||||
i + 1,
|
||||
{new Array(i + 1).fill(
|
||||
"Id elit esse enim reprehenderit enim nisi veniam nostrud. ",
|
||||
)}
|
||||
</Alert>
|
||||
))}
|
||||
|
||||
@@ -102,6 +102,7 @@ export const Stepper: StepperComponent = forwardRef<
|
||||
className="aksel-stepper__item"
|
||||
data-interactive={isInteractive}
|
||||
data-color={isInteractive ? undefined : "neutral"}
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Unsure how to fix gracefully
|
||||
key={index + (children?.toString?.() ?? "")}
|
||||
>
|
||||
<span className="aksel-stepper__line aksel-stepper__line--1" />
|
||||
|
||||
@@ -89,6 +89,7 @@ export const Lookup = forwardRef<HTMLSpanElement, LookupProps>(
|
||||
|
||||
return (
|
||||
<>
|
||||
{/** biome-ignore lint/a11y/useSemanticElements: We use a span since the built-in button styling does not allow for proper line-break. Lookup-text needs to be able to "flow" across multiple lines. */}
|
||||
<span
|
||||
{...rest}
|
||||
role="button"
|
||||
|
||||
@@ -149,24 +149,26 @@ export const EmptyHeader: StoryFn = () => (
|
||||
);
|
||||
|
||||
export const SizeSmall: StoryFn = () => (
|
||||
<Modal
|
||||
open
|
||||
onClose={() => null}
|
||||
header={{
|
||||
heading: "Simple header",
|
||||
size: "small",
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<Modal.Body>
|
||||
<BodyLong>Lorem ipsum dolor sit amet.</BodyLong>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button size="small" variant="secondary">
|
||||
Dummy button
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
<>
|
||||
<Modal
|
||||
open
|
||||
onClose={() => null}
|
||||
header={{
|
||||
heading: "Simple header",
|
||||
size: "small",
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
<Modal.Body>
|
||||
<BodyLong>Lorem ipsum dolor sit amet.</BodyLong>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button size="small" variant="secondary">
|
||||
Dummy button
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
SizeSmall.storyName = "Size = Small";
|
||||
|
||||
|
||||
@@ -170,7 +170,6 @@ const ModalRoot = forwardRef<HTMLDialogElement, ModalProps>(
|
||||
: ariaLabelledby;
|
||||
|
||||
const component = (
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- Native dialog handles backdrop dismissal and bubbling Escape events.
|
||||
<dialog
|
||||
{...rest}
|
||||
ref={mergedRef}
|
||||
|
||||
@@ -75,7 +75,6 @@ const MonthPickerButtonInternal = ({
|
||||
const isThisMonth = dateIsInCurrentMonth(month, year);
|
||||
|
||||
return (
|
||||
// oxlint-disable-next-line jsx-a11y/control-has-associated-label -- The full month name is provided by the nested screen-reader-only span.
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
|
||||
@@ -172,7 +172,7 @@ const PaginationRoot = forwardRef<HTMLElement, PaginationProps>(
|
||||
return Number.isNaN(n) ? (
|
||||
<li
|
||||
className="aksel-pagination__ellipsis"
|
||||
// oxlint-disable-next-line react/no-array-index-key -- Using index as key for ellipsis items
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Using index as key for ellipsis items
|
||||
key={`${step}-${i}`}
|
||||
>
|
||||
<BodyShort
|
||||
|
||||
@@ -16,162 +16,176 @@ export default {
|
||||
|
||||
export const Default = {
|
||||
render: () => (
|
||||
<VStack gap="space-8">
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginInline="space-40 space-0">
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginInline="10 0"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
<>
|
||||
<VStack gap="space-8">
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginInline="space-40 space-0">
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginInline="10 0"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginInline="space-0 space-40">
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginInline="0 10"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginInline="space-0 space-40">
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginInline="0 10"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginBlock="space-40 space-0">
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginBlock="10 0"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginBlock="space-40 space-0">
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginBlock="10 0"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginBlock="space-0 space-40">
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginBlock="0 10"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginBlock="space-0 space-40">
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginBlock="0 10"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
export const AsChild = {
|
||||
render: () => (
|
||||
<VStack gap="space-8">
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginInline="space-40 space-0" asChild>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginInline="10 0"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
<>
|
||||
<VStack gap="space-8">
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginInline="space-40 space-0" asChild>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginInline="10 0"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginInline="space-0 space-40" asChild>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginInline="0 10"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginInline="space-0 space-40" asChild>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginInline="0 10"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginBlock="space-40 space-0" asChild>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginBlock="10 0"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginBlock="space-40 space-0" asChild>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginBlock="10 0"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginBlock="space-0 space-40" asChild>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginBlock="0 10"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
<Box background="meta-purple-moderate" padding="space-20">
|
||||
<Box background="meta-lime-soft" padding="space-20">
|
||||
<Bleed marginBlock="space-0 space-40" asChild>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>marginBlock="0 10"</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
export const Breakpoints = {
|
||||
render: () => (
|
||||
<VStack gap="space-8">
|
||||
<Box
|
||||
background="meta-purple-moderate"
|
||||
padding={{ xs: "space-20", md: "space-40" }}
|
||||
>
|
||||
<>
|
||||
<VStack gap="space-8">
|
||||
<Box
|
||||
background="meta-lime-soft"
|
||||
background="meta-purple-moderate"
|
||||
padding={{ xs: "space-20", md: "space-40" }}
|
||||
>
|
||||
<Bleed
|
||||
marginInline={{ xs: "space-40 space-0", md: "space-80 space-0" }}
|
||||
<Box
|
||||
background="meta-lime-soft"
|
||||
padding={{ xs: "space-20", md: "space-40" }}
|
||||
>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>{'marginInline={{ xs: "10 0", md: "20 0" }}'}</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
<Bleed
|
||||
marginInline={{ xs: "space-40 space-0", md: "space-80 space-0" }}
|
||||
>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>
|
||||
{'marginInline={{ xs: "10 0", md: "20 0" }}'}
|
||||
</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
background="meta-purple-moderate"
|
||||
padding={{ xs: "space-20", md: "space-40" }}
|
||||
>
|
||||
<Box
|
||||
background="meta-lime-soft"
|
||||
background="meta-purple-moderate"
|
||||
padding={{ xs: "space-20", md: "space-40" }}
|
||||
>
|
||||
<Bleed
|
||||
marginInline={{ xs: "space-0 space-40", md: "space-0 space-80" }}
|
||||
<Box
|
||||
background="meta-lime-soft"
|
||||
padding={{ xs: "space-20", md: "space-40" }}
|
||||
>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>{'marginInline={{ xs: "0 10", md: "0 20" }}'}</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
<Bleed
|
||||
marginInline={{ xs: "space-0 space-40", md: "space-0 space-80" }}
|
||||
>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>
|
||||
{'marginInline={{ xs: "0 10", md: "0 20" }}'}
|
||||
</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
background="meta-purple-moderate"
|
||||
padding={{ xs: "space-20", md: "space-40" }}
|
||||
>
|
||||
<Box
|
||||
background="meta-lime-soft"
|
||||
background="meta-purple-moderate"
|
||||
padding={{ xs: "space-20", md: "space-40" }}
|
||||
>
|
||||
<Bleed
|
||||
marginBlock={{ xs: "space-40 space-0", md: "space-80 space-0" }}
|
||||
<Box
|
||||
background="meta-lime-soft"
|
||||
padding={{ xs: "space-20", md: "space-40" }}
|
||||
>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>{'marginBlock={{ xs: "10 0", md: "20 0" }}'}</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
<Bleed
|
||||
marginBlock={{ xs: "space-40 space-0", md: "space-80 space-0" }}
|
||||
>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>
|
||||
{'marginBlock={{ xs: "10 0", md: "20 0" }}'}
|
||||
</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box
|
||||
background="meta-purple-moderate"
|
||||
padding={{ xs: "space-20", md: "space-40" }}
|
||||
>
|
||||
<Box
|
||||
background="meta-lime-soft"
|
||||
background="meta-purple-moderate"
|
||||
padding={{ xs: "space-20", md: "space-40" }}
|
||||
>
|
||||
<Bleed
|
||||
marginBlock={{ xs: "space-0 space-40", md: "space-0 space-80" }}
|
||||
<Box
|
||||
background="meta-lime-soft"
|
||||
padding={{ xs: "space-20", md: "space-40" }}
|
||||
>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>{'marginBlock={{ xs: "0 10", md: "0 20" }}'}</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
<Bleed
|
||||
marginBlock={{ xs: "space-0 space-40", md: "space-0 space-80" }}
|
||||
>
|
||||
<Box padding="space-12" background="success-moderate">
|
||||
<BodyLong>
|
||||
{'marginBlock={{ xs: "0 10", md: "0 20" }}'}
|
||||
</BodyLong>
|
||||
</Box>
|
||||
</Bleed>
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
</VStack>
|
||||
</VStack>
|
||||
</>
|
||||
),
|
||||
};
|
||||
|
||||
|
||||
@@ -155,30 +155,32 @@ const ChatBubble = () => (
|
||||
</Box>
|
||||
);
|
||||
const PricePill = () => (
|
||||
<HStack>
|
||||
<Box
|
||||
background="success-soft"
|
||||
padding="space-16"
|
||||
borderRadius="full 0 0 full"
|
||||
>
|
||||
<VStack align="center">
|
||||
<Detail>Episk ny pris</Detail>
|
||||
<Heading size="medium">889.99 kr</Heading>
|
||||
</VStack>
|
||||
</Box>
|
||||
<Box
|
||||
background="danger-soft"
|
||||
padding="space-16"
|
||||
borderRadius="0 full full 0"
|
||||
>
|
||||
<VStack align="center">
|
||||
<Detail>Førpris</Detail>
|
||||
<Heading size="medium" style={{ textDecoration: "line-through" }}>
|
||||
399.99 kr
|
||||
</Heading>
|
||||
</VStack>
|
||||
</Box>
|
||||
</HStack>
|
||||
<>
|
||||
<HStack>
|
||||
<Box
|
||||
background="success-soft"
|
||||
padding="space-16"
|
||||
borderRadius="full 0 0 full"
|
||||
>
|
||||
<VStack align="center">
|
||||
<Detail>Episk ny pris</Detail>
|
||||
<Heading size="medium">889.99 kr</Heading>
|
||||
</VStack>
|
||||
</Box>
|
||||
<Box
|
||||
background="danger-soft"
|
||||
padding="space-16"
|
||||
borderRadius="0 full full 0"
|
||||
>
|
||||
<VStack align="center">
|
||||
<Detail>Førpris</Detail>
|
||||
<Heading size="medium" style={{ textDecoration: "line-through" }}>
|
||||
399.99 kr
|
||||
</Heading>
|
||||
</VStack>
|
||||
</Box>
|
||||
</HStack>
|
||||
</>
|
||||
);
|
||||
|
||||
export const PaddingBreakpoints = {
|
||||
|
||||
@@ -149,7 +149,7 @@ export const Process: ProcessComponent = forwardRef<
|
||||
<ol
|
||||
ref={mergedRef}
|
||||
data-color="info"
|
||||
// oxlint-disable-next-line jsx-a11y/no-redundant-roles -- See comment above
|
||||
// biome-ignore lint/a11y/noRedundantRoles: See comment above
|
||||
role="list"
|
||||
{...restProps}
|
||||
className={cl("aksel-process", className)}
|
||||
@@ -218,6 +218,7 @@ export const ProcessEvent = forwardRef<HTMLLIElement, ProcessEventProps>(
|
||||
const { syncAriaControls, hideStatusText, rootId } = useProcessContext();
|
||||
|
||||
// syncAriaControls is already memoized with useCallback
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We want to run this only when status changes
|
||||
useEffect(syncAriaControls, [status]); // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const isActive = status === "active";
|
||||
|
||||
@@ -98,6 +98,7 @@ const TableAnimateHeightInternal: React.FC<AnimateHeightProps> = ({
|
||||
const [useTransitions, setUseTransitions] = useState<boolean>(false);
|
||||
const didHideOnMount = useRef(false);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: Only run this on mount
|
||||
useEffect(() => {
|
||||
/**
|
||||
* We check for multiple runs caused by React.StrictMode.
|
||||
@@ -117,6 +118,7 @@ const TableAnimateHeightInternal: React.FC<AnimateHeightProps> = ({
|
||||
}, []);
|
||||
|
||||
// ------------------ Height update
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: This should be explicitly run only on height change
|
||||
useEffect(() => {
|
||||
if (height !== prevHeight.current && contentElement.current) {
|
||||
showContent(contentElement.current, prevHeight.current);
|
||||
|
||||
@@ -30,7 +30,6 @@ const TabsList = forwardRef<HTMLDivElement, TabsListProps>(
|
||||
onClick={scrollCtx.scrollLeft}
|
||||
/>
|
||||
)}
|
||||
{/* oxlint-disable-next-line jsx-a11y/interactive-supports-focus -- Focus roves between child tabs; the tablist must not be an extra tab stop. */}
|
||||
<div
|
||||
ref={mergedRef}
|
||||
{...rest}
|
||||
|
||||
@@ -35,6 +35,7 @@ const TabsPanel = forwardRef<HTMLDivElement, TabsPanelProps>(
|
||||
{...rest}
|
||||
className={cl("aksel-tabs__tabpanel", className)}
|
||||
role="tabpanel"
|
||||
// biome-ignore lint/a11y/noNoninteractiveTabindex: TabPanel is a non-interactive element that requires a tabindex for keyboard navigation.
|
||||
tabIndex={0}
|
||||
aria-labelledby={rest["aria-labelledby"] ?? ctx.labelledbyId}
|
||||
id={id ?? ctx.id}
|
||||
|
||||
@@ -75,7 +75,7 @@ const TimelineRowBase = forwardRef<HTMLOListElement, TimelineRowProps>(
|
||||
<div className="aksel-timeline__row-label">{label}</div>
|
||||
))}
|
||||
|
||||
{/* oxlint-disable-next-line jsx-a11y/no-static-element-interactions -- onKeyDown just captures events on child-elements here. Regular interaction patterns still works as expected. */}
|
||||
{/** biome-ignore lint/a11y/noStaticElementInteractions: onKeyDown just captures events on child-elements here. Regular interaction patterns still works as expected. */}
|
||||
<div
|
||||
className={cl("aksel-timeline__row", {
|
||||
"aksel-timeline__row--active": activeRow === elementRefState,
|
||||
|
||||
+1
@@ -1,6 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
||||
import type { AkselColorRole } from "@navikt/ds-tokens/types";
|
||||
|
||||
// biome-ignore lint/suspicious/noEmptyInterface: Users can/will augment this interface
|
||||
export interface CustomAkselColor {}
|
||||
|
||||
export type AkselColor = AkselColorRole | keyof CustomAkselColor;
|
||||
|
||||
@@ -7,9 +7,11 @@ import React, { useEffect, useState } from "react";
|
||||
|
||||
let globalId = 0;
|
||||
function useGlobalId(idOverride?: string): string | undefined {
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: At runtime, `useGlobalId` is invariant.
|
||||
const [defaultId, setDefaultId] = useState(idOverride);
|
||||
const id = idOverride || defaultId;
|
||||
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: At runtime, `useGlobalId` is invariant.
|
||||
useEffect(() => {
|
||||
if (defaultId == null) {
|
||||
// Fallback to this default id when possible.
|
||||
@@ -37,6 +39,7 @@ function useId(idOverride?: string): string {
|
||||
const reactId = maybeReactUseId();
|
||||
return idOverride ?? reactId.replace(/(:)/g, "");
|
||||
}
|
||||
// biome-ignore lint/correctness/useHookAtTopLevel: `useId` is invariant at runtime.
|
||||
return useGlobalId(idOverride) ?? "";
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ const HighlightText = ({ text, children }: HighlightTextProps) => {
|
||||
const end = children.substring(textIndex + text.length);
|
||||
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 the browser
|
||||
<span aria-label={children}>
|
||||
{start}
|
||||
{match && <mark>{match}</mark>}
|
||||
|
||||
@@ -11,6 +11,7 @@ function ListboxGroup({ label, children }: ListboxGroupProps) {
|
||||
const labelId = useId();
|
||||
|
||||
return (
|
||||
// biome-ignore lint/a11y/useSemanticElements: Fieldset is not generic enough
|
||||
<div
|
||||
role="group"
|
||||
className="aksel-listbox__group"
|
||||
|
||||
@@ -23,7 +23,6 @@ const ListboxInputSlot = ({
|
||||
}
|
||||
// @ts-expect-error - You are meant to use an <input>, but Slot doesn't know that.
|
||||
autoComplete="off"
|
||||
// oxlint-disable-next-line jsx-a11y/role-has-required-aria-props -- Required ARIA attributes are supplied by the slotted input.
|
||||
role="combobox"
|
||||
data-aksel-listbox-input
|
||||
>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** biome-ignore-all lint/a11y/useKeyWithMouseEvents: We know what we are doing */
|
||||
import React from "react";
|
||||
import { cl } from "../../../helpers";
|
||||
import { useMedia } from "../../../hooks";
|
||||
@@ -20,7 +21,6 @@ function ListboxOptions({ children, ...rest }: ListboxOptionsProps) {
|
||||
className={cl(rest.className, "aksel-listbox__options")}
|
||||
role="listbox"
|
||||
tabIndex={-1}
|
||||
// oxlint-disable-next-line jsx-a11y/mouse-events-have-key-events -- Mouse hover updates virtual focus; the listbox wrapper handles keyboard navigation.
|
||||
onMouseOver={(event) => {
|
||||
if (cannotHover) {
|
||||
// onMouseOver can be triggered by touch, but we don't want virtual focus in that case.
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** biome-ignore-all lint/a11y/noStaticElementInteractions: We know what we are doing */
|
||||
import React, { forwardRef } from "react";
|
||||
import { useId } from "../../../../utils-external";
|
||||
import { cl } from "../../../helpers";
|
||||
@@ -41,7 +42,6 @@ const ListboxComponent = forwardRef<HTMLDivElement, ListboxProps>(
|
||||
};
|
||||
|
||||
return (
|
||||
// oxlint-disable-next-line jsx-a11y/no-static-element-interactions -- Wrapper handles keyboard navigation for the listbox and input within.
|
||||
<div
|
||||
ref={ref}
|
||||
className={cl("aksel-listbox", className)}
|
||||
|
||||
+4
-4
@@ -135,7 +135,7 @@ export const InteractOutside: Story = {
|
||||
<div
|
||||
data-testid="outside-focusable"
|
||||
style={{ padding: "1rem" }}
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- testing only
|
||||
// biome-ignore lint/a11y/noNoninteractiveTabindex: testing only
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
@@ -182,7 +182,7 @@ export const DisableOutsidePointerEvents: Story = {
|
||||
<div
|
||||
data-testid="outside-focusable"
|
||||
style={{ padding: "1rem" }}
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- testing only
|
||||
// biome-ignore lint/a11y/noNoninteractiveTabindex: testing only
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
@@ -212,7 +212,7 @@ export const SafeZone: Story = {
|
||||
return (
|
||||
<div>
|
||||
<div
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- testing only
|
||||
// biome-ignore lint/a11y/noNoninteractiveTabindex: testing only
|
||||
tabIndex={0}
|
||||
ref={setSafeAnchor}
|
||||
data-testid="safe-anchor"
|
||||
@@ -568,7 +568,7 @@ export const PreventEvents: Story = {
|
||||
<div
|
||||
data-testid="outside-focusable"
|
||||
style={{ padding: "1rem" }}
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- testing only
|
||||
// biome-ignore lint/a11y/noNoninteractiveTabindex: testing only
|
||||
tabIndex={0}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -365,6 +365,7 @@ const DismissableLayer = forwardRef<HTMLDivElement, DismissableLayerProps>(
|
||||
*
|
||||
* We depend on `enabled` to clean up when the layer is disabled.
|
||||
*/
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We need to clean up after enabled changes.
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (!node) {
|
||||
|
||||
@@ -36,7 +36,7 @@ function FocusGuards({
|
||||
<React.Fragment>
|
||||
<span
|
||||
ref={startRefCombined}
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- All focus guards are non-interactive and should be tabbable
|
||||
// biome-ignore lint/a11y/noNoninteractiveTabindex: All focus guards are non-interactive and should be tabbable
|
||||
tabIndex={0}
|
||||
style={visuallyHidden}
|
||||
data-aksel-focus-guard=""
|
||||
@@ -44,7 +44,7 @@ function FocusGuards({
|
||||
{children}
|
||||
<span
|
||||
ref={endRefCombined}
|
||||
// oxlint-disable-next-line jsx-a11y/no-noninteractive-tabindex -- All focus guards are non-interactive and should be tabbable
|
||||
// biome-ignore lint/a11y/noNoninteractiveTabindex: All focus guards are non-interactive and should be tabbable
|
||||
tabIndex={0}
|
||||
style={visuallyHidden}
|
||||
data-aksel-focus-guard=""
|
||||
|
||||
@@ -223,6 +223,7 @@ const TextareaAutosize = forwardRef<HTMLTextAreaElement, TextareaAutosizeProps>(
|
||||
syncHeight();
|
||||
});
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: Since value is an external prop, we want to reset the renders on every time it changes, even when it is undefined or empty.
|
||||
useEffect(() => {
|
||||
renders.current = 0;
|
||||
}, [value]);
|
||||
|
||||
@@ -45,7 +45,9 @@ export const parseDate = (
|
||||
}
|
||||
}
|
||||
|
||||
for (const format of ALLOWED_FORMATS.map((x) => x.replace("yyyy", "yy"))) {
|
||||
for (const format of [
|
||||
...ALLOWED_FORMATS.map((x) => x.replace("yyyy", "yy")),
|
||||
]) {
|
||||
parsed = parse(date, format, today, { locale });
|
||||
if (
|
||||
isValidDate(parsed) &&
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** biome-ignore-all lint/suspicious/noAssignInExpressions: Because why not? */
|
||||
type ClassValue =
|
||||
| ClassArray
|
||||
| ClassDictionary
|
||||
|
||||
@@ -65,6 +65,7 @@ function createStrictContext<T>(options: {
|
||||
Context.displayName = name;
|
||||
|
||||
function Provider({ children, ...context }: ProviderProps<T>) {
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: Object.values(context) includes all dependencies.
|
||||
const value = React.useMemo(() => context, Object.values(context)) as T; // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
return <Context.Provider value={value}>{children}</Context.Provider>;
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
/* eslint-disable react-hooks/set-state-in-effect */
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
/** biome-ignore-all lint/correctness/useExhaustiveDependencies: Explained bellow */
|
||||
/* eslint-disable react-hooks/immutability */
|
||||
/* eslint-disable react-hooks/globals */
|
||||
/** biome-ignore-all lint/complexity/useLiteralKeys: Explained bellow */
|
||||
/**
|
||||
* Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
*
|
||||
|
||||
@@ -285,7 +285,7 @@ async function buildOtherTokenFormats() {
|
||||
},
|
||||
});
|
||||
|
||||
await SDDictionaryNonCSSFormats.hasInitialized;
|
||||
await Promise.all([SDDictionaryNonCSSFormats.hasInitialized]);
|
||||
|
||||
SDDictionaryNonCSSFormats.registerTransform(transformCSS);
|
||||
|
||||
@@ -372,7 +372,7 @@ async function buildCSSBundleForTokens({
|
||||
},
|
||||
});
|
||||
|
||||
await SDictionary.hasInitialized;
|
||||
await Promise.all([SDictionary.hasInitialized]);
|
||||
|
||||
SDictionary.registerTransform(transformCSS);
|
||||
await SDictionary.buildAllPlatforms();
|
||||
|
||||
@@ -30,7 +30,7 @@ For å komme i gang, se [Aksel dokumentasjonen](https://aksel.nav.no/grunnleggen
|
||||
| `yarn dev` | Starter dokumentasjonssiden |
|
||||
| `yarn storybook` | Starter Storybook |
|
||||
| `yarn test` | Kjører alle tester |
|
||||
| `yarn lint` | Kjører ESLint, Stylelint og Oxlint |
|
||||
| `yarn lint` | Kjører ESLint, Stylelint og Biome |
|
||||
| `yarn clean` | Fjerner genererte filer |
|
||||
| `yarn changeset` | Oppretter en ny versjon. |
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ function AkselDescriptionList({
|
||||
{...rest}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
// oxlint-disable-next-line react/no-array-index-key -- Handle more gracefully in the future
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Handle more gracefully in the future
|
||||
<ItemWrapper key={index}>
|
||||
<BodyLong
|
||||
as="dt"
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// biome-ignore lint/complexity/noStaticOnlyClass: single config namespace accessed via SchemaConfig.*
|
||||
class SchemaConfig {
|
||||
static readonly allArticleDocuments = [
|
||||
"komponent_artikkel",
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ function IconDetails({
|
||||
iconName?: keyof typeof meta;
|
||||
iconSvg?: string;
|
||||
}) {
|
||||
// oxlint-disable-next-line import/namespace -- We do not know which icon will be rendered at build time
|
||||
// biome-ignore lint/performance/noDynamicNamespaceImportAccess: We do not know which icon will be rendered at build time
|
||||
const IconComponent = Icons[`${iconName}Icon`];
|
||||
const metaData = iconName ? meta[iconName] : null;
|
||||
|
||||
|
||||
@@ -142,7 +142,7 @@ function IconPage({
|
||||
|
||||
<HStack gap="space-8" marginBlock="space-16 space-0">
|
||||
{section.icons.map((icon) => {
|
||||
// oxlint-disable-next-line import/namespace -- We do not know which icon will be rendered at build time
|
||||
// biome-ignore lint/performance/noDynamicNamespaceImportAccess: We do not know which icon will be rendered at build time
|
||||
const T = Icons[`${icon.id}Icon`];
|
||||
if (T === undefined) {
|
||||
return null;
|
||||
|
||||
+1
@@ -96,6 +96,7 @@ export const SearchForm = ({
|
||||
|
||||
const { years, categories } = params;
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We only want to run this when urlParams changes
|
||||
useEffect(() => {
|
||||
if (!urlParams.has("fritekst")) {
|
||||
// In case you clear the URL params by clicking on "Endringslogg" again in the menu
|
||||
|
||||
+1
@@ -25,6 +25,7 @@ const TokenCategory = ({
|
||||
const searchParams = useSearchParams();
|
||||
const tokenQuery = searchParams?.get("tokenQuery") as string | undefined;
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We want to reset selectedRole when tokenQuery changes
|
||||
useEffect(() => {
|
||||
// eslint-disable-next-line react-hooks/set-state-in-effect
|
||||
setSelectedRole(null);
|
||||
|
||||
@@ -32,7 +32,7 @@ async function GodPraksisPictogram({
|
||||
|
||||
return (
|
||||
<div
|
||||
// oxlint-disable-next-line react/no-danger -- We trust the SVG content and sanitize it before using it.
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: We trust the SVG content and sanitize it before using it.
|
||||
dangerouslySetInnerHTML={{ __html: cleanedSvg }}
|
||||
data-active={active}
|
||||
aria-hidden
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable @typescript-eslint/no-empty-object-type */
|
||||
// biome-ignore lint/complexity/noBannedTypes: This is the closest we get to the correct default type (empty object)
|
||||
export type PageProps<T extends object = {}> = {
|
||||
params: Promise<T>;
|
||||
searchParams: Promise<{ [key: string]: string | undefined }>;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** biome-ignore-all lint/suspicious/noFocusedTests: False positive */
|
||||
import { createImageUrlBuilder } from "@sanity/image-url";
|
||||
import { SANITY_DATASET, SANITY_PROJECT_ID } from "aksel-sanity-studio/env";
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ export const AvatarStack = ({
|
||||
<HStack as="ul" aria-hidden={showNames}>
|
||||
{avatars.map((avatar, idx) => {
|
||||
return (
|
||||
// oxlint-disable-next-line react/no-array-index-key -- Fix gracefully in the future
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Fix gracefully in the future
|
||||
<li key={idx} className={styles.avatarItem}>
|
||||
{avatar}
|
||||
</li>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
/* eslint-disable @next/next/no-img-element */
|
||||
/** biome-ignore-all lint/performance/noImgElement: img is fine here */
|
||||
import { BodyLong, HGrid, Link } from "@navikt/ds-react";
|
||||
import type { ExtractPortableComponentProps } from "@/app/_sanity/types";
|
||||
import { urlForImage } from "@/app/_sanity/utils";
|
||||
|
||||
@@ -31,6 +31,7 @@ function ChangelogDialog(props: ChangelogDialogProps) {
|
||||
const searchParams = useSearchParams();
|
||||
const firstRenderRef = useRef(true);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We only want to trigger this effect on URL change, not on initial render, and router object is stable
|
||||
useEffect(() => {
|
||||
if (firstRenderRef.current) {
|
||||
firstRenderRef.current = false;
|
||||
|
||||
@@ -148,7 +148,7 @@ function CodeBlockEditor(props: {
|
||||
|
||||
return (
|
||||
<div
|
||||
// oxlint-disable-next-line react/no-array-index-key -- Handle more gracefully in the future
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Handle more gracefully in the future
|
||||
key={i}
|
||||
{...lineProps}
|
||||
style={{ ...lineProps.style, "--line": `"${i + 1}"` }}
|
||||
@@ -163,7 +163,7 @@ function CodeBlockEditor(props: {
|
||||
const tokenProps = { ...getTokenProps({ token }) };
|
||||
return (
|
||||
<span
|
||||
// oxlint-disable-next-line react/no-array-index-key -- Handle more gracefully in the future
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Handle more gracefully in the future
|
||||
key={key}
|
||||
{...tokenProps}
|
||||
style={{
|
||||
|
||||
@@ -34,6 +34,7 @@ function CompareImages(props: ExtractPortableComponentProps<"compare_images">) {
|
||||
className={styles.compareImagesImgContainer}
|
||||
data-position="left"
|
||||
>
|
||||
{/** biome-ignore lint/performance/noImgElement: Img element is fine here */}
|
||||
<img
|
||||
src={imageOneUrl}
|
||||
alt={image_1.alt}
|
||||
@@ -44,6 +45,7 @@ function CompareImages(props: ExtractPortableComponentProps<"compare_images">) {
|
||||
className={styles.compareImagesImgContainer}
|
||||
data-position="right"
|
||||
>
|
||||
{/** biome-ignore lint/performance/noImgElement: Img elemnt is fine here */}
|
||||
<img
|
||||
src={imageTwoUrl}
|
||||
alt={image_2.alt}
|
||||
|
||||
@@ -34,7 +34,7 @@ function AkselDescriptionList({
|
||||
{...rest}
|
||||
>
|
||||
{items.map((item, index) => (
|
||||
// oxlint-disable-next-line react/no-array-index-key -- Handle more gracefully in the future
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Handle more gracefully in the future
|
||||
<ItemWrapper key={index}>
|
||||
<BodyLong
|
||||
as="dt"
|
||||
|
||||
@@ -39,6 +39,7 @@ function DoDont(props: ExtractPortableComponentProps<"do_dont">) {
|
||||
className={styles.doDont}
|
||||
>
|
||||
<Notch block={block} />
|
||||
{/** biome-ignore lint/performance/noImgElement: Img element is fine here */}
|
||||
<img
|
||||
className={`light ${styles.doDontImage}`}
|
||||
alt={block.alt}
|
||||
|
||||
@@ -99,7 +99,7 @@ function AsciiTree({ right = false }: { right?: boolean }) {
|
||||
>
|
||||
<pre style={{ lineHeight: "8px", fontSize: "8px" }}>
|
||||
{lines.map((line, idx) => (
|
||||
// oxlint-disable-next-line react/no-array-index-key -- Handle more gracefully in the future
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Handle more gracefully in the future
|
||||
<span key={idx}>
|
||||
{" "}
|
||||
{line}
|
||||
|
||||
@@ -33,7 +33,7 @@ async function ImageAsThemedSvg({
|
||||
|
||||
return (
|
||||
<div
|
||||
// oxlint-disable-next-line react/no-danger -- We purify the SVG string, so its safer to use locally here.
|
||||
// biome-ignore lint/security/noDangerouslySetInnerHtml: We purify the SVG string, so its safer to use locally here.
|
||||
dangerouslySetInnerHTML={{ __html: cleanedSvg }}
|
||||
aria-hidden
|
||||
style={{ "--website-svg-size": `${size}px` }}
|
||||
|
||||
@@ -35,13 +35,13 @@ function MetadataSeksjonCode({
|
||||
<code>
|
||||
{tokens.map((line, i) => (
|
||||
<div
|
||||
// oxlint-disable-next-line react/no-array-index-key -- Handle more gracefully in the future
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Handle more gracefully in the future
|
||||
key={i}
|
||||
{...getLineProps({ line })}
|
||||
className={styles.propsSeksjonCode}
|
||||
>
|
||||
{line.map((token, key) => (
|
||||
// oxlint-disable-next-line react/no-array-index-key -- Handle more gracefully in the future
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Handle more gracefully in the future
|
||||
<span key={key} {...getTokenProps({ token })} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
@@ -29,7 +29,7 @@ function TableV2(props: ExtractPortableComponentProps<"tabell_v2">) {
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{header?.map((cell, y) => (
|
||||
// oxlint-disable-next-line react/no-array-index-key -- Handle more gracefully in the future
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Handle more gracefully in the future
|
||||
<TableHeaderCell key={y} scope="col">
|
||||
{cell}
|
||||
</TableHeaderCell>
|
||||
@@ -40,7 +40,7 @@ function TableV2(props: ExtractPortableComponentProps<"tabell_v2">) {
|
||||
{content?.map((row) => (
|
||||
<TableRow key={row?._key}>
|
||||
{row?.cells?.map((cell, y) => (
|
||||
// oxlint-disable-next-line react/no-array-index-key -- Handle more gracefully in the future
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Handle more gracefully in the future
|
||||
<TableDataCell key={y}>{cell}</TableDataCell>
|
||||
))}
|
||||
</TableRow>
|
||||
|
||||
@@ -65,7 +65,7 @@ function TableOfContents({
|
||||
<ul
|
||||
className={`${styles.tocMenuUl} ${styles.hideScrollbar}`}
|
||||
id="toc-scroll-wrapper"
|
||||
// oxlint-disable-next-line jsx-a11y/no-redundant-roles -- WebKit browsers remove list semantics when list-style-type is none
|
||||
// biome-ignore lint/a11y/noRedundantRoles: WebKit browsers remove list semantics when list-style-type is none
|
||||
role="list"
|
||||
>
|
||||
{toc.map((node) => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/** biome-ignore-all lint/suspicious/noAssignInExpressions: Because why not? */
|
||||
type ClassValue =
|
||||
| ClassArray
|
||||
| ClassDictionary
|
||||
|
||||
Loaded 100 of 114 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user