mirror of
https://github.com/navikt/aksel.git
synced 2026-11-08 13:54:05 +00:00
Compare commits
43
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c9663dbb10 | ||
|
|
8a6de24284 | ||
|
|
c4322cace2 | ||
|
|
b9ae4832df | ||
|
|
243489af5c | ||
|
|
fe37b0b075 | ||
|
|
2469120f78 | ||
|
|
a536846c13 | ||
|
|
400ad15aa9 | ||
|
|
48767feb97 | ||
|
|
a7d1a01275 | ||
|
|
d9725ede5e | ||
|
|
f0a05a111d | ||
|
|
7025566613 | ||
|
|
ff02846846 | ||
|
|
473ac7a481 | ||
|
|
44f741d15c | ||
|
|
95e0d36605 | ||
|
|
89c86f731d | ||
|
|
b627225c8c | ||
|
|
0b741c4168 | ||
|
|
adbc05012f | ||
|
|
a3412d13b8 | ||
|
|
945672e126 | ||
|
|
d52b156bd2 | ||
|
|
122d0c3bd8 | ||
|
|
7bee95f190 | ||
|
|
746f8a514c | ||
|
|
8f37601c22 | ||
|
|
c53f3d42f2 | ||
|
|
eb270dee6c | ||
|
|
661ab81055 | ||
|
|
8176159439 | ||
|
|
58aaceaa49 | ||
|
|
319bc09eeb | ||
|
|
4bf535b311 | ||
|
|
882b3e1bcf | ||
|
|
de34578da9 | ||
|
|
c4b3fc0b23 | ||
|
|
03643665be | ||
|
|
8b63010549 | ||
|
|
4de40e0dc6 | ||
|
|
2c5d5f0c60 |
No files matched your search
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/aksel-icons": minor
|
||||
---
|
||||
|
||||
Icons: Add 46 icons for text editing, tables, keyboard input, and filtering.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@navikt/ds-react": minor
|
||||
"@navikt/ds-css": patch
|
||||
---
|
||||
|
||||
DataGrid: Adds client- and server-side pagination through the `pagination` prop and `DataGrid.Pagination` controls.
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"@navikt/ds-react": minor
|
||||
"@navikt/ds-css": minor
|
||||
---
|
||||
|
||||
DataGrid: Add top and bottom content areas outside the table's scrollable viewport.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/ds-react": patch
|
||||
---
|
||||
|
||||
DataGridTable: Restore keyboard navigation position after loading.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/ds-react": patch
|
||||
---
|
||||
|
||||
DatePicker: Preserve keyboard focus when controlled month changes replace the focused day.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
"@navikt/aksel-icons": minor
|
||||
---
|
||||
|
||||
Icons: All icons are now available as CSS custom properties. Import `@navikt/aksel-icons/css/<IconName>.css` for a single icon (e.g. `--aksel-icon-magnifying-glass`), or `@navikt/aksel-icons/css/index.css` for every icon.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/ds-react": patch
|
||||
---
|
||||
|
||||
Dialog, Lookup: Avoid setting aria-hidden on elements with role=alert/status when open
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/ds-react": patch
|
||||
---
|
||||
|
||||
DataGrid: Fix focus getting trapped when pressing Shift+Tab from the first cell.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@navikt/ds-css": patch
|
||||
---
|
||||
|
||||
DataTable: Constrain details panel width to the visible table area.
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Meld inn bugs eller feil
|
||||
title: "[BUG] "
|
||||
title: "[BUG]"
|
||||
labels: "bug \U0001F41B"
|
||||
assignees: ""
|
||||
---
|
||||
@@ -21,7 +21,7 @@ assignees: ""
|
||||
## Steg for å reprodusere problemet (valgfri)
|
||||
|
||||
<!--
|
||||
1. Steg én
|
||||
1. Steg en
|
||||
2. Steg to
|
||||
3. ....
|
||||
-->
|
||||
@@ -31,10 +31,10 @@ assignees: ""
|
||||
## Eksempel/Tilleggsinformasjon (valgfri)
|
||||
|
||||
<!--
|
||||
Screenshots, kodesnippets eller sandbox-lenke.
|
||||
Legg gjerne ved info som nettleser eller desktop/mobil hvis det er relevant.
|
||||
Screenshots eller kodesnippets
|
||||
Legg gjerne ved ekstrainfo som nettleser eller desktop/mobil hvis det er relevant.
|
||||
-->
|
||||
|
||||
```
|
||||
Hvis mulig, legg gjerne til kode her, ev. lenke til kode i repo.
|
||||
Hvis mulig, legg gjerne til kode her, evt lenke til kode i repo
|
||||
```
|
||||
@@ -1,8 +1,8 @@
|
||||
---
|
||||
name: Feature Request
|
||||
about: Foreslå noe nytt!
|
||||
title: "[NEW] "
|
||||
labels: nytt ✨
|
||||
title: "[NEW]"
|
||||
labels: new ✨
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Eksempel på bruk
|
||||
description: Send et eksempel på ting fra Aksel som er brukt i det fri (tokens, komponenter, maler, etc.)
|
||||
description: Send et eksempel på ting fra Aksel som er brukt i det fri (tokens, komponenter, maler, etc)
|
||||
title: "[Eksempel på bruk]: "
|
||||
labels: ["dokumentasjon 📖"]
|
||||
body:
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Ny komponent
|
||||
description: Send et ønske om én eller flere nye komponenter
|
||||
description: Send et ønske om en eller flere nye komponenter
|
||||
title: "[Ny komponent]: "
|
||||
labels: ["nytt ✨", "forespørsel 🥰", "komponenter 🧩"]
|
||||
body:
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
name: Bug Report
|
||||
about: Meld inn bugs eller feil
|
||||
title: "[BUG]"
|
||||
labels: "bug \U0001F41B"
|
||||
assignees: ""
|
||||
---
|
||||
|
||||
## Hva forventet du å finne for dette søket?
|
||||
@@ -1,5 +1,5 @@
|
||||
name: Innspill til komponent
|
||||
description: Send inn tilbakemelding på komponent
|
||||
description: Send inn innspill til komponent
|
||||
title: "[Innspill til komponent]: "
|
||||
labels: ["forespørsel 🥰", "komponenter 🧩", "nytt ✨", "beta 🧪"]
|
||||
body:
|
||||
@@ -8,7 +8,7 @@ body:
|
||||
value: |
|
||||
Takk for at du bidrar inn i Aksel!
|
||||
- type: textarea
|
||||
id: feedback
|
||||
id: feeback
|
||||
attributes:
|
||||
label: Tilbakemelding
|
||||
description: Beskriv hvilke tilbakemeldinger du har til komponenten
|
||||
@@ -1,6 +1,6 @@
|
||||
name: Innspill til ikon
|
||||
description: Send inn tilbakemelding på ikon
|
||||
title: "[Innspill til ikon]: "
|
||||
description: Send inn innspill til ikon
|
||||
title: "[Tilbakemelding på ikon]: "
|
||||
labels: ["forespørsel 🥰", "ikoner 🖼️"]
|
||||
body:
|
||||
- type: markdown
|
||||
@@ -8,7 +8,7 @@ body:
|
||||
value: |
|
||||
Takk for at du bidrar inn i Aksel!
|
||||
- type: textarea
|
||||
id: feedback
|
||||
id: feeback
|
||||
attributes:
|
||||
label: Tilbakemelding
|
||||
description: Beskriv hvilke tilbakemeldinger du har til ikonet
|
||||
@@ -61,14 +61,8 @@ runs:
|
||||
SANITY_AUTH_TOKEN: ${{ inputs.sanity_deploy_studio }}
|
||||
shell: bash
|
||||
|
||||
- name: Generate RSS feed
|
||||
run: yarn workspace website update:rss
|
||||
env:
|
||||
SANITY_READ_NO_DRAFTS: ${{ inputs.sanity_read_no_drafts_token }}
|
||||
shell: bash
|
||||
|
||||
- name: Build Next.js website
|
||||
run: yarn workspace website build:with-playroom
|
||||
run: yarn build:next
|
||||
env:
|
||||
SANITY_READ: ${{ inputs.sanity_read_token }}
|
||||
SANITY_READ_NO_DRAFTS: ${{ inputs.sanity_read_no_drafts_token }}
|
||||
|
||||
@@ -10,10 +10,11 @@
|
||||
|
||||
## Behavior
|
||||
|
||||
- Respond in the language the user prompted in, unless they request another language.
|
||||
- Use concise, plain language without sacrificing technical precision or completeness.
|
||||
- Lead with the outcome. Avoid filler and unnecessary apologies; summarize meaningful changes and validation when useful.
|
||||
- Do not narrate routine tool use. Explain decisions, blockers, uncertainty, and tradeoffs when relevant.
|
||||
- Only respond in same language as the input. Prefer English always.
|
||||
- Use caveman-skill unless asked not to.
|
||||
- No apologies, no "I think", no change summaries.
|
||||
- Answer-first. 0-2 sentences max.
|
||||
- No thinking aloud; do next edit. Explain only when asked or non-obvious.
|
||||
- Show only what changes. Minimal diffs; don't paste whole files.
|
||||
- Edit only the files the change needs (see Definition of done); no unrelated files. Ask only when truly blocked.
|
||||
- Favor perf/security; avoid re-renders, top-level side effects, unsafe HTML, env var leaks.
|
||||
@@ -21,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, 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 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
|
||||
@@ -38,18 +39,18 @@
|
||||
|
||||
## Definition of done
|
||||
|
||||
- Behavior/visual change: update the implementation and closest existing story. Prefer Storybook play tests for UI behavior and unit tests for small utilities; combine overlapping coverage into existing stories instead of adding redundant stories.
|
||||
- Behavior/visual change: update impl + closest story + test.
|
||||
- New public export: sync component `index.ts`, `src/index.ts` (or `src/preview.ts`) and `package.json` exports, then `corepack yarn workspace aksel-playroom sync-imports` (root `lint` fails on stale playroom imports).
|
||||
- User-facing change to a published package: add a changeset (`add-changeset` skill) unless told otherwise.
|
||||
|
||||
## 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 eslint --max-warnings=0 --no-warn-ignored <files...>` / `corepack yarn stylelint <files...>` / `corepack yarn biome lint --no-errors-on-unmatched <files...>`
|
||||
- `corepack yarn tsc --noEmit --incremental false --project <tsconfig>` - targeted type-check
|
||||
- `corepack yarn workspace <name> <script>` - e.g. `@navikt/ds-react test` (Vitest + tsc), `website test`
|
||||
- `corepack yarn lint` / `corepack yarn test` / `corepack yarn boot` - root suites (slow; cross-workspace changes only)
|
||||
- `corepack yarn storybook` (:6006) / `corepack yarn storybook:aksel` (website examples, :6007, no secrets) / `corepack yarn storybook:test` (play tests, Playwright Firefox; Chromium when `COPILOT_CLI` is set) / `corepack yarn dev` (website, :3000, needs Sanity token)
|
||||
- Targeted play tests: `corepack yarn storybook:test <path/to/X.tests.stories.tsx>` (repo root). Run after changing play tests. Under Copilot CLI this runs Chromium; if the browser can't launch, report tests as not run with the exact command.
|
||||
- Targeted play tests: `corepack yarn vitest run --config ./vitest.config.ts --project=storybook <path/to/X.tests.stories.tsx>` (repo root). Run after changing play tests. Under Copilot CLI this runs Chromium; if the browser can't launch, report tests as not run with the exact command.
|
||||
|
||||
## Where to edit
|
||||
|
||||
@@ -63,13 +64,12 @@ 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: `eslint.config.js`, `stylelint.config.mjs`, `biome.json`, `vitest.config.ts`, `.storybook/`
|
||||
|
||||
## Skills and agents
|
||||
|
||||
- `aksel-local-validation` - pick and run the right lint/type/test commands
|
||||
- `code-review` - review a branch, diff or PR for consumer-facing risk
|
||||
- `component-contract` - define and verify component behavior, state ownership and accessibility contracts
|
||||
- `add-changeset` - write `.changeset/*.md` (never run interactive `yarn changeset`)
|
||||
- `ds-component-restructure` - move/rename component files to canonical layout
|
||||
- `website-example` - add aksel.nav.no examples/templates
|
||||
|
||||
@@ -19,6 +19,7 @@ updates:
|
||||
applies-to: version-updates
|
||||
patterns:
|
||||
- "@next/eslint-plugin-next"
|
||||
- "@next/bundle-analyzer"
|
||||
- "next"
|
||||
sanity:
|
||||
applies-to: version-updates
|
||||
@@ -52,6 +53,7 @@ updates:
|
||||
applies-to: security-updates
|
||||
patterns:
|
||||
- "@next/eslint-plugin-next"
|
||||
- "@next/bundle-analyzer"
|
||||
- "next"
|
||||
sanity-security:
|
||||
applies-to: security-updates
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
description: >
|
||||
Ultra-compressed communication mode. Cuts token usage ~75% by speaking like caveman
|
||||
while keeping full technical accuracy. Supports intensity levels: lite, full (default), ultra.
|
||||
Always use caveman for code-related questions unless spesificed otherwise. For non-code topics, use caveman if user seems to want quick, concise answers.
|
||||
applyTo: "**"
|
||||
---
|
||||
|
||||
# caveman
|
||||
|
||||
ACTIVE EVERY RESPONSE.
|
||||
|
||||
default: full
|
||||
|
||||
switch mode:
|
||||
|
||||
- "/caveman lite" → lite
|
||||
- "/caveman full" → full
|
||||
- "/caveman ultra" → ultra
|
||||
- "stop caveman" | "normal mode" → off
|
||||
|
||||
persist: keep last state
|
||||
|
||||
## intensity
|
||||
|
||||
| Level | What change |
|
||||
| --------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| **lite** | short sentences, no filler, keep grammar |
|
||||
| **full** | fragments ok, drop articles, short words. Classic caveman |
|
||||
| **ultra** | Abbreviate (DB/auth/config/req/res/fn/impl), fragments, abbrev (db/api/req/res/fn), arrows (→), minimal words |
|
||||
|
||||
Example - "Why React component re-render?"
|
||||
|
||||
- lite: "Your component re-renders because you create a new object reference each render. Wrap it in `useMemo`."
|
||||
- full: "New object ref each render. Inline object prop = new ref = re-render. Wrap in `useMemo`."
|
||||
- ultra: "Inline obj prop → new ref → re-render. `useMemo`."
|
||||
|
||||
## global rules
|
||||
|
||||
- no filler (just/really/basically/etc)
|
||||
- no pleasantries
|
||||
- no hedging
|
||||
- keep tech exact
|
||||
- no long sentences
|
||||
- prefer symbols (→, =)
|
||||
|
||||
## patterns
|
||||
|
||||
Patterns: `[thing] [action] [reason]. [next step].`
|
||||
|
||||
## examples
|
||||
|
||||
lite:
|
||||
"Component re-renders because you create a new object each render. Use useMemo."
|
||||
|
||||
full:
|
||||
"New object each render → new ref → re-render. useMemo."
|
||||
|
||||
ultra:
|
||||
"inline obj → new ref → re-render. fix: useMemo."
|
||||
|
||||
## auto-Clarity
|
||||
|
||||
Drop caveman for: security warnings, irreversible action confirmations, multi-step sequences where fragment order risks misread, user asks to clarify or repeats question. Resume caveman after clear part done.
|
||||
|
||||
Example - destructive op:
|
||||
|
||||
> **Warning:** This will permanently delete all rows in the `users` table and cannot be undone.
|
||||
>
|
||||
> ```sql
|
||||
> DROP TABLE users;
|
||||
> ```
|
||||
>
|
||||
> Caveman resume. Verify backup exist first.
|
||||
|
||||
## boundaries
|
||||
|
||||
Code/commits/PRs: write normal. "stop caveman" or "normal mode": revert. Level persist until changed or session end
|
||||
@@ -7,8 +7,6 @@ applyTo: "@navikt/core/react/src/**"
|
||||
|
||||
Read the whole component folder (`index.ts`, source, `.meta.ts`, stories, tests) before editing. Moving or renaming files: use the `ds-component-restructure` skill.
|
||||
|
||||
For changes to component state, callbacks, async behavior, composition, semantics, keyboard or focus behavior, use the [component-contract skill](../skills/component-contract/SKILL.md).
|
||||
|
||||
## 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/`.
|
||||
|
||||
@@ -6,15 +6,10 @@ applyTo: "**/*.test.ts,**/*.test.tsx,**/*.stories.tsx"
|
||||
# Tests and stories
|
||||
|
||||
- User flows: use a story `play` function in `<Component>.tests.stories.tsx`. Pure logic and hooks: use Vitest `*.test.ts(x)` next to the source file.
|
||||
- `corepack yarn storybook:test` runs every story that has a `play` function (Storybook auto-tags them `play-fn`, see root `vitest.config.mts`). To opt out, use `tags: ["skip-test"]` with a reason.
|
||||
- `corepack yarn storybook:test` runs every story that has a `play` function (Storybook auto-tags them `play-fn`, see root `vitest.config.ts`). To opt out, use `tags: ["!play-fn", "skip-test"]` with a reason.
|
||||
- Play-test files set `parameters: { chromatic: { disable: true } }`. Visual stories feed Chromatic; use `renderStoriesForChromatic` from `utils/` when adding visual variants.
|
||||
- Import from `@storybook/react-vite` and `storybook/test`, not `@storybook/react` or `@storybook/test`.
|
||||
- Query by role and accessible name (`getByRole("button", { name })`). Avoid snapshots unless nearby tests already use them.
|
||||
- Cover the main path and meaningful edge cases in the nearest existing story/play-test where they belong. Prefer combining overlapping cases over adding redundant stories; add a separate story for a distinct user-visible scenario.
|
||||
- A behavior change updates the nearest story and test in the same change. Use unit tests for small utilities and pure logic; use Storybook play tests for UI behavior.
|
||||
- Describe behavioral coverage as starting state → action → observable result. Assert relevant intermediate states so a no-op cannot pass as success; pair a negative/blocked case with a successful control when it helps distinguish the behavior.
|
||||
- Match assertions to timing: assert synchronous outcomes immediately and wait only for behavior that is contractually eventual. When changing ARIA relationships, verify the referenced element and resulting relationship, not just that an attribute exists.
|
||||
- Before removing or consolidating tests, map each meaningful old scenario/assertion to its replacement story or test. Keep coverage that still represents a supported behavior; explain when a scenario is intentionally obsolete.
|
||||
- For state, accessibility, keyboard or focus behavior, use the [component-contract skill](../skills/component-contract/SKILL.md) to identify the contract and evidence to preserve.
|
||||
- Cover the main path plus 1-2 meaningful edge cases. A behavior change updates the nearest story and test in the same change.
|
||||
- ds-react unit tests run with `TZ=UTC`. Date logic must not depend on the local timezone.
|
||||
- Browser tests need Playwright Firefox (Chromium under Copilot CLI, see `vitest.config.mts`). Yarn has `enableScripts: false`, so if the browser is missing, report it instead of installing globally.
|
||||
- Browser tests need Playwright Firefox (Chromium under Copilot CLI, see `vitest.config.ts`). Yarn has `enableScripts: false`, so if the browser is missing, report it instead of installing globally.
|
||||
@@ -7,7 +7,6 @@ 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.
|
||||
- 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`.
|
||||
|
||||
@@ -57,7 +57,6 @@ Read the current manifest before running a table command. A manifest is source o
|
||||
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>
|
||||
|
||||
@@ -14,27 +14,12 @@ argument-hint: "PR number, branch or files to review"
|
||||
|
||||
## Process
|
||||
|
||||
1. **Scope**: identify what the user wants reviewed.
|
||||
- PR: get `gh pr diff <n>` and the PR description. Note the stated goal.
|
||||
- Branch: compare against its stated base (for example, `git diff origin/main...HEAD`).
|
||||
- Current worktree or "my changes": inspect staged, unstaged, and relevant untracked files. Do not substitute the committed branch diff for worktree changes.
|
||||
|
||||
```sh
|
||||
git status --short
|
||||
git diff
|
||||
git diff --cached
|
||||
git ls-files --others --exclude-standard
|
||||
```
|
||||
|
||||
Read in-scope untracked files too. If the requested review target is unclear, ask before choosing a scope.
|
||||
|
||||
1. **Scope**: get the diff (`gh pr diff <n>` or `git diff origin/main...HEAD`) and the PR description. Note the stated goal.
|
||||
2. **Read**: for every changed file, read the surrounding code, component `index.ts`, stories, tests and the matching `.github/instructions/*.instructions.md`. Never review from the diff alone.
|
||||
3. **Check**: run the smallest validation that covers the change (`aksel-local-validation` skill). Tool output replaces style comments.
|
||||
4. **Analyze** against the checklist below.
|
||||
5. **Report** in the output format.
|
||||
|
||||
For changes to component state, callbacks, async behavior, composition, semantics, keyboard or focus behavior, use the [component-contract skill](../component-contract/SKILL.md) to trace the affected contract and identify observable evidence.
|
||||
|
||||
## Priorities
|
||||
|
||||
- 🔴 **Blocker**: must fix before merge. Breaks consumers, regresses accessibility, security issue or wrong behavior.
|
||||
@@ -63,7 +48,6 @@ Skip anything ESLint, Stylelint, Biome, Prettier or TypeScript already catches.
|
||||
- Full keyboard support, logical focus order, focus returned after close, visible `:focus-visible`.
|
||||
- Works in `forced-colors`, dark theme and at 200% zoom. Motion relies on the global `prefers-reduced-motion` handling.
|
||||
- User-facing strings use `useI18n`, with keys in `nb`, `nn` and `en` under `@navikt/core/react/src/utils/i18n/locales/`.
|
||||
- Review only the interaction pattern in scope; do not apply modal focus or dismissal rules to unrelated popup patterns. Check changed accessible names and relationships against their rendered targets.
|
||||
|
||||
### Wiring (🔴 for new public components)
|
||||
|
||||
@@ -88,15 +72,8 @@ Skip anything ESLint, Stylelint, Biome, Prettier or TypeScript already catches.
|
||||
- Complex logic with a higher risk of breaking has a story `play` test or Vitest test proving the user-facing contract. Queries use role and accessible name.
|
||||
- Tests are deterministic: no dependence on timezone, current time or randomness.
|
||||
- Edge cases that are easy to miss: controlled vs. uncontrolled, `ref`/`className`/`...rest` forwarding, `as` prop, empty and very long content.
|
||||
- For behavior changes, check that tests distinguish the intended outcome from a no-op and cover relevant intermediate states. When tests are consolidated or removed, verify meaningful old scenarios have replacement coverage.
|
||||
- Website examples (`aksel.nav.no/website/pages/eksempler/`) are updated when usage changes.
|
||||
|
||||
### Component contracts and public consumer impact
|
||||
|
||||
- `COMP-01` **Contract evidence (🟡; 🔴 if behavior is broken):** For changed state, callbacks, async behavior, composition, keyboard or focus behavior, use the [component-contract skill](../component-contract/SKILL.md). Check that a nearby story or test demonstrates the changed user-visible outcome; if coverage is missing, request it.
|
||||
- `COMP-02` **Public impact (🟡; 🔴 if a consumer contract regresses):** For changed hooks, contexts, CSS or tokens, trace affected exported components and inspect a representative consumer-visible contract. Do not stop at the implementation diff; request a public-boundary example or test when impact is not demonstrated.
|
||||
- `COMP-03` **Generated API metadata (🟡; 🔴 if published API is wrong):** For public prop or JSDoc changes, check the relevant generated props metadata as well as exports and tests; request the documented metadata update or correction when stale.
|
||||
|
||||
### Release (🟡, 🔴 if breaking)
|
||||
|
||||
- User-facing change to a publishable package has a `.changeset/*.md` with the right bump and a `<Component>: <consumer-facing text>` summary. Website, playroom and tooling changes get none.
|
||||
@@ -115,13 +92,6 @@ Skip anything ESLint, Stylelint, Biome, Prettier or TypeScript already catches.
|
||||
|
||||
What the change does, overall verdict and key risks (1–3 sentences).
|
||||
|
||||
### Evidence and replay
|
||||
|
||||
- **Inspected:** relevant files, stories and behavior states reviewed.
|
||||
- **Checks run:** exact command and result, or `Not run` with reason.
|
||||
- **Not inspected:** relevant surfaces or behavior not verified.
|
||||
- **Reviewer replay:** starting story/URL, action and expected result, or `Not needed`. Label unexecuted steps as unverified.
|
||||
|
||||
### Findings
|
||||
|
||||
| File | Line | Priority | Issue |
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
---
|
||||
name: component-contract
|
||||
description: "Define and verify component behavior, state ownership, accessibility and compatibility before changing a public component contract. Use when adding or changing controlled state, callbacks, async transitions, composition, keyboard or focus behavior, semantics, or consumer-facing props."
|
||||
---
|
||||
|
||||
# Component contract
|
||||
|
||||
Use this workflow for the behavior affected by a component change. Do not turn every dimension below into a new requirement when the change does not touch it.
|
||||
|
||||
## Establish the existing contract
|
||||
|
||||
Read the component folder, including its exports, metadata, stories, tests and relevant CSS. Trace related hooks, contexts and callers when the behavior crosses component boundaries. Preserve established behavior unless the requested change intentionally changes it.
|
||||
|
||||
For the affected behavior, record a compact contract:
|
||||
|
||||
| Area | Define when relevant |
|
||||
| ------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| State ownership | Controlled and uncontrolled inputs, defaults, source of truth, and what callbacks signal. A controlled component must reflect the owner's prop; do not assume a requested update was accepted. |
|
||||
| Async transitions | Relevant pending, success, failure, cancellation or stale-result behavior, and what the user sees. |
|
||||
| Composition | Which component or context owns registration, event handling and cleanup; how nesting or portals affect the behavior. |
|
||||
| Accessibility | Native semantics, accessible name/content responsibility, roles, states and relationships. Identify what the library guarantees and what the consumer must provide. |
|
||||
| Keyboard and focus | Supported keys, focus entry/movement/return, and whether each outcome is synchronous or eventual. Apply pattern-specific behavior only where that pattern is used. |
|
||||
| Compatibility | Public API and React 17–19 / SSR constraints affected by the change. |
|
||||
|
||||
Keep the contract proportional to the change. Use existing documentation, JSDoc, stories and tests to record decisions; do not create a new metadata format or a separate document for every component.
|
||||
|
||||
## Verify the contract
|
||||
|
||||
- Identify the closest existing story or test that demonstrates each changed behavior. Prefer observable public-component outcomes over assertions on implementation details.
|
||||
- Check that the starting state and intermediate outcome distinguish the intended behavior from a no-op.
|
||||
- Cover meaningful accepted and rejected controlled updates, async outcomes, or nested interactions when they are part of the change.
|
||||
- Verify accessible names and relationships against the actual rendered elements when those semantics change. Do not infer accessibility from attribute presence alone.
|
||||
- Separate automated interaction evidence from visual inspection or manual assistive-technology checks; report what was not verified.
|
||||
- Use `aksel-local-validation` to select commands. This skill defines behavior, not a separate test or validation process.
|
||||
|
||||
Do not copy another library's state semantics, API, React hooks or architectural layers as Aksel's contract. Follow Aksel's existing component and compatibility instructions.
|
||||
@@ -54,7 +54,6 @@ Hard rules (enforced by `tooling/eslint` rules `aksel-local/*`):
|
||||
## 3. Validate
|
||||
|
||||
```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
|
||||
|
||||
@@ -17,7 +17,7 @@ permissions:
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
name: Lint (Oxlint, ESLint, Stylelint etc.)
|
||||
name: Lint (ESLint, 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
-1
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"recommendations": [
|
||||
"oxc.oxc-vscode",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"stylelint.vscode-stylelint",
|
||||
"biomejs.biome",
|
||||
|
||||
@@ -1,23 +1,5 @@
|
||||
# @navikt/aksel-icons
|
||||
|
||||
## 8.22.1
|
||||
|
||||
## 8.22.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Icons: New icons `Gift`, `ArrowsPointingIn` and `ArrowsPointingOut`. ([#5484](https://github.com/navikt/aksel/pull/5484))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- ESM: Mark ESM output as side-effect free, so bundlers like webpack, Turbopack and esbuild can tree-shake unused components and icons. ([#5472](https://github.com/navikt/aksel/pull/5472))
|
||||
|
||||
## 8.21.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- Icons: Add 46 icons for text editing, tables, keyboard input, and filtering. ([#5458](https://github.com/navikt/aksel/pull/5458))
|
||||
|
||||
## 8.20.1
|
||||
|
||||
## 8.20.0
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
|
||||
## Foreslå nye ikoner
|
||||
|
||||
Om du ikke finner ikonet du leter etter kan du [lage et issue på GitHub](https://github.com/navikt/aksel/issues/new?template=new-icon.yaml).
|
||||
Om du ikke finner ikonet du leter etter kan du [lage et issue på GitHub](https://github.com/navikt/aksel/issues/new?labels=nytt+✨%2Cikoner+🖼%2Cforespørsel+🥰&template=new-icon.yaml&title=%5BNytt+ikon%5D%3A+).
|
||||
|
||||
## Oppdatere ikon
|
||||
|
||||
Hvis du tenker et ikon fortjener en oppdatering eller er utdatert kan [du foreslå det her](https://github.com/navikt/aksel/issues/new?template=update-icon.yml).
|
||||
Hvis du tenker et ikon fortjener en oppdatering eller er utdatert kan [du foreslå det her](https://github.com/navikt/aksel/issues/new?labels=forespørsel+🥰&template=update-icon.yml&title=%5BInnspill+til+ikon%5D%3A+).
|
||||
|
||||
## Design av nye ikoner
|
||||
|
||||
|
||||
@@ -59,37 +59,6 @@ declare module "*.svg" {
|
||||
}
|
||||
```
|
||||
|
||||
### CSS custom properties
|
||||
|
||||
All icons are available as CSS custom properties behind `/css`, one file per icon. Each file declares a `--aksel-icon-<kebab-case-name>` variable on `:root`, containing the icon as an SVG data URI.
|
||||
|
||||
```css
|
||||
@import "@navikt/aksel-icons/css/MagnifyingGlass.css";
|
||||
|
||||
.search-button::before {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
background-color: currentColor;
|
||||
mask: var(--aksel-icon-magnifying-glass) center / contain no-repeat;
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
.search-button::before {
|
||||
background-color: CanvasText;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
To get every icon, import `@navikt/aksel-icons/css/index.css`. This file contains all ~1000 icons (≈700 kB, ≈135 kB gzipped), so prefer importing only the icons you use.
|
||||
|
||||
Import icon files once in a global stylesheet (e.g. your root layout or `globals.css`), not in CSS modules or per-component stylesheets. The variables are global, and the SVG is inlined in each file, so importing the same icon in several places duplicates it in every CSS chunk.
|
||||
|
||||
- Icons added with CSS are decorative only. Give the parent element an accessible name, e.g. with visible text or `aria-label`.
|
||||
- In forced colors mode (e.g. Windows High Contrast), `background-color` is forced to the page background, which hides the icon. Use `background-color: CanvasText` inside `@media (forced-colors: active)` as shown above, so the icon follows the user's text color.
|
||||
- If you use a Content Security Policy, `img-src` must allow `data:`.
|
||||
|
||||
## [Learn more](https://aksel.nav.no/ikoner)
|
||||
|
||||
## License
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
import fastglob from "fast-glob";
|
||||
import path from "node:path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import {
|
||||
optimizeSvg,
|
||||
svgToDataUri,
|
||||
toCssVariableName,
|
||||
// @ts-expect-error -- css-icons is JavaScript without a declaration file.
|
||||
} from "../config/css-icons";
|
||||
|
||||
describe("toCssVariableName", () => {
|
||||
test.each([
|
||||
["MagnifyingGlass", "--aksel-icon-magnifying-glass"],
|
||||
["Buildings2Fill", "--aksel-icon-buildings-2-fill"],
|
||||
["ClipboardXMark", "--aksel-icon-clipboard-x-mark"],
|
||||
["XMark", "--aksel-icon-x-mark"],
|
||||
])("%s -> %s", (iconName, expected) => {
|
||||
expect(toCssVariableName(iconName)).toBe(expected);
|
||||
});
|
||||
|
||||
test("is unique for every icon", () => {
|
||||
const names = fastglob
|
||||
.sync("*.svg", { cwd: path.resolve(__dirname, "../icons") })
|
||||
.map((file) => toCssVariableName(path.basename(file, ".svg")));
|
||||
|
||||
expect(new Set(names).size).toBe(names.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe("svgToDataUri", () => {
|
||||
const svg = `<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.5 3.25 4.75 10.5Z" fill="#202733"/>
|
||||
</svg>`;
|
||||
|
||||
test("keeps viewBox, uses currentColor and escapes unsafe characters", () => {
|
||||
const uri = svgToDataUri(optimizeSvg(svg));
|
||||
const data = uri.slice('url("'.length, -'")'.length);
|
||||
|
||||
expect(uri.startsWith('url("data:image/svg+xml,')).toBe(true);
|
||||
expect(data).toContain("viewBox='0 0 24 24'");
|
||||
expect(data).toContain("fill='currentColor'");
|
||||
expect(data).not.toMatch(/["#<>]|width=|height=/);
|
||||
});
|
||||
});
|
||||
@@ -1,95 +0,0 @@
|
||||
const fastglob = require("fast-glob");
|
||||
const kebabCase = require("lodash/kebabCase");
|
||||
const path = require("node:path");
|
||||
const {
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
} = require("node:fs");
|
||||
const { optimize } = require("svgo");
|
||||
|
||||
/**
|
||||
* @param {string} iconName e.g. "MagnifyingGlass"
|
||||
* @returns {string} e.g. "--aksel-icon-magnifying-glass"
|
||||
*/
|
||||
function toCssVariableName(iconName) {
|
||||
return `--aksel-icon-${kebabCase(iconName)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} svgString
|
||||
* @returns {string} Minified SVG with currentColor fill and preserved viewBox
|
||||
*/
|
||||
function optimizeSvg(svgString) {
|
||||
return optimize(svgString.replaceAll("#202733", "currentColor"), {
|
||||
multipass: true,
|
||||
plugins: [
|
||||
{
|
||||
name: "preset-default",
|
||||
params: { overrides: { removeViewBox: false } },
|
||||
},
|
||||
{ name: "removeAttrs", params: { attrs: "svg:(width|height)" } },
|
||||
],
|
||||
}).data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Encodes SVG for use in a CSS `url()`. Keeps readable characters unescaped for smaller output.
|
||||
* @param {string} svgString
|
||||
* @returns {string}
|
||||
*/
|
||||
function svgToDataUri(svgString) {
|
||||
const encoded = svgString
|
||||
.replaceAll('"', "'")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim()
|
||||
.replace(/[^\x20-\x7e]|[%#<>{}"\\]/g, encodeURIComponent);
|
||||
|
||||
return `url("data:image/svg+xml,${encoded}")`;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string[]} declarations
|
||||
* @returns {string}
|
||||
*/
|
||||
function createCssFile(declarations) {
|
||||
return `:root {\n${declarations.map((d) => ` ${d}`).join("\n")}\n}\n`;
|
||||
}
|
||||
|
||||
function main() {
|
||||
const basePath = path.resolve(__dirname, "../icons");
|
||||
const cssFolder = path.resolve(__dirname, "../dist/css");
|
||||
|
||||
const svgList = fastglob.sync("*.svg", { cwd: basePath }).sort();
|
||||
|
||||
if (existsSync(cssFolder)) {
|
||||
rmSync(cssFolder, { recursive: true, force: true });
|
||||
}
|
||||
mkdirSync(cssFolder, { recursive: true });
|
||||
|
||||
const declarations = [];
|
||||
|
||||
for (const svg of svgList) {
|
||||
const iconName = path.basename(svg, ".svg");
|
||||
const svgString = readFileSync(path.join(basePath, svg)).toString();
|
||||
const declaration = `${toCssVariableName(iconName)}: ${svgToDataUri(optimizeSvg(svgString))};`;
|
||||
|
||||
declarations.push(declaration);
|
||||
writeFileSync(
|
||||
path.join(cssFolder, `${iconName}.css`),
|
||||
createCssFile([declaration]),
|
||||
);
|
||||
}
|
||||
|
||||
writeFileSync(path.join(cssFolder, "index.css"), createCssFile(declarations));
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createCssFile,
|
||||
main,
|
||||
optimizeSvg,
|
||||
svgToDataUri,
|
||||
toCssVariableName,
|
||||
};
|
||||
@@ -2,12 +2,6 @@ import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import React from "react";
|
||||
import * as AkselIcons from "@navikt/aksel-icons";
|
||||
import type { StarFillIcon } from "@navikt/aksel-icons";
|
||||
import "@navikt/aksel-icons/css/Airplane.css";
|
||||
import "@navikt/aksel-icons/css/Buildings2Fill.css";
|
||||
import "@navikt/aksel-icons/css/MagnifyingGlass.css";
|
||||
import "@navikt/aksel-icons/css/StarFill.css";
|
||||
import "@navikt/aksel-icons/css/XMarkOctagonFill.css";
|
||||
import StarIcon from "@navikt/aksel-icons/svg/Star.svg?no-inline";
|
||||
|
||||
const meta: Meta<typeof StarFillIcon> = {
|
||||
title: "aksel-icons/Icons",
|
||||
@@ -31,76 +25,3 @@ export const IconsInverted: Story = {
|
||||
render: Icons.render,
|
||||
globals: { theme: "dark" },
|
||||
};
|
||||
|
||||
const cssIconVariables = [
|
||||
"--aksel-icon-airplane",
|
||||
"--aksel-icon-buildings-2-fill",
|
||||
"--aksel-icon-magnifying-glass",
|
||||
"--aksel-icon-star-fill",
|
||||
"--aksel-icon-x-mark-octagon-fill",
|
||||
];
|
||||
|
||||
export const CssIcons: Story = {
|
||||
render: () => (
|
||||
<div
|
||||
style={{ display: "flex", gap: "1rem", color: "var(--ax-text-accent)" }}
|
||||
>
|
||||
<style>
|
||||
{`.css-icon {
|
||||
display: inline-block;
|
||||
width: 3rem;
|
||||
height: 3rem;
|
||||
background-color: currentColor;
|
||||
print-color-adjust: exact;
|
||||
}
|
||||
@media (forced-colors: active) {
|
||||
.css-icon { background-color: CanvasText; }
|
||||
}`}
|
||||
</style>
|
||||
{cssIconVariables.map((variable) => (
|
||||
<span
|
||||
key={variable}
|
||||
title={variable}
|
||||
className="css-icon"
|
||||
style={{ mask: `var(${variable}) center / contain no-repeat` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const CssIconsInverted: Story = {
|
||||
render: CssIcons.render,
|
||||
globals: { theme: "dark" },
|
||||
};
|
||||
|
||||
export const InlineSvgIcon: Story = {
|
||||
render: () => (
|
||||
<div style={{ color: "var(--ax-text-accent)" }}>
|
||||
<svg aria-hidden width="3rem" height="3rem">
|
||||
<use
|
||||
href={`${StarIcon}`}
|
||||
width="100%"
|
||||
height="100%"
|
||||
color="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
|
||||
export const InlineSvgIconInverted: Story = {
|
||||
render: () => (
|
||||
<div style={{ color: "var(--ax-text-accent)" }}>
|
||||
<svg aria-hidden width="3rem" height="3rem">
|
||||
<use
|
||||
href={`${StarIcon}`}
|
||||
width="100%"
|
||||
height="100%"
|
||||
color="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
</div>
|
||||
),
|
||||
globals: { theme: "dark" },
|
||||
};
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0001 13.2501C10.4142 13.2501 10.7501 13.5859 10.7501 14.0001V18.0001C10.75 18.4142 10.4142 18.75 10.0001 18.7501C9.58586 18.7501 9.25009 18.4142 9.25006 18.0001V15.8106L5.03033 20.0303C4.73746 20.3232 4.26268 20.3232 3.96978 20.0303C3.67689 19.7374 3.67689 19.2627 3.96978 18.9698L8.18951 14.7501H6.00006C5.58586 14.7501 5.25009 14.4142 5.25006 14.0001C5.25006 13.5858 5.58584 13.2501 6.00006 13.2501H10.0001ZM18.0001 13.2501C18.4142 13.2501 18.7501 13.5859 18.7501 14.0001C18.75 14.4142 18.4142 14.75 18.0001 14.7501H15.8106L20.0303 18.9698C20.3232 19.2627 20.3232 19.7375 20.0303 20.0303C19.7375 20.3232 19.2627 20.3232 18.9698 20.0303L14.7501 15.8106V18.0001C14.75 18.4142 14.4142 18.75 14.0001 18.7501C13.5859 18.7501 13.2501 18.4142 13.2501 18.0001V14.0001C13.2501 13.8011 13.3291 13.6104 13.4698 13.4698C13.6104 13.3291 13.8011 13.2501 14.0001 13.2501H18.0001ZM3.96978 3.96978C4.26268 3.67689 4.73744 3.67689 5.03033 3.96978L9.25006 8.18951V6.00005C9.25006 5.58584 9.58584 5.25005 10.0001 5.25005C10.4142 5.25008 10.7501 5.58586 10.7501 6.00005V10.0001C10.75 10.4142 10.4142 10.75 10.0001 10.7501H6.00006C5.58586 10.7501 5.25009 10.4142 5.25006 10.0001C5.25006 9.58584 5.58584 9.25005 6.00006 9.25005H8.18951L3.96978 5.03033C3.67689 4.73743 3.67689 4.26267 3.96978 3.96978ZM18.9698 3.96978C19.2627 3.67689 19.7374 3.67689 20.0303 3.96978C20.3232 4.26268 20.3232 4.73745 20.0303 5.03033L15.8106 9.25005H18.0001C18.4142 9.25008 18.7501 9.58586 18.7501 10.0001C18.75 10.4142 18.4142 10.75 18.0001 10.7501H14.0001C13.5859 10.7501 13.2501 10.4142 13.2501 10.0001V6.00005C13.2501 5.58584 13.5858 5.25005 14.0001 5.25005C14.4142 5.25008 14.7501 5.58586 14.7501 6.00005V8.18951L18.9698 3.96978Z" fill="#202733"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,12 +0,0 @@
|
||||
name: ArrowsPointingIn
|
||||
category: Arrows
|
||||
sub_category: Arrow
|
||||
keywords:
|
||||
- arrows
|
||||
- piler
|
||||
- fullscreen
|
||||
- fullskjerm
|
||||
- lukk
|
||||
- close
|
||||
variant: Stroke
|
||||
created_at: 31.10.2026
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M8.96973 13.9697C9.26262 13.6768 9.73738 13.6768 10.0303 13.9697C10.3232 14.2626 10.3232 14.7374 10.0303 15.0303L5.81055 19.25H8C8.41421 19.25 8.75 19.5858 8.75 20C8.75 20.4142 8.41421 20.75 8 20.75H4C3.80109 20.75 3.61038 20.6709 3.46973 20.5303C3.32907 20.3896 3.25 20.1989 3.25 20V16C3.25 15.5858 3.58579 15.25 4 15.25C4.41421 15.25 4.75 15.5858 4.75 16V18.1895L8.96973 13.9697ZM13.9697 13.9697C14.2626 13.6768 14.7374 13.6768 15.0303 13.9697L19.25 18.1895V16C19.25 15.5858 19.5858 15.25 20 15.25C20.4142 15.25 20.75 15.5858 20.75 16V20C20.75 20.1989 20.6709 20.3896 20.5303 20.5303C20.3896 20.6709 20.1989 20.75 20 20.75H16C15.5858 20.75 15.25 20.4142 15.25 20C15.25 19.5858 15.5858 19.25 16 19.25H18.1895L13.9697 15.0303C13.6768 14.7374 13.6768 14.2626 13.9697 13.9697ZM8 3.25C8.41421 3.25 8.75 3.58579 8.75 4C8.75 4.41421 8.41421 4.75 8 4.75H5.81055L10.0303 8.96973C10.3232 9.26262 10.3232 9.73738 10.0303 10.0303C9.73738 10.3232 9.26262 10.3232 8.96973 10.0303L4.75 5.81055V8C4.75 8.41421 4.41421 8.75 4 8.75C3.58579 8.75 3.25 8.41421 3.25 8V4C3.25 3.58579 3.58579 3.25 4 3.25H8ZM20 3.25C20.4142 3.25 20.75 3.58579 20.75 4V8C20.75 8.41421 20.4142 8.75 20 8.75C19.5858 8.75 19.25 8.41421 19.25 8V5.81055L15.0303 10.0303C14.7374 10.3232 14.2626 10.3232 13.9697 10.0303C13.6768 9.73738 13.6768 9.26262 13.9697 8.96973L18.1895 4.75H16C15.5858 4.75 15.25 4.41421 15.25 4C15.25 3.58579 15.5858 3.25 16 3.25H20Z" fill="#202733"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB |
@@ -1,12 +0,0 @@
|
||||
name: ArrowsPointingOut
|
||||
category: Arrows
|
||||
sub_category: Arrow
|
||||
keywords:
|
||||
- arrows
|
||||
- piler
|
||||
- fullscreen
|
||||
- fullskjerm
|
||||
- open
|
||||
- åpne
|
||||
variant: Stroke
|
||||
created_at: 31.10.2026
|
||||
@@ -6,7 +6,5 @@ keywords:
|
||||
- søkeresultater
|
||||
- tabell
|
||||
- trakt
|
||||
- fjern
|
||||
- slett
|
||||
variant: Stroke
|
||||
created_at: 26.10.2026
|
||||
@@ -6,7 +6,5 @@ keywords:
|
||||
- søkeresultater
|
||||
- tabell
|
||||
- trakt
|
||||
- fjern
|
||||
- slett
|
||||
variant: Fill
|
||||
created_at: 26.10.2026
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14 3.25C15.5188 3.25 16.75 4.48122 16.75 6C16.75 6.45058 16.6394 6.87468 16.4473 7.25H19C19.9665 7.25 20.75 8.0335 20.75 9V11C20.75 11.6977 20.3407 12.298 19.75 12.5791V19C19.75 19.9665 18.9665 20.75 18 20.75H6C5.0335 20.75 4.25 19.9665 4.25 19V12.5791C3.65928 12.298 3.25 11.6977 3.25 11V9C3.25 8.0335 4.0335 7.25 5 7.25H7.55273C7.36065 6.87468 7.25 6.45058 7.25 6C7.25 4.48122 8.48122 3.25 10 3.25C10.789 3.25 11.4985 3.58399 12 4.11621C12.5015 3.58399 13.211 3.25 14 3.25ZM5.75 19C5.75 19.1381 5.86193 19.25 6 19.25H11.25V12.75H5.75V19ZM12.75 19.25H18C18.1381 19.25 18.25 19.1381 18.25 19V12.75H12.75V19.25ZM5 8.75C4.86193 8.75 4.75 8.86193 4.75 9V11C4.75 11.1381 4.86193 11.25 5 11.25H11.25V8.75H5ZM12.75 11.25H19C19.1381 11.25 19.25 11.1381 19.25 11V9C19.25 8.86193 19.1381 8.75 19 8.75H12.75V11.25ZM10 4.75C9.30964 4.75 8.75 5.30964 8.75 6C8.75 6.69036 9.30964 7.25 10 7.25H11.25V6C11.25 5.30964 10.6904 4.75 10 4.75ZM14 4.75C13.3096 4.75 12.75 5.30964 12.75 6V7.25H14C14.6904 7.25 15.25 6.69036 15.25 6C15.25 5.30964 14.6904 4.75 14 4.75Z" fill="#202733"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,8 +0,0 @@
|
||||
name: Gift
|
||||
category: Home
|
||||
sub_category: Toys
|
||||
keywords:
|
||||
- gave
|
||||
- presang
|
||||
variant: Stroke
|
||||
created_at: 31.10.2026
|
||||
@@ -1,3 +0,0 @@
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M4.56055 13.2197C4.70421 13.2397 4.85088 13.25 5 13.25H11C11.1381 13.25 11.25 13.3619 11.25 13.5V20.5C11.25 20.6381 11.1381 20.75 11 20.75H6C5.0335 20.75 4.25 19.9665 4.25 19V13.4775C4.25 13.3162 4.40077 13.1975 4.56055 13.2197ZM19.4395 13.2197C19.5992 13.1975 19.75 13.3162 19.75 13.4775V19C19.75 19.9665 18.9665 20.75 18 20.75H13C12.8619 20.75 12.75 20.6381 12.75 20.5V13.5C12.75 13.3619 12.8619 13.25 13 13.25H19C19.1491 13.25 19.2958 13.2397 19.4395 13.2197ZM14 3.25C15.5188 3.25 16.75 4.48122 16.75 6C16.75 6.45058 16.6394 6.87468 16.4473 7.25H19C19.9665 7.25 20.75 8.0335 20.75 9V10C20.75 10.9665 19.9665 11.75 19 11.75H13C12.8619 11.75 12.75 11.6381 12.75 11.5V7.25H14C14.6904 7.25 15.25 6.69036 15.25 6C15.25 5.30964 14.6904 4.75 14 4.75C13.3096 4.75 12.75 5.30964 12.75 6V7.24707H11.25V6C11.25 5.30964 10.6904 4.75 10 4.75C9.30964 4.75 8.75 5.30964 8.75 6C8.75 6.69036 9.30964 7.25 10 7.25H11.25V11.5C11.25 11.6381 11.1381 11.75 11 11.75H5C4.0335 11.75 3.25 10.9665 3.25 10V9C3.25 8.0335 4.0335 7.25 5 7.25H7.55273C7.36065 6.87468 7.25 6.45058 7.25 6C7.25 4.48122 8.48122 3.25 10 3.25C10.789 3.25 11.4985 3.58399 12 4.11621C12.5015 3.58399 13.211 3.25 14 3.25Z" fill="#202733"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.3 KiB |
@@ -1,8 +0,0 @@
|
||||
name: GiftFill
|
||||
category: Home
|
||||
sub_category: Toys
|
||||
keywords:
|
||||
- gave
|
||||
- presang
|
||||
variant: Fill
|
||||
created_at: 31.10.2026
|
||||
@@ -2,6 +2,6 @@ name: Keyboard
|
||||
category: Interface
|
||||
sub_category: Devices
|
||||
keywords:
|
||||
- Tastatur
|
||||
- Keyboard
|
||||
variant: Stroke
|
||||
created_at: 26.10.2026
|
||||
@@ -2,6 +2,6 @@ name: KeyboardFill
|
||||
category: Interface
|
||||
sub_category: Devices
|
||||
keywords:
|
||||
- Tastatur
|
||||
- KeyboardFill
|
||||
variant: Fill
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableCellsMerge
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- celle
|
||||
- slå sammen
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableCellsMerge
|
||||
variant: Stroke
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableCellsMergeFill
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- celle
|
||||
- slå sammen
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableCellsMergeFill
|
||||
variant: Fill
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableCellsSplit
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- celle
|
||||
- splitt
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableCellsSplit
|
||||
variant: Stroke
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableCellsSplitFill
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- celle
|
||||
- splitt
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableCellsSplitFill
|
||||
variant: Fill
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableCross
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- tabell
|
||||
- datagrid
|
||||
- fjern
|
||||
- slett
|
||||
- TableCross
|
||||
variant: Stroke
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableCrossFill
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- tabell
|
||||
- datagrid
|
||||
- fjern
|
||||
- slett
|
||||
- TableCrossFill
|
||||
variant: Fill
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableDeleteColumn
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- slett kolonne
|
||||
- tabell
|
||||
- datagrid
|
||||
- fjern
|
||||
- TableDeleteColumn
|
||||
variant: Stroke
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableDeleteColumnFill
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- slett kolonne
|
||||
- tabell
|
||||
- datagrid
|
||||
- fjern
|
||||
- TableDeleteColumnFill
|
||||
variant: Fill
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableDeleteRow
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- slett rad
|
||||
- tabell
|
||||
- datagrid
|
||||
- fjern
|
||||
- TableDeleteRow
|
||||
variant: Stroke
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableDeleteRowFill
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- slett rad
|
||||
- tabell
|
||||
- datagrid
|
||||
- fjern
|
||||
- TableDeleteRowFill
|
||||
variant: Fill
|
||||
created_at: 26.10.2026
|
||||
@@ -2,7 +2,6 @@ name: TableGrid
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableGrid
|
||||
variant: Stroke
|
||||
created_at: 26.10.2026
|
||||
@@ -2,7 +2,6 @@ name: TableGridFill
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableGridFill
|
||||
variant: Fill
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableInsertColumnLeft
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- ny kolonne
|
||||
- venstre
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableInsertColumnLeft
|
||||
variant: Stroke
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableInsertColumnLeftFill
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- ny kolonne
|
||||
- venstre
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableInsertColumnLeftFill
|
||||
variant: Fill
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableInsertColumnRight
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- ny kolonne
|
||||
- høyre
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableInsertColumnRight
|
||||
variant: Stroke
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableInsertColumnRightFill
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- ny kolonne
|
||||
- høyre
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableInsertColumnRightFill
|
||||
variant: Fill
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableInsertRowAbove
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- ny rad
|
||||
- over
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableInsertRowAbove
|
||||
variant: Stroke
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableInsertRowAboveFill
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- ny rad
|
||||
- over
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableInsertRowAboveFill
|
||||
variant: Fill
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableInsertRowBelow
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- ny rad
|
||||
- under
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableInsertRowBelow
|
||||
variant: Stroke
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TableInsertRowBelowFill
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- ny rad
|
||||
- under
|
||||
- tabell
|
||||
- datagrid
|
||||
- TableInsertRowBelowFill
|
||||
variant: Fill
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TablePlus
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- tabell
|
||||
- legg til
|
||||
- ny
|
||||
- datagrid
|
||||
- TablePlus
|
||||
variant: Stroke
|
||||
created_at: 26.10.2026
|
||||
@@ -2,9 +2,6 @@ name: TablePlusFill
|
||||
category: Interface
|
||||
sub_category: Table
|
||||
keywords:
|
||||
- tabell
|
||||
- legg til
|
||||
- ny
|
||||
- datagrid
|
||||
- TablePlusFill
|
||||
variant: Fill
|
||||
created_at: 26.10.2026
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-icons",
|
||||
"version": "8.22.1",
|
||||
"version": "8.20.1",
|
||||
"author": "Aksel | Nav",
|
||||
"homepage": "https://aksel.nav.no/ikoner",
|
||||
"repository": {
|
||||
@@ -23,9 +23,7 @@
|
||||
"access": "public",
|
||||
"provenance": true
|
||||
},
|
||||
"sideEffects": [
|
||||
"*.css"
|
||||
],
|
||||
"sideEffects": false,
|
||||
"license": "MIT",
|
||||
"main": "./dist/react/cjs/index.js",
|
||||
"module": "./dist/react/esm/index.js",
|
||||
@@ -46,7 +44,6 @@
|
||||
"default": "./dist/metadata.js"
|
||||
},
|
||||
"./svg/*.svg": "./dist/svg/*.svg",
|
||||
"./css/*.css": "./dist/css/*.css",
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"typesVersions": {
|
||||
@@ -65,11 +62,10 @@
|
||||
"create-icons": "svgr --no-prettier --silent --index-template config/index-template.js --out-dir src icons && yarn prettify-icons",
|
||||
"prettify-icons": "yarn prettier ./src --write --ignore-path nonexisting-path --log-level silent --no-config",
|
||||
"parse-svg": "node -e 'require(\"./config/parse-svg.js\").main()'",
|
||||
"css-icons": "node -e 'require(\"./config/css-icons.js\").main()'",
|
||||
"copy": "copyfiles util/* src",
|
||||
"update-metadata": "node config/metadata.js",
|
||||
"write-packagejson": "echo '{\"type\": \"module\", \"sideEffects\": false}' > dist/react/esm/package.json",
|
||||
"build": "yarn copy && yarn create-icons && concurrently \"tsc\" \"tsc -p tsconfig.esm.json && tsc-alias -p tsconfig.esm.json && yarn write-packagejson\" && node config/cleanTypes.js && yarn update-metadata && yarn parse-svg && yarn css-icons",
|
||||
"write-packagejson": "echo '{\"type\": \"module\"}' > dist/react/esm/package.json",
|
||||
"build": "yarn copy && yarn create-icons && concurrently \"tsc\" \"tsc -p tsconfig.esm.json && tsc-alias -p tsconfig.esm.json && yarn write-packagejson\" && node config/cleanTypes.js && yarn update-metadata && yarn parse-svg",
|
||||
"fetch-new:icons": "tsx --env-file=.env config/figma/index.ts",
|
||||
"zip:icons": "node config/zip.js",
|
||||
"test": "vitest run",
|
||||
@@ -91,11 +87,10 @@
|
||||
"prettier": "3.9.6",
|
||||
"react": "19.3.1",
|
||||
"rehype-parse": "9.0.1",
|
||||
"svgo": "3.3.5",
|
||||
"tsc-alias": "1.9.7",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "6.0.3",
|
||||
"unified": "11.0.5",
|
||||
"vitest": "5.0.5"
|
||||
"vitest": "4.1.11"
|
||||
}
|
||||
}
|
||||
File renamed without changes.
@@ -1,11 +1,5 @@
|
||||
# @navikt/aksel-stylelint
|
||||
|
||||
## 8.22.1
|
||||
|
||||
## 8.22.0
|
||||
|
||||
## 8.21.0
|
||||
|
||||
## 8.20.1
|
||||
|
||||
## 8.20.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-stylelint",
|
||||
"version": "8.22.1",
|
||||
"version": "8.20.1",
|
||||
"type": "module",
|
||||
"engines": {
|
||||
"node": ">=18.12.0"
|
||||
@@ -43,13 +43,13 @@
|
||||
"dev": "yarn watch:lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-css": "^8.22.1",
|
||||
"@navikt/ds-tokens": "^8.22.1",
|
||||
"@navikt/ds-css": "^8.20.1",
|
||||
"@navikt/ds-tokens": "^8.20.1",
|
||||
"concurrently": "10.0.6",
|
||||
"postcss-selector-parser": "^7.1.7",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"stylelint": "^17.15.0",
|
||||
"stylelint-test-rule-node": "^1.3.0",
|
||||
"stylelint-test-rule-node": "^1.2.0",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "6.0.3"
|
||||
}
|
||||
|
||||
@@ -1,11 +1,5 @@
|
||||
# @navikt/aksel
|
||||
|
||||
## 8.22.1
|
||||
|
||||
## 8.22.0
|
||||
|
||||
## 8.21.0
|
||||
|
||||
## 8.20.1
|
||||
|
||||
## 8.20.0
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel",
|
||||
"version": "8.22.1",
|
||||
"version": "8.20.1",
|
||||
"description": "Aksel command line interface. Codemods and other utilities for Aksel users.",
|
||||
"author": "Aksel | Nav designsystem team",
|
||||
"license": "MIT",
|
||||
@@ -58,7 +58,7 @@
|
||||
"@types/lodash": "4.17.25",
|
||||
"rimraf": "6.1.3",
|
||||
"typescript": "6.0.3",
|
||||
"vitest": "5.0.5"
|
||||
"vitest": "4.1.11"
|
||||
},
|
||||
"sideEffects": false,
|
||||
"engines": {
|
||||
|
||||
File renamed without changes.
@@ -1,23 +1,5 @@
|
||||
# @navikt/ds-css
|
||||
|
||||
## 8.22.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- DataGrid: Column dividers in the table header are now hidden when column dividers are disabled, and only shown on hover, focus or while resizing. ([#5505](https://github.com/navikt/aksel/pull/5505))
|
||||
|
||||
## 8.22.0
|
||||
|
||||
## 8.21.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- DataGrid: Add top and bottom content areas outside the table's scrollable viewport. ([#5442](https://github.com/navikt/aksel/pull/5442))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- DataTable: Constrain details panel width to the visible table area. ([#5433](https://github.com/navikt/aksel/pull/5433))
|
||||
|
||||
## 8.20.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "8.22.1",
|
||||
"version": "8.20.1",
|
||||
"description": "CSS for Nav Designsystem",
|
||||
"author": "Aksel | Nav designsystem team",
|
||||
"keywords": [
|
||||
@@ -32,7 +32,7 @@
|
||||
"css:get-version": "node config/get-version.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^8.22.1",
|
||||
"@navikt/ds-tokens": "^8.20.1",
|
||||
"browserslist": "^4.28.7",
|
||||
"esbuild": "^0.28.2",
|
||||
"fast-glob": "3.3.3",
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
overflow: hidden;
|
||||
min-height: 0;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.aksel-data-table__frame {
|
||||
@@ -208,20 +209,11 @@
|
||||
padding: 0;
|
||||
font-size: var(--__axc-data-table-text-size);
|
||||
|
||||
.aksel-data-table__thead &,
|
||||
.aksel-data-table[data-column-dividers="true"] & {
|
||||
box-shadow: inset 1px 1px 0 0 var(--ax-border-neutral-subtle);
|
||||
}
|
||||
|
||||
.aksel-data-table[data-column-dividers="false"] & {
|
||||
transition: box-shadow 0.1s ease-in-out;
|
||||
}
|
||||
|
||||
.aksel-data-table[data-column-dividers="false"]
|
||||
.aksel-data-table__thead:where(:hover, :has(:focus-visible, .aksel-data-table__th-resize-handle:active))
|
||||
& {
|
||||
box-shadow: inset 1px 1px 0 0 var(--ax-border-neutral-subtle);
|
||||
}
|
||||
|
||||
&:not(.aksel-data-table__column-header, [data-sticky]) {
|
||||
overflow: hidden; /* Needed for cases where left margin is added to cell-content (nested rows) */
|
||||
}
|
||||
@@ -763,6 +755,95 @@ html[data-dragging-cursor="true"] *::after {
|
||||
|
||||
/* ------------------------ DataGrid.Preferences End ------------------------ */
|
||||
|
||||
/* ------------------------ DataGrid.Pagination Start ----------------------- */
|
||||
.aksel-data-grid__pagination {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ax-space-8);
|
||||
|
||||
/* Equal side columns keep page size centered when summary width changes. */
|
||||
&[data-layout="preset"] {
|
||||
@media (min-width: 768px) {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr);
|
||||
|
||||
& > :last-child {
|
||||
justify-self: end;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aksel-data-grid__pagination-page-size {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ax-space-8);
|
||||
|
||||
.aksel-data-grid__pagination[data-layout="preset"] & {
|
||||
display: none;
|
||||
|
||||
@media (min-width: 768px) {
|
||||
display: flex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aksel-data-grid__pagination-navigation {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ax-space-4);
|
||||
}
|
||||
|
||||
.aksel-data-grid__pagnation-navigation-button {
|
||||
/* TODO: Avoids focus loss now, but is unconventional so consider revisiting this approach */
|
||||
&[aria-disabled="true"] {
|
||||
cursor: default;
|
||||
opacity: var(--ax-opacity-disabled);
|
||||
}
|
||||
|
||||
@media (forced-colors: active) {
|
||||
/* TODO: Avoids focus loss now, but is unconventional so consider revisiting this approach */
|
||||
&[aria-disabled="true"] {
|
||||
color: GrayText;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.aksel-data-grid__pagination-summary {
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.aksel-data-grid__pagination-navigation-page {
|
||||
display: inline-grid;
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
|
||||
&::before {
|
||||
content: attr(data-max-label);
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
&::before,
|
||||
& > span {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
}
|
||||
|
||||
.aksel-data-grid__pagination-navigation-page {
|
||||
text-align: center;
|
||||
display: none;
|
||||
|
||||
@media (min-width: 480px) {
|
||||
display: inline-grid;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------------------------- DataGrid.Pagination End ------------------------ */
|
||||
|
||||
.aksel-data-table__attachment {
|
||||
padding: var(--ax-space-12) var(--ax-space-16);
|
||||
border: 1px solid var(--ax-border-neutral-subtle);
|
||||
|
||||
@@ -142,7 +142,7 @@
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
|
||||
.aksel-combobox2:not(:has(.aksel-combobox2__modal)) & {
|
||||
.aksel-combobox2:has(:not(.aksel-combobox2__modal)) & {
|
||||
outline: 1px solid var(--ax-border-neutral-subtleA);
|
||||
outline-offset: -1px;
|
||||
border-radius: var(--ax-radius-8);
|
||||
@@ -158,21 +158,26 @@
|
||||
margin: -1px;
|
||||
}
|
||||
|
||||
.aksel-combobox2__filter .aksel-search__wrapper:has(.aksel-search__input:focus-visible) {
|
||||
outline: none;
|
||||
position: relative;
|
||||
.aksel-combobox2__filter .aksel-search__wrapper {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
border: 2px solid var(--ax-border-focus);
|
||||
}
|
||||
&:has(.aksel-search__input:focus-visible) {
|
||||
outline: none;
|
||||
position: relative;
|
||||
|
||||
.aksel-combobox2:not(:has(.aksel-combobox2__modal)) &::after {
|
||||
border-top-left-radius: var(--ax-radius-8);
|
||||
border-top-right-radius: var(--ax-radius-8);
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
pointer-events: none;
|
||||
border: 2px solid var(--ax-border-focus);
|
||||
}
|
||||
|
||||
.aksel-combobox2:has(:not(.aksel-combobox2__modal)) &::after {
|
||||
border-top-left-radius: var(--ax-radius-8);
|
||||
border-top-right-radius: var(--ax-radius-8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,7 +189,7 @@
|
||||
border-color: var(--ax-border-neutral-subtleA);
|
||||
}
|
||||
|
||||
.aksel-combobox2__popup[data-size="medium"] & {
|
||||
.aksel-combobox2[data-size="medium"] & {
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
@@ -216,6 +221,15 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/************************************************
|
||||
* Group *
|
||||
************************************************/
|
||||
|
||||
.aksel-combobox2__group-label {
|
||||
padding: var(--ax-space-8) var(--ax-space-8);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
/************************************************
|
||||
* Option *
|
||||
************************************************/
|
||||
@@ -239,7 +253,7 @@
|
||||
margin-top: var(--ax-space-2);
|
||||
float: left;
|
||||
|
||||
.aksel-combobox2__popup[data-size="small"] & {
|
||||
.aksel-combobox2[data-size="small"] & {
|
||||
--__axc-combobox2-option-checkmark-size: 1rem;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,51 +1,5 @@
|
||||
# @navikt/ds-react
|
||||
|
||||
## 8.22.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@8.22.1
|
||||
- @navikt/aksel-icons@8.22.1
|
||||
|
||||
## 8.22.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Combobox: The list is no longer focusable ([#5490](https://github.com/navikt/aksel/pull/5490))
|
||||
|
||||
- Popover: Closed popovers no longer calculate their position on mount. This makes pages with many Dropdowns or HelpTexts render faster. ([#5474](https://github.com/navikt/aksel/pull/5474))
|
||||
|
||||
- ESM: Mark ESM output as side-effect free, so bundlers like webpack, Turbopack and esbuild can tree-shake unused components and icons. ([#5472](https://github.com/navikt/aksel/pull/5472))
|
||||
|
||||
- MonthPicker: Fixed issue where navigating to a year without any enabled months could lead to re-render loop in React ([#5503](https://github.com/navikt/aksel/pull/5503))
|
||||
|
||||
- MonthPicker: Arrow-key navigation no longer freezes the page when no later or earlier month is enabled. Keyboard search now respects `fromDate`/`toDate` with both caption types and looks at most 100 years ahead or back. ([#5477](https://github.com/navikt/aksel/pull/5477))
|
||||
|
||||
- Date utilities: Import date-fns functions and locales from their own paths, so unbundled Node imports (SSR, Vitest/Jest) no longer load every date-fns locale. ([#5476](https://github.com/navikt/aksel/pull/5476))
|
||||
|
||||
- DataGrid: Fixed text selection inside details panels when keyboard navigation is enabled. ([`0bdad46`](https://github.com/navikt/aksel/commit/0bdad462664e04abd89c4121de515f6ae6d9aad1))
|
||||
|
||||
- Updated dependencies [[`16da3ce`](https://github.com/navikt/aksel/commit/16da3ce770041acc41589fcdd087717fab027126), [`b69c5fc`](https://github.com/navikt/aksel/commit/b69c5fca305f28ad99b48dd74e4f736fe4257d48)]:
|
||||
- @navikt/aksel-icons@8.22.0
|
||||
- @navikt/ds-tokens@8.22.0
|
||||
|
||||
## 8.21.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- DataGrid: Add top and bottom content areas outside the table's scrollable viewport. ([#5442](https://github.com/navikt/aksel/pull/5442))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- DatePicker: Preserve keyboard focus when controlled month changes replace the focused day. ([#5408](https://github.com/navikt/aksel/pull/5408))
|
||||
|
||||
- Dialog, Lookup: Avoid setting aria-hidden on elements with role=alert/status when open ([#5466](https://github.com/navikt/aksel/pull/5466))
|
||||
|
||||
- Updated dependencies [[`f87f195`](https://github.com/navikt/aksel/commit/f87f1951fef77430c433d3e36892f18fc1387eb3)]:
|
||||
- @navikt/aksel-icons@8.21.0
|
||||
- @navikt/ds-tokens@8.21.0
|
||||
|
||||
## 8.20.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "8.22.1",
|
||||
"version": "8.20.1",
|
||||
"description": "React components from the Norwegian Labour and Welfare Administration.",
|
||||
"author": "Aksel, a team part of the Norwegian Labour and Welfare Administration.",
|
||||
"license": "MIT",
|
||||
@@ -775,21 +775,21 @@
|
||||
"docgen:meta": "yarn metadata:generate-names && tsx ./scripts/extract-metadata.ts",
|
||||
"metadata:generate-names": "tsx ./scripts/extract-component-names.ts",
|
||||
"i18n-jsdoc": "tsx ./scripts/addJsdocToLocales.ts",
|
||||
"write-packagejson": "echo '{\"type\": \"module\", \"sideEffects\": false}' > esm/package.json",
|
||||
"write-packagejson": "echo '{\"type\": \"module\"}' > esm/package.json",
|
||||
"clean": "rimraf cjs esm",
|
||||
"build": "yarn i18n-jsdoc && yarn metadata:generate-names && concurrently \"tsc -p tsconfig.build.json --module commonjs --outDir cjs --tsBuildInfoFile cjs/.tsbuildinfo\" \"tsc -p tsconfig.build.json --module esnext --outDir esm --tsBuildInfoFile esm/.tsbuildinfo && tsc-alias -p tsconfig.build.json --dir esm -f && yarn write-packagejson\" && yarn i18n-jsdoc --cleanup && yarn copy-types",
|
||||
"watch": "tsc --watch -p tsconfig.build.json --module esnext --outDir esm --tsBuildInfoFile esm/.tsbuildinfo",
|
||||
"test": "yarn metadata:generate-names && concurrently \"cross-env TZ=UTC vitest run -c tests/vitest.config.mts\" \"yarn test-sb-build\"",
|
||||
"test:bench": "cross-env TZ=UTC vitest bench -c tests/vitest.config.mts",
|
||||
"test": "yarn metadata:generate-names && concurrently \"cross-env TZ=UTC vitest run -c tests/vitest.config.ts\" \"yarn test-sb-build\"",
|
||||
"test:bench": "cross-env TZ=UTC vitest bench -c tests/vitest.config.ts",
|
||||
"test-sb-build": "tsc -p tsconfig.json",
|
||||
"test:watch": "TZ=UTC vitest watch -c tests/vitest.config.mts",
|
||||
"test:watch": "TZ=UTC vitest watch -c tests/vitest.config.ts",
|
||||
"copy-types": "copyfiles -f ./src/types/theme.d.ts ./esm/types"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "0.27.20",
|
||||
"@floating-ui/react-dom": "^2.1.9",
|
||||
"@navikt/aksel-icons": "^8.22.1",
|
||||
"@navikt/ds-tokens": "^8.22.1",
|
||||
"@navikt/aksel-icons": "^8.20.1",
|
||||
"@navikt/ds-tokens": "^8.20.1",
|
||||
"date-fns": "^4.0.0",
|
||||
"react-day-picker": "10.0.2"
|
||||
},
|
||||
@@ -806,7 +806,7 @@
|
||||
"copyfiles": "^2.4.1",
|
||||
"cross-env": "^10.1.0",
|
||||
"jscodeshift": "17.4.0",
|
||||
"jsdom": "30.1.3",
|
||||
"jsdom": "29.1.1",
|
||||
"react": "19.3.1",
|
||||
"react-dom": "19.3.1",
|
||||
"rimraf": "6.1.3",
|
||||
@@ -814,7 +814,7 @@
|
||||
"tsc-alias": "1.9.7",
|
||||
"tsx": "^4.23.15",
|
||||
"typescript": "6.0.3",
|
||||
"vitest": "5.0.5"
|
||||
"vitest": "4.1.11"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "^17.0.30 || ^18 || ^19",
|
||||
|
||||
@@ -7,6 +7,10 @@ const metadata: ComponentMetadata = {
|
||||
DataGrid,
|
||||
"DataGrid.Table": DataGridTable,
|
||||
"DataGrid.Preferences": DataGridPreferences,
|
||||
"DataGrid.Pagination": DataGrid.Pagination,
|
||||
"DataGrid.PaginationSummary": DataGrid.PaginationSummary,
|
||||
"DataGrid.PaginationPageSize": DataGrid.PaginationPageSize,
|
||||
"DataGrid.PaginationNavigation": DataGrid.PaginationNavigation,
|
||||
},
|
||||
keywords: ["data grid", "datagrid", "table", "tabell", "grid"],
|
||||
related: ["Table"],
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import React, { useState } from "react";
|
||||
import { DataGrid } from "../root/DataGridRoot";
|
||||
|
||||
const meta: Meta<typeof DataGrid> = {
|
||||
title: "ds-react/DataGrid/Pagination",
|
||||
component: DataGrid,
|
||||
parameters: {
|
||||
layout: "padded",
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof DataGrid>;
|
||||
|
||||
type Row = { id: number; name: string; role: string };
|
||||
|
||||
const ROWS: Row[] = Array.from({ length: 137 }, (_, i) => ({
|
||||
id: i + 1,
|
||||
name: `Person ${i + 1}`,
|
||||
role: ["Utvikler", "Designer", "Leder"][i % 3],
|
||||
}));
|
||||
|
||||
const columns: DataGrid.Columns<Row> = [
|
||||
{ id: "id", header: "Id", bodyCell: (row) => row.id, align: "right" },
|
||||
{ id: "name", header: "Navn", bodyCell: (row) => row.name },
|
||||
{ id: "role", header: "Rolle", bodyCell: (row) => row.role },
|
||||
];
|
||||
|
||||
const sortableColumns: DataGrid.Columns<Row> = columns.map((column) => ({
|
||||
...column,
|
||||
isSortable: column.id === "id",
|
||||
}));
|
||||
|
||||
function PagedGrid({ paginationAbove = false }: { paginationAbove?: boolean }) {
|
||||
return (
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={ROWS}
|
||||
getRowId={(row) => String(row.id)}
|
||||
pagination={{ defaultPageSize: 10 }}
|
||||
>
|
||||
<DataGrid.Table
|
||||
topContent={paginationAbove ? <DataGrid.Pagination /> : undefined}
|
||||
bottomContent={<DataGrid.Pagination />}
|
||||
/>
|
||||
</DataGrid>
|
||||
);
|
||||
}
|
||||
|
||||
export const Default: Story = {
|
||||
render: () => <PagedGrid />,
|
||||
};
|
||||
|
||||
export const ClientSideLoading: Story = {
|
||||
render: () => {
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={() => setIsLoading((value) => !value)}>
|
||||
{isLoading ? "Complete load" : "Reload data"}
|
||||
</button>
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={isLoading ? [] : ROWS}
|
||||
getRowId={(row) => String(row.id)}
|
||||
isLoading={isLoading}
|
||||
|
||||
pagination={{ defaultPage: 2 }}
|
||||
>
|
||||
<DataGrid.Table
|
||||
loadingContent={{ variant: "skeleton", rows: 10 }}
|
||||
bottomContent={<DataGrid.Pagination />}
|
||||
/>
|
||||
</DataGrid>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const ServerSide: Story = {
|
||||
render: () => {
|
||||
const [page, setPage] = useState(0);
|
||||
const [pageSize, setPageSize] = useState(10);
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={ROWS.slice(page * pageSize, (page + 1) * pageSize)}
|
||||
getRowId={(row) => String(row.id)}
|
||||
pagination={{
|
||||
mode: "server",
|
||||
page,
|
||||
onPageChange: setPage,
|
||||
pageSize,
|
||||
onPageSizeChange: setPageSize,
|
||||
totalRows: ROWS.length,
|
||||
}}
|
||||
>
|
||||
<DataGrid.Table bottomContent={<DataGrid.Pagination />} />
|
||||
</DataGrid>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const ResetPageOnSort: Story = {
|
||||
render: () => {
|
||||
const [sort, setSort] = useState<DataGrid.Table.SortEntry[]>([]);
|
||||
const direction = sort[0]?.direction;
|
||||
const sortedRows = [...ROWS].sort((a, b) =>
|
||||
direction === "desc" ? b.id - a.id : a.id - b.id,
|
||||
);
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
columns={sortableColumns}
|
||||
data={sortedRows}
|
||||
getRowId={(row) => String(row.id)}
|
||||
pagination={{ defaultPageSize: 10 }}
|
||||
>
|
||||
<DataGrid.Table
|
||||
sorting={{
|
||||
sortOrder: sort,
|
||||
onSortOrderChange: setSort,
|
||||
allowMultiSort: false,
|
||||
}}
|
||||
bottomContent={<DataGrid.Pagination />}
|
||||
/>
|
||||
</DataGrid>
|
||||
);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,314 @@
|
||||
import type { Meta, StoryObj } from "@storybook/react-vite";
|
||||
import React, { useState } from "react";
|
||||
import { expect, userEvent, waitFor, within } from "storybook/test";
|
||||
import type { SortEntry } from "../../data/table/root/DataGridTable.types";
|
||||
import { DataGrid } from "../root/DataGridRoot";
|
||||
import { useDataGridRootPaginationContext } from "../root/DataGridRoot.context";
|
||||
|
||||
const meta: Meta<typeof DataGrid> = {
|
||||
title: "ds-react/DataGrid/Pagination/tests",
|
||||
component: DataGrid,
|
||||
parameters: {
|
||||
layout: "padded",
|
||||
chromatic: { disable: true },
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
type Story = StoryObj<typeof DataGrid>;
|
||||
type Row = { id: number; name: string };
|
||||
|
||||
const getRowId = (row: Row) => String(row.id);
|
||||
|
||||
const rows: Row[] = Array.from({ length: 5 }, (_, index) => ({
|
||||
id: index + 1,
|
||||
name: `Person ${index + 1}`,
|
||||
}));
|
||||
|
||||
const columns: DataGrid.Columns<Row> = [
|
||||
{ id: "name", header: "Name", bodyCell: (row) => row.name },
|
||||
];
|
||||
|
||||
const sortableColumns: DataGrid.Columns<Row> = [
|
||||
{ id: "name", header: "Name", bodyCell: (row) => row.name, isSortable: true },
|
||||
];
|
||||
|
||||
function LoadingClientGrid({ controlled = false }: { controlled?: boolean }) {
|
||||
const [data, setData] = useState<Row[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [page, setPage] = useState(2);
|
||||
const [correctionCount, setCorrectionCount] = useState(0);
|
||||
|
||||
return (
|
||||
<>
|
||||
<output aria-label="Page corrections">{correctionCount}</output>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setData(rows);
|
||||
setIsLoading(false);
|
||||
}}
|
||||
>
|
||||
Complete load
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setData([]);
|
||||
setIsLoading(true);
|
||||
}}
|
||||
>
|
||||
Reload
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
setData(rows.slice(0, 3));
|
||||
setIsLoading(false);
|
||||
}}
|
||||
>
|
||||
Complete smaller load
|
||||
</button>
|
||||
<DataGrid
|
||||
columns={columns}
|
||||
data={data}
|
||||
getRowId={getRowId}
|
||||
isLoading={isLoading}
|
||||
pagination={{
|
||||
page: controlled ? page : undefined,
|
||||
defaultPage: 2,
|
||||
defaultPageSize: 2,
|
||||
pageSizeOptions: [2],
|
||||
onPageChange: (nextPage) => {
|
||||
setPage(nextPage);
|
||||
setCorrectionCount((count) => count + 1);
|
||||
},
|
||||
}}
|
||||
>
|
||||
<DataGrid.Table bottomContent={<DataGrid.Pagination />} />
|
||||
</DataGrid>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const testLoadingPreservesPage: Story["play"] = async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const corrections = canvas.getByRole("status", {
|
||||
name: "Page corrections",
|
||||
});
|
||||
const completeLoad = canvas.getByRole("button", { name: "Complete load" });
|
||||
const reload = canvas.getByRole("button", { name: "Reload" });
|
||||
|
||||
expect(corrections).toHaveTextContent("0");
|
||||
await userEvent.click(completeLoad);
|
||||
expect(canvas.getByText("Side 3 av 3")).toBeInTheDocument();
|
||||
expect(canvas.getByRole("cell", { name: "Person 5" })).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(reload);
|
||||
expect(corrections).toHaveTextContent("0");
|
||||
await userEvent.click(completeLoad);
|
||||
expect(canvas.getByText("Side 3 av 3")).toBeInTheDocument();
|
||||
expect(corrections).toHaveTextContent("0");
|
||||
|
||||
await userEvent.click(reload);
|
||||
await userEvent.click(
|
||||
canvas.getByRole("button", { name: "Complete smaller load" }),
|
||||
);
|
||||
expect(await canvas.findByText("Side 2 av 2")).toBeInTheDocument();
|
||||
expect(canvas.getByRole("cell", { name: "Person 3" })).toBeInTheDocument();
|
||||
expect(corrections).toHaveTextContent("1");
|
||||
};
|
||||
|
||||
export const LoadingPreservesUncontrolledPage: Story = {
|
||||
render: () => <LoadingClientGrid />,
|
||||
play: testLoadingPreservesPage,
|
||||
};
|
||||
|
||||
export const LoadingPreservesControlledPage: Story = {
|
||||
render: () => <LoadingClientGrid controlled />,
|
||||
play: testLoadingPreservesPage,
|
||||
};
|
||||
|
||||
const manyRows: Row[] = Array.from({ length: 60 }, (_, index) => ({
|
||||
id: index + 1,
|
||||
name: `Person ${String(index + 1).padStart(2, "0")}`,
|
||||
}));
|
||||
|
||||
function ResetControls({
|
||||
onSortProgrammatically,
|
||||
}: {
|
||||
onSortProgrammatically: () => void;
|
||||
}) {
|
||||
const { page, pageSize, setPage, setPageSize } =
|
||||
useDataGridRootPaginationContext();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<p>{`Page ${page + 1} · ${pageSize} rows`}</p>
|
||||
<button type="button" onClick={() => setPage(page + 1)}>
|
||||
Next page
|
||||
</button>
|
||||
<button type="button" onClick={() => setPageSize(10)}>
|
||||
Ten rows
|
||||
</button>
|
||||
<button type="button" onClick={onSortProgrammatically}>
|
||||
Sort programmatically
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function ResetGrid({ acceptSort }: { acceptSort: boolean }) {
|
||||
const [sortOrder, setSortOrder] = useState<SortEntry[]>([]);
|
||||
|
||||
return (
|
||||
<div style={{ display: "flex", height: 360 }}>
|
||||
<DataGrid
|
||||
columns={sortableColumns}
|
||||
data={manyRows}
|
||||
getRowId={(row) => String(row.id)}
|
||||
pagination={{ defaultPageSize: 20, pageSizeOptions: [10, 20] }}
|
||||
>
|
||||
<ResetControls
|
||||
onSortProgrammatically={() =>
|
||||
setSortOrder([{ columnId: "name", direction: "desc" }])
|
||||
}
|
||||
/>
|
||||
<DataGrid.Table
|
||||
aria-label="Personer"
|
||||
sorting={{
|
||||
sortOrder,
|
||||
onSortOrderChange: (next) => {
|
||||
if (acceptSort) {
|
||||
setSortOrder(next);
|
||||
}
|
||||
},
|
||||
}}
|
||||
/>
|
||||
</DataGrid>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function getScrollWrapper(canvasElement: HTMLElement) {
|
||||
const wrapper = canvasElement.querySelector<HTMLElement>(
|
||||
".aksel-data-table__scroll-wrapper",
|
||||
);
|
||||
if (!wrapper) {
|
||||
throw new Error("Table scroll wrapper not found");
|
||||
}
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
function scrollDown(wrapper: HTMLElement) {
|
||||
wrapper.scrollTop = 150;
|
||||
expect(wrapper.scrollTop).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
export const ResetsPageAndTableScroll: Story = {
|
||||
render: () => <ResetGrid acceptSort />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const wrapper = getScrollWrapper(canvasElement);
|
||||
const view = canvasElement.ownerDocument.defaultView ?? window;
|
||||
const windowScrollY = view.scrollY;
|
||||
|
||||
scrollDown(wrapper);
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Next page" }));
|
||||
expect(canvas.getByText("Page 2 · 20 rows")).toBeInTheDocument();
|
||||
await waitFor(() => expect(wrapper.scrollTop).toBe(0));
|
||||
|
||||
scrollDown(wrapper);
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Name" }));
|
||||
expect(canvas.getByText("Page 1 · 20 rows")).toBeInTheDocument();
|
||||
expect(canvas.getByRole("columnheader", { name: "Name" })).toHaveAttribute(
|
||||
"aria-sort",
|
||||
"ascending",
|
||||
);
|
||||
await waitFor(() => expect(wrapper.scrollTop).toBe(0));
|
||||
|
||||
scrollDown(wrapper);
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Name" }));
|
||||
expect(canvas.getByText("Page 1 · 20 rows")).toBeInTheDocument();
|
||||
await waitFor(() => expect(wrapper.scrollTop).toBe(0));
|
||||
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Next page" }));
|
||||
scrollDown(wrapper);
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Ten rows" }));
|
||||
expect(canvas.getByText("Page 1 · 10 rows")).toBeInTheDocument();
|
||||
await waitFor(() => expect(wrapper.scrollTop).toBe(0));
|
||||
|
||||
expect(view.scrollY).toBe(windowScrollY);
|
||||
},
|
||||
};
|
||||
|
||||
export const RejectedSortDoesNotReset: Story = {
|
||||
render: () => <ResetGrid acceptSort={false} />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const wrapper = getScrollWrapper(canvasElement);
|
||||
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Next page" }));
|
||||
scrollDown(wrapper);
|
||||
const scrollTop = wrapper.scrollTop;
|
||||
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Name" }));
|
||||
expect(canvas.getByText("Page 2 · 20 rows")).toBeInTheDocument();
|
||||
expect(canvas.getByRole("columnheader", { name: "Name" })).toHaveAttribute(
|
||||
"aria-sort",
|
||||
"none",
|
||||
);
|
||||
expect(wrapper.scrollTop).toBe(scrollTop);
|
||||
|
||||
await userEvent.click(
|
||||
canvas.getByRole("button", { name: "Sort programmatically" }),
|
||||
);
|
||||
expect(canvas.getByText("Page 1 · 20 rows")).toBeInTheDocument();
|
||||
await waitFor(() => expect(wrapper.scrollTop).toBe(0));
|
||||
},
|
||||
};
|
||||
|
||||
function ServerResetGrid() {
|
||||
const [sortOrder, setSortOrder] = useState<SortEntry[]>([]);
|
||||
|
||||
return (
|
||||
<DataGrid
|
||||
columns={sortableColumns}
|
||||
data={manyRows.slice(0, 20)}
|
||||
getRowId={getRowId}
|
||||
pagination={{
|
||||
mode: "server",
|
||||
defaultPageSize: 20,
|
||||
pageSizeOptions: [10, 20],
|
||||
totalRows: manyRows.length,
|
||||
}}
|
||||
>
|
||||
<DataGrid.Table
|
||||
aria-label="Personer"
|
||||
sorting={{ sortOrder, onSortOrderChange: setSortOrder }}
|
||||
bottomContent={<DataGrid.Pagination />}
|
||||
/>
|
||||
</DataGrid>
|
||||
);
|
||||
}
|
||||
|
||||
export const ServerSortDoesNotResetPage: Story = {
|
||||
render: () => <ServerResetGrid />,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
||||
expect(canvas.getByText("Side 1 av 3")).toBeInTheDocument();
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Neste side" }));
|
||||
expect(canvas.getByText("Side 2 av 3")).toBeInTheDocument();
|
||||
expect(canvas.getByRole("cell", { name: "Person 01" })).toBeInTheDocument();
|
||||
expect(canvas.getByRole("cell", { name: "Person 20" })).toBeInTheDocument();
|
||||
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Name" }));
|
||||
expect(canvas.getByRole("columnheader", { name: "Name" })).toHaveAttribute(
|
||||
"aria-sort",
|
||||
"ascending",
|
||||
);
|
||||
expect(canvas.getByText("Side 2 av 3")).toBeInTheDocument();
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,54 @@
|
||||
import React from "react";
|
||||
import type { DataGridPaginationConfig } from "../root/DataGrid.types";
|
||||
import { DataGridRootPaginationContextProvider } from "../root/DataGridRoot.context";
|
||||
import { DataGridPaginationLiveRegion } from "./root/DataGridPaginationRoot";
|
||||
import { useDataGridPagination } from "./root/useDataGridPagination";
|
||||
|
||||
/**
|
||||
* Mounted only when `pagination` is set, so uncontrolled defaults apply
|
||||
* when pagination is enabled and no state or warnings exist while it is off.
|
||||
*/
|
||||
function DataGridPaginationInternal({
|
||||
pagination,
|
||||
rowCount,
|
||||
isLoading,
|
||||
children,
|
||||
}: {
|
||||
pagination: DataGridPaginationConfig;
|
||||
rowCount: number;
|
||||
isLoading: boolean;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const paginationState = useDataGridPagination({
|
||||
pagination,
|
||||
rows:
|
||||
pagination.mode === "server"
|
||||
? pagination.totalRows !== undefined
|
||||
? {
|
||||
mode: "server",
|
||||
pageRowCount: rowCount,
|
||||
totalRows: pagination.totalRows,
|
||||
}
|
||||
: {
|
||||
mode: "server",
|
||||
pageRowCount: rowCount,
|
||||
hasNextPage: pagination.hasNextPage,
|
||||
}
|
||||
: { mode: "client", rowCount },
|
||||
isLoading,
|
||||
});
|
||||
|
||||
return (
|
||||
<DataGridRootPaginationContextProvider
|
||||
{...paginationState}
|
||||
mode={pagination.mode ?? "client"}
|
||||
>
|
||||
{children}
|
||||
{pagination.announcePageChanges !== false && (
|
||||
<DataGridPaginationLiveRegion />
|
||||
)}
|
||||
</DataGridRootPaginationContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export { DataGridPaginationInternal };
|
||||
@@ -0,0 +1,221 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { getPaginationRange } from "./pagination-range";
|
||||
|
||||
describe("getPaginationRange", () => {
|
||||
test("calculates the range and page count for a known total", () => {
|
||||
expect(
|
||||
getPaginationRange({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
mode: "server",
|
||||
pageRowCount: 10,
|
||||
totalRows: 25,
|
||||
}),
|
||||
).toEqual({
|
||||
page: 1,
|
||||
pageCount: 3,
|
||||
from: 11,
|
||||
to: 20,
|
||||
startIndex: 10,
|
||||
endIndex: 20,
|
||||
canPrevious: true,
|
||||
canNext: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("uses hasNextPage when the total is unknown", () => {
|
||||
expect(
|
||||
getPaginationRange({
|
||||
page: 2,
|
||||
pageSize: 10,
|
||||
mode: "server",
|
||||
pageRowCount: 4,
|
||||
hasNextPage: false,
|
||||
}),
|
||||
).toEqual({
|
||||
page: 2,
|
||||
pageCount: undefined,
|
||||
from: 21,
|
||||
to: 24,
|
||||
startIndex: 20,
|
||||
endIndex: 24,
|
||||
canPrevious: true,
|
||||
canNext: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("returns one page and an empty range for zero known rows", () => {
|
||||
expect(
|
||||
getPaginationRange({
|
||||
page: 0,
|
||||
pageSize: 10,
|
||||
mode: "server",
|
||||
pageRowCount: 0,
|
||||
totalRows: 0,
|
||||
}),
|
||||
).toEqual({
|
||||
page: 0,
|
||||
pageCount: 1,
|
||||
from: 0,
|
||||
to: 0,
|
||||
startIndex: 0,
|
||||
endIndex: 0,
|
||||
canPrevious: false,
|
||||
canNext: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("returns an empty range for an unknown-total page without data", () => {
|
||||
expect(
|
||||
getPaginationRange({
|
||||
page: 3,
|
||||
pageSize: 10,
|
||||
mode: "server",
|
||||
pageRowCount: 0,
|
||||
hasNextPage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
page: 3,
|
||||
pageCount: undefined,
|
||||
from: 0,
|
||||
to: 0,
|
||||
startIndex: 30,
|
||||
endIndex: 30,
|
||||
canPrevious: true,
|
||||
canNext: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("clamps negative unknown-total pages to zero", () => {
|
||||
expect(
|
||||
getPaginationRange({
|
||||
page: -1,
|
||||
pageSize: 10,
|
||||
mode: "server",
|
||||
pageRowCount: 3,
|
||||
hasNextPage: true,
|
||||
}),
|
||||
).toEqual({
|
||||
page: 0,
|
||||
pageCount: undefined,
|
||||
from: 1,
|
||||
to: 3,
|
||||
startIndex: 0,
|
||||
endIndex: 3,
|
||||
canPrevious: false,
|
||||
canNext: true,
|
||||
});
|
||||
});
|
||||
|
||||
test("limits the range on the last partial page", () => {
|
||||
expect(
|
||||
getPaginationRange({
|
||||
page: 2,
|
||||
pageSize: 10,
|
||||
mode: "server",
|
||||
pageRowCount: 5,
|
||||
totalRows: 25,
|
||||
}),
|
||||
).toEqual({
|
||||
page: 2,
|
||||
pageCount: 3,
|
||||
from: 21,
|
||||
to: 25,
|
||||
startIndex: 20,
|
||||
endIndex: 25,
|
||||
canPrevious: true,
|
||||
canNext: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("clamps an out-of-range page before calculating the range", () => {
|
||||
expect(
|
||||
getPaginationRange({
|
||||
page: 4,
|
||||
pageSize: 10,
|
||||
mode: "server",
|
||||
pageRowCount: 2,
|
||||
totalRows: 22,
|
||||
}),
|
||||
).toEqual({
|
||||
page: 2,
|
||||
pageCount: 3,
|
||||
from: 21,
|
||||
to: 22,
|
||||
startIndex: 20,
|
||||
endIndex: 22,
|
||||
canPrevious: true,
|
||||
canNext: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("ends the slice at the known total when the page reports extra rows", () => {
|
||||
expect(
|
||||
getPaginationRange({
|
||||
mode: "server",
|
||||
page: 2,
|
||||
pageSize: 10,
|
||||
pageRowCount: 10,
|
||||
totalRows: 25,
|
||||
}),
|
||||
).toMatchObject({ from: 21, to: 25, startIndex: 20, endIndex: 25 });
|
||||
});
|
||||
|
||||
describe("client mode", () => {
|
||||
test("derives the page slice from the total row count", () => {
|
||||
expect(
|
||||
getPaginationRange({
|
||||
mode: "client",
|
||||
page: 2,
|
||||
pageSize: 10,
|
||||
rowCount: 25,
|
||||
}),
|
||||
).toEqual({
|
||||
page: 2,
|
||||
pageCount: 3,
|
||||
from: 21,
|
||||
to: 25,
|
||||
startIndex: 20,
|
||||
endIndex: 25,
|
||||
canPrevious: true,
|
||||
canNext: false,
|
||||
});
|
||||
});
|
||||
|
||||
test("clamps the page when rows shrink", () => {
|
||||
expect(
|
||||
getPaginationRange({
|
||||
mode: "client",
|
||||
page: 4,
|
||||
pageSize: 10,
|
||||
rowCount: 12,
|
||||
}),
|
||||
).toMatchObject({
|
||||
page: 1,
|
||||
from: 11,
|
||||
to: 12,
|
||||
startIndex: 10,
|
||||
endIndex: 12,
|
||||
});
|
||||
});
|
||||
|
||||
test("returns an empty first page without rows", () => {
|
||||
expect(
|
||||
getPaginationRange({
|
||||
mode: "client",
|
||||
page: 3,
|
||||
pageSize: 10,
|
||||
rowCount: 0,
|
||||
}),
|
||||
).toMatchObject({
|
||||
page: 0,
|
||||
pageCount: 1,
|
||||
from: 0,
|
||||
to: 0,
|
||||
startIndex: 0,
|
||||
endIndex: 0,
|
||||
canNext: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
type PaginationRangeOptions = {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
} & (
|
||||
| {
|
||||
/** All top-level rows are available; the page is sliced from them. */
|
||||
mode: "client";
|
||||
rowCount: number;
|
||||
pageRowCount?: never;
|
||||
totalRows?: never;
|
||||
hasNextPage?: never;
|
||||
}
|
||||
| ({
|
||||
/** Only the current page is available. */
|
||||
mode: "server";
|
||||
pageRowCount: number;
|
||||
rowCount?: never;
|
||||
} & (
|
||||
| {
|
||||
totalRows: number;
|
||||
hasNextPage?: never;
|
||||
}
|
||||
| {
|
||||
hasNextPage: boolean;
|
||||
totalRows?: never;
|
||||
}
|
||||
))
|
||||
);
|
||||
|
||||
type PaginationRange = {
|
||||
page: number;
|
||||
pageCount: number | undefined;
|
||||
/** 1-based first visible row, or 0 when the page is empty. */
|
||||
from: number;
|
||||
/** 1-based last visible row, or 0 when the page is empty. */
|
||||
to: number;
|
||||
/** 0-based inclusive start index of the page among all top-level rows. */
|
||||
startIndex: number;
|
||||
/** 0-based exclusive end index of the page among all top-level rows. */
|
||||
endIndex: number;
|
||||
canPrevious: boolean;
|
||||
canNext: boolean;
|
||||
};
|
||||
|
||||
function getPaginationRange(options: PaginationRangeOptions): PaginationRange {
|
||||
const { page, pageSize } = options;
|
||||
|
||||
if (options.mode === "client") {
|
||||
const pageCount = Math.max(1, Math.ceil(options.rowCount / pageSize));
|
||||
const currentPage = Math.min(Math.max(page, 0), pageCount - 1);
|
||||
const startIndex = currentPage * pageSize;
|
||||
const visibleRows = Math.min(
|
||||
Math.max(options.rowCount - startIndex, 0),
|
||||
pageSize,
|
||||
);
|
||||
|
||||
return {
|
||||
page: currentPage,
|
||||
pageCount,
|
||||
from: visibleRows === 0 ? 0 : startIndex + 1,
|
||||
to: visibleRows === 0 ? 0 : startIndex + visibleRows,
|
||||
startIndex,
|
||||
endIndex: startIndex + visibleRows,
|
||||
canPrevious: currentPage > 0,
|
||||
canNext: currentPage < pageCount - 1,
|
||||
};
|
||||
}
|
||||
|
||||
const { totalRows, pageRowCount, hasNextPage } = options;
|
||||
const pageCount =
|
||||
totalRows === undefined
|
||||
? undefined
|
||||
: Math.max(1, Math.ceil(totalRows / pageSize));
|
||||
const currentPage =
|
||||
pageCount === undefined
|
||||
? Math.max(page, 0)
|
||||
: Math.min(Math.max(page, 0), pageCount - 1);
|
||||
const startIndex = currentPage * pageSize;
|
||||
const visibleRows =
|
||||
totalRows === undefined
|
||||
? pageRowCount
|
||||
: Math.min(pageRowCount, Math.max(totalRows - startIndex, 0));
|
||||
const from = visibleRows === 0 ? 0 : startIndex + 1;
|
||||
const to = visibleRows === 0 ? 0 : startIndex + visibleRows;
|
||||
|
||||
return {
|
||||
page: currentPage,
|
||||
pageCount,
|
||||
from,
|
||||
to,
|
||||
startIndex,
|
||||
endIndex: startIndex + visibleRows,
|
||||
canPrevious: currentPage > 0,
|
||||
canNext:
|
||||
pageCount === undefined
|
||||
? hasNextPage === true
|
||||
: currentPage < pageCount - 1,
|
||||
};
|
||||
}
|
||||
|
||||
export { getPaginationRange };
|
||||
export type { PaginationRange, PaginationRangeOptions };
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import {
|
||||
ChevronLeftFirstIcon,
|
||||
ChevronLeftIcon,
|
||||
ChevronRightIcon,
|
||||
ChevronRightLastIcon,
|
||||
} from "@navikt/aksel-icons";
|
||||
import { Button } from "../../../button";
|
||||
import { cl } from "../../../utils/helpers";
|
||||
import { useI18n } from "../../../utils/i18n/i18n.hooks";
|
||||
import { useDataGridRootPaginationContext } from "../../root/DataGridRoot.context";
|
||||
|
||||
interface DataGridPaginationNavigationProps extends React.HTMLAttributes<HTMLElement> {
|
||||
children?: never;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides first, previous, next, and last page controls.
|
||||
*
|
||||
* @see 🏷️ {@link DataGridPagin dationNavigationProps}
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <DataGrid columns={columns} data={rows} pagination={{}}>
|
||||
* <DataGrid.Pagination>
|
||||
* <DataGrid.PaginationNavigation />
|
||||
* </DataGrid.Pagination>
|
||||
* </DataGrid>
|
||||
* ```
|
||||
*/
|
||||
const DataGridPaginationNavigation = forwardRef<
|
||||
HTMLElement,
|
||||
DataGridPaginationNavigationProps
|
||||
>(({ className, "aria-label": ariaLabel, ...rest }, forwardedRef) => {
|
||||
const { page, pageCount, canPrevious, canNext, isLoading, setPage } =
|
||||
useDataGridRootPaginationContext(
|
||||
true,
|
||||
"[Aksel] DataGrid.PaginationNavigation must be used within <DataGrid pagination={...}>",
|
||||
);
|
||||
const translate = useI18n("DataGrid");
|
||||
|
||||
return (
|
||||
<nav
|
||||
{...rest}
|
||||
ref={forwardedRef}
|
||||
aria-label={ariaLabel ?? translate("Pagination.navLabel")}
|
||||
className={cl("aksel-data-grid__pagination-navigation", className)}
|
||||
>
|
||||
<Button
|
||||
type="button"
|
||||
className="aksel-data-grid__pagnation-navigation-button"
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
data-color="neutral"
|
||||
icon={<ChevronLeftFirstIcon aria-hidden />}
|
||||
aria-label={translate("Pagination.first")}
|
||||
aria-disabled={isLoading || !canPrevious}
|
||||
onClick={() => {
|
||||
if (!isLoading && canPrevious) {
|
||||
setPage(0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
type="button"
|
||||
className="aksel-data-grid__pagnation-navigation-button"
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
data-color="neutral"
|
||||
icon={<ChevronLeftIcon aria-hidden />}
|
||||
aria-label={translate("Pagination.previous")}
|
||||
aria-disabled={isLoading || !canPrevious}
|
||||
onClick={() => {
|
||||
if (!isLoading && canPrevious) {
|
||||
setPage(page - 1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span
|
||||
className="aksel-data-grid__pagination-navigation-page"
|
||||
data-max-label={
|
||||
pageCount === undefined
|
||||
? translate("Pagination.pageOfUnknownTotal", {
|
||||
page: "9".repeat(Math.max(3, String(page + 1).length)),
|
||||
})
|
||||
: translate("Pagination.pageOf", { page: pageCount, pageCount })
|
||||
}
|
||||
>
|
||||
<span>
|
||||
{pageCount === undefined
|
||||
? translate("Pagination.pageOfUnknownTotal", { page: page + 1 })
|
||||
: translate("Pagination.pageOf", { page: page + 1, pageCount })}
|
||||
</span>
|
||||
</span>
|
||||
<Button
|
||||
type="button"
|
||||
className="aksel-data-grid__pagnation-navigation-button"
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
data-color="neutral"
|
||||
icon={<ChevronRightIcon aria-hidden />}
|
||||
aria-label={translate("Pagination.next")}
|
||||
aria-disabled={isLoading || !canNext}
|
||||
onClick={() => {
|
||||
if (!isLoading && canNext) {
|
||||
setPage(page + 1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{pageCount !== undefined && (
|
||||
<Button
|
||||
type="button"
|
||||
className="aksel-data-grid__pagnation-navigation-button"
|
||||
size="small"
|
||||
variant="tertiary"
|
||||
data-color="neutral"
|
||||
icon={<ChevronRightLastIcon aria-hidden />}
|
||||
aria-label={translate("Pagination.last")}
|
||||
aria-disabled={isLoading || !canNext}
|
||||
onClick={() => {
|
||||
if (!isLoading && canNext) {
|
||||
setPage(pageCount - 1);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</nav>
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace DataGridPaginationNavigation {
|
||||
export type Props = DataGridPaginationNavigationProps;
|
||||
}
|
||||
|
||||
export { DataGridPaginationNavigation };
|
||||
export type { DataGridPaginationNavigationProps };
|
||||
@@ -0,0 +1,70 @@
|
||||
import React, { forwardRef, useMemo } from "react";
|
||||
import { Select, type SelectProps } from "../../../form/select";
|
||||
import { cl } from "../../../utils/helpers";
|
||||
import { useI18n } from "../../../utils/i18n/i18n.hooks";
|
||||
import { useDataGridRootPaginationContext } from "../../root/DataGridRoot.context";
|
||||
|
||||
type DataGridPaginationPageSizeProps = Omit<
|
||||
SelectProps,
|
||||
"children" | "label" | "value" | "onChange"
|
||||
>;
|
||||
|
||||
/**
|
||||
* Lets users choose how many rows to display per page.
|
||||
*
|
||||
* @see 🏷️ {@link DataGridPaginationPageSizeProps}
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <DataGrid columns={columns} data={rows} pagination={{}}>
|
||||
* <DataGrid.Pagination>
|
||||
* <DataGrid.PaginationPageSize />
|
||||
* </DataGrid.Pagination>
|
||||
* </DataGrid>
|
||||
* ```
|
||||
*/
|
||||
const DataGridPaginationPageSize = forwardRef<
|
||||
HTMLSelectElement,
|
||||
DataGridPaginationPageSizeProps
|
||||
>(({ className, style, ...rest }, forwardedRef) => {
|
||||
const { pageSize, pageSizeOptions, setPageSize } =
|
||||
useDataGridRootPaginationContext(
|
||||
true,
|
||||
"[Aksel] DataGrid.PaginationPageSize must be used within <DataGrid pagination={...}>",
|
||||
);
|
||||
const translate = useI18n("DataGrid");
|
||||
const options = useMemo(
|
||||
() =>
|
||||
pageSizeOptions.includes(pageSize)
|
||||
? pageSizeOptions
|
||||
: [...pageSizeOptions, pageSize],
|
||||
[pageSize, pageSizeOptions],
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
{...rest}
|
||||
ref={forwardedRef}
|
||||
className={cl("aksel-data-grid__pagination-page-size", className)}
|
||||
label={translate("Pagination.pageSize")}
|
||||
size="small"
|
||||
style={{ width: "auto", ...style }}
|
||||
value={pageSize}
|
||||
onChange={(event) => setPageSize(Number(event.currentTarget.value))}
|
||||
>
|
||||
{options.map((size) => (
|
||||
<option key={size} value={size}>
|
||||
{size}
|
||||
</option>
|
||||
))}
|
||||
</Select>
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace DataGridPaginationPageSize {
|
||||
export type Props = DataGridPaginationPageSizeProps;
|
||||
}
|
||||
|
||||
export { DataGridPaginationPageSize };
|
||||
export type { DataGridPaginationPageSizeProps };
|
||||
@@ -0,0 +1,27 @@
|
||||
type DataGridPaginationSharedOptions = {
|
||||
/** Controlled, zero-indexed page. */
|
||||
page?: number;
|
||||
/**
|
||||
* Initial zero-indexed page when `page` is uncontrolled.
|
||||
* @default 0
|
||||
*/
|
||||
defaultPage?: number;
|
||||
/** Called with the requested zero-indexed page. */
|
||||
onPageChange?: (page: number) => void;
|
||||
/** Controlled number of rows per page. */
|
||||
pageSize?: number;
|
||||
/**
|
||||
* Initial page size when `pageSize` is uncontrolled.
|
||||
* @default pageSizeOptions[0]
|
||||
*/
|
||||
defaultPageSize?: number;
|
||||
/** Called when the page size changes. */
|
||||
onPageSizeChange?: (pageSize: number) => void;
|
||||
/**
|
||||
* Page-size options.
|
||||
* @default [10, 25, 50, 100]
|
||||
*/
|
||||
pageSizeOptions?: number[];
|
||||
};
|
||||
|
||||
export type { DataGridPaginationSharedOptions };
|
||||
@@ -0,0 +1,100 @@
|
||||
import React, { forwardRef, useEffect, useRef, useState } from "react";
|
||||
import { cl } from "../../../utils/helpers";
|
||||
import { useI18n } from "../../../utils/i18n/i18n.hooks";
|
||||
import { useDataGridRootPaginationContext } from "../../root/DataGridRoot.context";
|
||||
import { DataGridPaginationNavigation } from "../navigation/DataGridPaginationNavigation";
|
||||
import { DataGridPaginationPageSize } from "../page-size/DataGridPaginationPageSize";
|
||||
import { DataGridPaginationSummary } from "../summary/DataGridPaginationSummary";
|
||||
|
||||
interface DataGridPaginationProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
* Custom layout built from the pagination parts.
|
||||
* Renders the preset layout when omitted.
|
||||
*/
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pagination bar for the parent DataGrid. Requires the `pagination` prop on `<DataGrid>`.
|
||||
*
|
||||
* @see 🏷️ {@link DataGridPaginationProps}
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <DataGrid columns={columns} data={rows} pagination={{ defaultPageSize: 10 }}>
|
||||
* <DataGrid.Table />
|
||||
* <DataGrid.Pagination />
|
||||
* </DataGrid>
|
||||
* ```
|
||||
*/
|
||||
const DataGridPagination = forwardRef<HTMLDivElement, DataGridPaginationProps>(
|
||||
({ children, className, ...rest }, forwardedRef) => {
|
||||
useDataGridRootPaginationContext();
|
||||
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
ref={forwardedRef}
|
||||
className={cl("aksel-data-grid__pagination", className)}
|
||||
data-layout={children ? "custom" : "preset"}
|
||||
>
|
||||
{children ?? (
|
||||
<>
|
||||
<DataGridPaginationSummary />
|
||||
<DataGridPaginationPageSize />
|
||||
<DataGridPaginationNavigation />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
function DataGridPaginationLiveRegion() {
|
||||
const { page, pageCount, pageSize, from, to, totalRows, isLoading } =
|
||||
useDataGridRootPaginationContext();
|
||||
const translate = useI18n("DataGrid");
|
||||
const lastAnnouncedPage = useRef({ page, pageSize });
|
||||
const [announcement, setAnnouncement] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
isLoading ||
|
||||
(lastAnnouncedPage.current.page === page &&
|
||||
lastAnnouncedPage.current.pageSize === pageSize)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const summary =
|
||||
to === 0
|
||||
? translate("Pagination.summaryEmpty")
|
||||
: totalRows === undefined
|
||||
? translate("Pagination.summaryUnknownTotal", { from, to })
|
||||
: translate("Pagination.summary", { from, to, totalRows });
|
||||
const pageLabel =
|
||||
pageCount === undefined
|
||||
? translate("Pagination.pageOfUnknownTotal", { page: page + 1 })
|
||||
: translate("Pagination.pageOf", { page: page + 1, pageCount });
|
||||
|
||||
lastAnnouncedPage.current = { page, pageSize };
|
||||
// Keep the completed message during loading and unrelated renders.
|
||||
setAnnouncement(
|
||||
`${summary}. ${pageLabel}. ${translate("Pagination.pageSize")}: ${pageSize}`,
|
||||
);
|
||||
}, [isLoading, page, pageCount, pageSize, from, to, totalRows, translate]);
|
||||
|
||||
return (
|
||||
<div aria-live="polite" aria-atomic="true" className="aksel-sr-only">
|
||||
{announcement}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace DataGridPagination {
|
||||
export type Props = DataGridPaginationProps;
|
||||
}
|
||||
|
||||
export { DataGridPagination, DataGridPaginationLiveRegion };
|
||||
export type { DataGridPaginationProps };
|
||||
@@ -0,0 +1,171 @@
|
||||
import { useEffect, useRef } from "react";
|
||||
import { consoleWarning } from "../../../utils/helpers/consoleWarning";
|
||||
import { useControllableState, useEventCallback } from "../../../utils/hooks";
|
||||
import { getPaginationRange } from "../helpers/pagination-range";
|
||||
import type { DataGridPaginationSharedOptions } from "./DataGridPagination.types";
|
||||
|
||||
const defaultPageSizeOptions = [10, 25, 50, 100];
|
||||
|
||||
type DataGridPaginationRows =
|
||||
| {
|
||||
/** Client mode: count of all top-level rows. DataGrid slices the page. */
|
||||
mode: "client";
|
||||
rowCount: number;
|
||||
totalRows?: never;
|
||||
hasNextPage?: never;
|
||||
}
|
||||
| ({
|
||||
/** Server mode: count of top-level rows on the consumer-sliced page. */
|
||||
mode: "server";
|
||||
pageRowCount: number;
|
||||
} & (
|
||||
| { totalRows: number; hasNextPage?: never }
|
||||
| { hasNextPage: boolean; totalRows?: never }
|
||||
));
|
||||
|
||||
function useDataGridPagination({
|
||||
pagination,
|
||||
rows,
|
||||
isLoading,
|
||||
}: {
|
||||
pagination: DataGridPaginationSharedOptions;
|
||||
rows: DataGridPaginationRows;
|
||||
isLoading: boolean;
|
||||
}) {
|
||||
const {
|
||||
page: controlledPage,
|
||||
defaultPage = 0,
|
||||
onPageChange,
|
||||
pageSize: controlledPageSize,
|
||||
defaultPageSize,
|
||||
onPageSizeChange,
|
||||
pageSizeOptions = defaultPageSizeOptions,
|
||||
} = pagination;
|
||||
|
||||
const [storedPage, setStoredPage] = useControllableState({
|
||||
value: controlledPage,
|
||||
defaultValue: defaultPage,
|
||||
onChange: onPageChange,
|
||||
});
|
||||
const [pageSize, setStoredPageSize] = useControllableState({
|
||||
value: controlledPageSize,
|
||||
defaultValue:
|
||||
defaultPageSize ?? pageSizeOptions[0] ?? defaultPageSizeOptions[0],
|
||||
onChange: onPageSizeChange,
|
||||
});
|
||||
|
||||
const totalRows =
|
||||
rows.mode === "client"
|
||||
? rows.rowCount
|
||||
: "totalRows" in rows
|
||||
? rows.totalRows
|
||||
: undefined;
|
||||
|
||||
const range = getPaginationRange({ page: storedPage, pageSize, ...rows });
|
||||
const previousSummaryRange = useRef({ from: range.from, to: range.to });
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading) {
|
||||
previousSummaryRange.current = { from: range.from, to: range.to };
|
||||
}
|
||||
}, [isLoading, range.from, range.to]);
|
||||
|
||||
const setPage = useEventCallback((page: number) => {
|
||||
const nextPage = getPaginationRange({ page, pageSize, ...rows }).page;
|
||||
if (nextPage !== storedPage) {
|
||||
setStoredPage(nextPage);
|
||||
}
|
||||
});
|
||||
|
||||
const setPageSize = useEventCallback((size: number) => {
|
||||
setStoredPageSize(size);
|
||||
setPage(0);
|
||||
});
|
||||
|
||||
const lastCorrection = useRef<{
|
||||
storedPage: number;
|
||||
page: number;
|
||||
} | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (storedPage === range.page) {
|
||||
lastCorrection.current = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Request each correction once, also when effects re-run (Strict Mode).
|
||||
if (
|
||||
lastCorrection.current?.storedPage === storedPage &&
|
||||
lastCorrection.current.page === range.page
|
||||
) {
|
||||
return;
|
||||
}
|
||||
lastCorrection.current = { storedPage, page: range.page };
|
||||
|
||||
if (controlledPage !== undefined) {
|
||||
consoleWarning(
|
||||
`DataGrid pagination: controlled page ${storedPage} is out of range. Requesting page ${range.page}.`,
|
||||
);
|
||||
}
|
||||
|
||||
setStoredPage(range.page);
|
||||
}, [controlledPage, isLoading, storedPage, range.page, setStoredPage]);
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
if (controlledPage !== undefined && !onPageChange) {
|
||||
consoleWarning(
|
||||
"DataGrid pagination: controlled `page` requires `onPageChange`.",
|
||||
);
|
||||
}
|
||||
if (controlledPageSize !== undefined && !onPageSizeChange) {
|
||||
consoleWarning(
|
||||
"DataGrid pagination: controlled `pageSize` requires `onPageSizeChange`.",
|
||||
);
|
||||
}
|
||||
if (pageSizeOptions.length === 0) {
|
||||
consoleWarning(
|
||||
"DataGrid pagination: `pageSizeOptions` must not be empty.",
|
||||
);
|
||||
}
|
||||
if (new Set(pageSizeOptions).size !== pageSizeOptions.length) {
|
||||
consoleWarning(
|
||||
"DataGrid pagination: `pageSizeOptions` must contain unique values.",
|
||||
);
|
||||
}
|
||||
if (pageSizeOptions.some((size) => !Number.isInteger(size) || size <= 0)) {
|
||||
consoleWarning(
|
||||
"DataGrid pagination: `pageSizeOptions` must contain positive integers.",
|
||||
);
|
||||
}
|
||||
if (!pageSizeOptions.includes(pageSize)) {
|
||||
consoleWarning(
|
||||
"DataGrid pagination: the effective `pageSize` is missing from `pageSizeOptions`.",
|
||||
);
|
||||
}
|
||||
if (totalRows !== undefined && totalRows < 0) {
|
||||
consoleWarning("DataGrid pagination: `totalRows` must not be negative.");
|
||||
}
|
||||
}
|
||||
|
||||
const summaryRange =
|
||||
totalRows === undefined && isLoading ? previousSummaryRange.current : range;
|
||||
|
||||
return {
|
||||
...range,
|
||||
from: summaryRange.from,
|
||||
to: summaryRange.to,
|
||||
pageSize,
|
||||
pageSizeOptions,
|
||||
totalRows,
|
||||
isLoading,
|
||||
setPage,
|
||||
setPageSize,
|
||||
};
|
||||
}
|
||||
|
||||
export { useDataGridPagination };
|
||||
export type { DataGridPaginationRows };
|
||||
@@ -0,0 +1,61 @@
|
||||
import React, { forwardRef } from "react";
|
||||
import { BodyShort } from "../../../typography";
|
||||
import { cl } from "../../../utils/helpers";
|
||||
import { useI18n } from "../../../utils/i18n/i18n.hooks";
|
||||
import { useDataGridRootPaginationContext } from "../../root/DataGridRoot.context";
|
||||
|
||||
interface DataGridPaginationSummaryProps extends React.HTMLAttributes<HTMLParagraphElement> {
|
||||
children?: never;
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows which rows are visible, e.g. "Showing 1–10 of 150 rows".
|
||||
*
|
||||
* @see 🏷️ {@link DataGridPaginationSummaryProps}
|
||||
*
|
||||
* @example
|
||||
* ```jsx
|
||||
* <DataGrid columns={columns} data={rows} pagination={{}}>
|
||||
* <DataGrid.Pagination>
|
||||
* <DataGrid.PaginationSummary />
|
||||
* </DataGrid.Pagination>
|
||||
* </DataGrid>
|
||||
* ```
|
||||
*/
|
||||
const DataGridPaginationSummary = forwardRef<
|
||||
HTMLParagraphElement,
|
||||
DataGridPaginationSummaryProps
|
||||
>(({ className, ...rest }, forwardedRef) => {
|
||||
const { from, to, totalRows } = useDataGridRootPaginationContext(
|
||||
true,
|
||||
"[Aksel] DataGrid.PaginationSummary must be used within <DataGrid pagination={...}>",
|
||||
);
|
||||
const translate = useI18n("DataGrid");
|
||||
|
||||
let text: string;
|
||||
if (to === 0) {
|
||||
text = translate("Pagination.summaryEmpty");
|
||||
} else if (totalRows === undefined) {
|
||||
text = translate("Pagination.summaryUnknownTotal", { from, to });
|
||||
} else {
|
||||
text = translate("Pagination.summary", { from, to, totalRows });
|
||||
}
|
||||
|
||||
return (
|
||||
<BodyShort
|
||||
{...rest}
|
||||
ref={forwardedRef}
|
||||
className={cl("aksel-data-grid__pagination-summary", className)}
|
||||
>
|
||||
{text}
|
||||
</BodyShort>
|
||||
);
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace DataGridPaginationSummary {
|
||||
export type Props = DataGridPaginationSummaryProps;
|
||||
}
|
||||
|
||||
export { DataGridPaginationSummary };
|
||||
export type { DataGridPaginationSummaryProps };
|
||||
@@ -1,3 +1,35 @@
|
||||
import type { DataGridPaginationSharedOptions } from "../pagination/root/DataGridPagination.types";
|
||||
|
||||
type DataGridPaginationConfig = DataGridPaginationSharedOptions & {
|
||||
/**
|
||||
* Whether completed page and page-size changes are announced.
|
||||
* @default true
|
||||
*/
|
||||
announcePageChanges?: boolean;
|
||||
} & (
|
||||
| {
|
||||
/**
|
||||
* Client mode: DataGrid receives all rows and slices the current page.
|
||||
* @default "client"
|
||||
*/
|
||||
mode?: "client";
|
||||
totalRows?: never;
|
||||
hasNextPage?: never;
|
||||
}
|
||||
| {
|
||||
/** Server mode: DataGrid receives the current page's rows. */
|
||||
mode: "server";
|
||||
totalRows: number;
|
||||
hasNextPage?: never;
|
||||
}
|
||||
| {
|
||||
/** Server mode: DataGrid receives the current page's rows. */
|
||||
mode: "server";
|
||||
totalRows?: never;
|
||||
hasNextPage: boolean;
|
||||
}
|
||||
);
|
||||
|
||||
type DataGridSettings = {
|
||||
/**
|
||||
* Controls vertical cell padding.
|
||||
@@ -48,4 +80,4 @@ type DataGridSettings = {
|
||||
columnDisplay?: { id: string; visible: boolean | "always" }[];
|
||||
};
|
||||
|
||||
export type { DataGridSettings };
|
||||
export type { DataGridPaginationConfig, DataGridSettings };
|
||||
@@ -2,6 +2,7 @@ import type { resolveDataGridSettings } from "../../data-grid-preferences/helper
|
||||
import type { SelectionProps } from "../../data/table/hooks/useTableSelection";
|
||||
import type { ColumnDefinitions } from "../../data/table/root/DataGridTable.types";
|
||||
import { createStrictContext } from "../../utils/helpers";
|
||||
import type { useDataGridPagination } from "../pagination/root/useDataGridPagination";
|
||||
import type { DataGridSettings } from "./DataGrid.types";
|
||||
|
||||
type DataGridContextValue<RowT = unknown> = {
|
||||
@@ -20,4 +21,53 @@ const { Provider: DataGridContextProvider, useContext: useDataGridContext } =
|
||||
errorMessage: "DataGrid hooks must be used within a <DataGrid />",
|
||||
});
|
||||
|
||||
export { DataGridContextProvider, useDataGridContext };
|
||||
type DataGridRootPaginationContextValue = ReturnType<
|
||||
typeof useDataGridPagination
|
||||
> & {
|
||||
mode: "client" | "server";
|
||||
};
|
||||
|
||||
const paginationContextErrorMessage =
|
||||
"DataGrid.Pagination must be used within a <DataGrid pagination={...}>";
|
||||
|
||||
const {
|
||||
Provider: DataGridRootPaginationContextProvider,
|
||||
useContext: usePaginationContext,
|
||||
} = createStrictContext<DataGridRootPaginationContextValue | { mode: "none" }>({
|
||||
name: "DataGridRootPaginationContext",
|
||||
errorMessage: paginationContextErrorMessage,
|
||||
});
|
||||
|
||||
function useDataGridRootPaginationContext<S extends boolean = true>(
|
||||
strict?: S,
|
||||
errorMessageOverride?: string,
|
||||
): S extends true
|
||||
? DataGridRootPaginationContextValue
|
||||
: DataGridRootPaginationContextValue | undefined;
|
||||
function useDataGridRootPaginationContext(
|
||||
strict = true,
|
||||
errorMessageOverride?: string,
|
||||
) {
|
||||
const context = usePaginationContext(strict, errorMessageOverride);
|
||||
|
||||
if (context?.mode === "none") {
|
||||
if (strict) {
|
||||
const error = new Error(
|
||||
errorMessageOverride ?? paginationContextErrorMessage,
|
||||
);
|
||||
error.name = "ContextError";
|
||||
Error.captureStackTrace?.(error, useDataGridRootPaginationContext);
|
||||
throw error;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return context;
|
||||
}
|
||||
|
||||
export {
|
||||
DataGridContextProvider,
|
||||
DataGridRootPaginationContextProvider,
|
||||
useDataGridContext,
|
||||
useDataGridRootPaginationContext,
|
||||
};
|
||||
@@ -5,10 +5,53 @@ import type { ColumnDefinitions } from "../../data/table/root/DataGridTable.type
|
||||
import { DataGridTable } from "../../data/table/root/DataGridTableRoot";
|
||||
import { cl } from "../../utils/helpers";
|
||||
import { useDataGridSettings } from "../hooks/useDataGridSettings";
|
||||
import type { DataGridSettings } from "./DataGrid.types";
|
||||
import { DataGridContextProvider } from "./DataGridRoot.context";
|
||||
import { DataGridPaginationInternal } from "../pagination/DataGridPaginationInternal";
|
||||
import { DataGridPaginationNavigation } from "../pagination/navigation/DataGridPaginationNavigation";
|
||||
import { DataGridPaginationPageSize } from "../pagination/page-size/DataGridPaginationPageSize";
|
||||
import { DataGridPagination } from "../pagination/root/DataGridPaginationRoot";
|
||||
import { DataGridPaginationSummary } from "../pagination/summary/DataGridPaginationSummary";
|
||||
import type {
|
||||
DataGridPaginationConfig,
|
||||
DataGridSettings,
|
||||
} from "./DataGrid.types";
|
||||
import {
|
||||
DataGridContextProvider,
|
||||
DataGridRootPaginationContextProvider,
|
||||
} from "./DataGridRoot.context";
|
||||
|
||||
interface DataGridProps<RowT> {
|
||||
type DataGridRowIdProps<RowT> =
|
||||
| {
|
||||
pagination?: never;
|
||||
/**
|
||||
* Function to get unique row ID from row data.
|
||||
*
|
||||
* If not provided, the row index will be used as ID.
|
||||
* This can cause issues if your data changes dynamically,
|
||||
* so it's recommended to provide a stable ID if possible.
|
||||
*/
|
||||
getRowId?: (rowData: RowT) => string;
|
||||
}
|
||||
| {
|
||||
/**
|
||||
* Enables root-owned pagination state. Pages are zero-indexed.
|
||||
*
|
||||
* Client mode (default) accepts all rows; server mode accepts the current
|
||||
* page and requires exactly one of `totalRows` or `hasNextPage`.
|
||||
*
|
||||
* Requires `getRowId`.
|
||||
*/
|
||||
pagination: DataGridPaginationConfig;
|
||||
/**
|
||||
* Function to get unique row ID from row data.
|
||||
*
|
||||
* Required with `pagination`, so row IDs stay unique across pages.
|
||||
*/
|
||||
getRowId: (rowData: RowT) => string;
|
||||
};
|
||||
|
||||
type DataGridProps<RowT> = DataGridBaseProps<RowT> & DataGridRowIdProps<RowT>;
|
||||
|
||||
interface DataGridBaseProps<RowT> {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
style?: React.CSSProperties;
|
||||
@@ -23,14 +66,6 @@ interface DataGridProps<RowT> {
|
||||
* object are used to render the cells based on `columnDefinitions`.
|
||||
*/
|
||||
data: RowT[];
|
||||
/**
|
||||
* Function to get unique row ID from row data.
|
||||
*
|
||||
* If not provided, the row index will be used as ID.
|
||||
* This can cause issues if your data changes dynamically,
|
||||
* so it's recommended to provide a stable ID if possible.
|
||||
*/
|
||||
getRowId?: (rowData: RowT) => string;
|
||||
/**
|
||||
* Object with props related to row selection.
|
||||
*/
|
||||
@@ -77,11 +112,35 @@ interface DataGridComponent {
|
||||
* </DataGrid>
|
||||
*/
|
||||
Preferences: typeof DataGridPreferences;
|
||||
/**
|
||||
* @see 🏷️ {@link DataGridPagination.Props}
|
||||
*
|
||||
* @example
|
||||
* <DataGrid columns={columns} data={rows} getRowId={(row) => row.id} pagination={{ defaultPageSize: 10 }}>
|
||||
* <DataGrid.Table />
|
||||
* <DataGrid.Pagination />
|
||||
* </DataGrid>
|
||||
*/
|
||||
Pagination: typeof DataGridPagination;
|
||||
/**
|
||||
* @see 🏷️ {@link DataGridPaginationSummary.Props}
|
||||
*/
|
||||
PaginationSummary: typeof DataGridPaginationSummary;
|
||||
/**
|
||||
* @see 🏷️ {@link DataGridPaginationPageSize.Props}
|
||||
*/
|
||||
PaginationPageSize: typeof DataGridPaginationPageSize;
|
||||
/**
|
||||
* @see 🏷️ {@link DataGridPaginationNavigation.Props}
|
||||
*/
|
||||
PaginationNavigation: typeof DataGridPaginationNavigation;
|
||||
}
|
||||
|
||||
/**
|
||||
* Component for displaying tabular data.
|
||||
*
|
||||
* **WARNING: This component is in active development and may receive breaking changes outside major releases!**
|
||||
*
|
||||
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/datagrid)
|
||||
* @see 🏷️ {@link DataGridProps}
|
||||
*
|
||||
@@ -100,6 +159,7 @@ const DataGridInternal = forwardRef<HTMLDivElement, DataGridProps<any>>(
|
||||
className,
|
||||
columns,
|
||||
data,
|
||||
pagination,
|
||||
getRowId,
|
||||
selection,
|
||||
isLoading = false,
|
||||
@@ -127,7 +187,19 @@ const DataGridInternal = forwardRef<HTMLDivElement, DataGridProps<any>>(
|
||||
tableSettings={resolvedSettings}
|
||||
updateTableSettings={updateSettings}
|
||||
>
|
||||
{children}
|
||||
{pagination ? (
|
||||
<DataGridPaginationInternal
|
||||
pagination={pagination}
|
||||
rowCount={data.length}
|
||||
isLoading={isLoading}
|
||||
>
|
||||
{children}
|
||||
</DataGridPaginationInternal>
|
||||
) : (
|
||||
<DataGridRootPaginationContextProvider mode="none">
|
||||
{children}
|
||||
</DataGridRootPaginationContextProvider>
|
||||
)}
|
||||
</DataGridContextProvider>
|
||||
</div>
|
||||
);
|
||||
@@ -137,6 +209,10 @@ const DataGridInternal = forwardRef<HTMLDivElement, DataGridProps<any>>(
|
||||
const DataGrid = DataGridInternal as unknown as DataGridComponent;
|
||||
DataGrid.Table = DataGridTable;
|
||||
DataGrid.Preferences = DataGridPreferences;
|
||||
DataGrid.Pagination = DataGridPagination;
|
||||
DataGrid.PaginationSummary = DataGridPaginationSummary;
|
||||
DataGrid.PaginationPageSize = DataGridPaginationPageSize;
|
||||
DataGrid.PaginationNavigation = DataGridPaginationNavigation;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace DataGrid {
|
||||
@@ -162,6 +238,30 @@ export namespace DataGrid {
|
||||
// biome-ignore lint/suspicious/noShadow: intentional namespace re-export
|
||||
export type Props = DataGridPreferences.Props;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace Pagination {
|
||||
// biome-ignore lint/suspicious/noShadow: intentional namespace re-export
|
||||
export type Props = DataGridPagination.Props;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace PaginationSummary {
|
||||
// biome-ignore lint/suspicious/noShadow: intentional namespace re-export
|
||||
export type Props = DataGridPaginationSummary.Props;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace PaginationPageSize {
|
||||
// biome-ignore lint/suspicious/noShadow: intentional namespace re-export
|
||||
export type Props = DataGridPaginationPageSize.Props;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-namespace
|
||||
export namespace PaginationNavigation {
|
||||
// biome-ignore lint/suspicious/noShadow: intentional namespace re-export
|
||||
export type Props = DataGridPaginationNavigation.Props;
|
||||
}
|
||||
}
|
||||
|
||||
export { DataGrid, DataGridInternal }; // DataGridRoot needs to be exported b.c. of docgen
|
||||
|
||||
@@ -84,9 +84,6 @@ export const Attachments: Story = {
|
||||
options: ["none", "x", "y", "both"],
|
||||
},
|
||||
},
|
||||
args: {
|
||||
overflow: "none",
|
||||
},
|
||||
};
|
||||
|
||||
export const Chromatic = renderStoriesForChromatic({
|
||||
|
||||
@@ -3,6 +3,7 @@ import React, { useState } from "react";
|
||||
import { expect, fn, userEvent, within } from "storybook/test";
|
||||
import { Button } from "../../button";
|
||||
import { DataGrid } from "../../data-grid";
|
||||
import { ResetPageOnSort } from "../../data-grid/pagination/DataGridPagination.stories";
|
||||
import { HGrid } from "../../primitives/grid";
|
||||
import { VStack } from "../../primitives/stack";
|
||||
import { Tag } from "../../tag";
|
||||
@@ -498,6 +499,146 @@ export const SelectionPagination: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
type SelectionPageRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
children?: SelectionPageRow[];
|
||||
};
|
||||
|
||||
const selectionPageRows: SelectionPageRow[] = [
|
||||
{
|
||||
id: "alpha",
|
||||
name: "Alpha",
|
||||
children: [
|
||||
{
|
||||
id: "alpha-child",
|
||||
name: "Alpha child",
|
||||
children: [{ id: "alpha-grandchild", name: "Alpha grandchild" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: "beta", name: "Beta" },
|
||||
{
|
||||
id: "gamma",
|
||||
name: "Gamma",
|
||||
children: [
|
||||
{
|
||||
id: "gamma-child",
|
||||
name: "Gamma child",
|
||||
children: [{ id: "gamma-grandchild", name: "Gamma grandchild" }],
|
||||
},
|
||||
],
|
||||
},
|
||||
{ id: "delta", name: "Delta" },
|
||||
];
|
||||
|
||||
const selectionPageColumnDef: ColumnDefinitions<SelectionPageRow> = [
|
||||
{ id: "name", header: "Name", bodyCell: (row) => row.name },
|
||||
];
|
||||
|
||||
export const TestSelectionAcrossPages: Story = {
|
||||
render: () => {
|
||||
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<output aria-label="Selected row IDs">
|
||||
{JSON.stringify([...selectedRowIds].sort())}
|
||||
</output>
|
||||
<DataGrid
|
||||
columns={selectionPageColumnDef}
|
||||
data={selectionPageRows}
|
||||
getRowId={(row) => row.id}
|
||||
selection={{
|
||||
mode: "multiple",
|
||||
onSelectedRowIdsChange: setSelectedRowIds,
|
||||
}}
|
||||
pagination={{ defaultPageSize: 2, pageSizeOptions: [2] }}
|
||||
>
|
||||
<DataGrid.Table<SelectionPageRow>
|
||||
subRows={{ getRows: (row) => row.children ?? [] }}
|
||||
/>
|
||||
<DataGrid.Pagination />
|
||||
</DataGrid>
|
||||
</>
|
||||
);
|
||||
},
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const selectedIds = canvas.getByRole("status", {
|
||||
name: "Selected row IDs",
|
||||
});
|
||||
const firstPageIds = ["alpha", "alpha-child", "alpha-grandchild", "beta"];
|
||||
const secondPageIds = ["delta", "gamma", "gamma-child", "gamma-grandchild"];
|
||||
|
||||
expect(canvas.queryByText("Alpha child")).not.toBeInTheDocument();
|
||||
expect(canvas.queryByText("Alpha grandchild")).not.toBeInTheDocument();
|
||||
await userEvent.click(
|
||||
canvas.getByRole("checkbox", { name: "Velg alle synlige rader" }),
|
||||
);
|
||||
expect(selectedIds.textContent).toBe(JSON.stringify(firstPageIds));
|
||||
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Neste side" }));
|
||||
expect(canvas.getByText("Gamma")).toBeInTheDocument();
|
||||
expect(canvas.queryByText("Alpha")).not.toBeInTheDocument();
|
||||
expect(selectedIds.textContent).toBe(JSON.stringify(firstPageIds));
|
||||
for (const checkbox of canvas.getAllByRole("checkbox")) {
|
||||
expect(checkbox).not.toBeChecked();
|
||||
}
|
||||
|
||||
await userEvent.click(
|
||||
canvas.getByRole("checkbox", { name: "Velg alle synlige rader" }),
|
||||
);
|
||||
expect(selectedIds.textContent).toBe(
|
||||
JSON.stringify([...firstPageIds, ...secondPageIds].sort()),
|
||||
);
|
||||
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Forrige side" }));
|
||||
for (const checkbox of canvas.getAllByRole("checkbox")) {
|
||||
expect(checkbox).toBeChecked();
|
||||
}
|
||||
await userEvent.click(
|
||||
canvas.getByRole("checkbox", {
|
||||
name: "Fjern alle synlige valgte rader",
|
||||
}),
|
||||
);
|
||||
expect(selectedIds.textContent).toBe(JSON.stringify(secondPageIds));
|
||||
for (const checkbox of canvas.getAllByRole("checkbox")) {
|
||||
expect(checkbox).not.toBeChecked();
|
||||
}
|
||||
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Neste side" }));
|
||||
for (const checkbox of canvas.getAllByRole("checkbox")) {
|
||||
expect(checkbox).toBeChecked();
|
||||
}
|
||||
await userEvent.click(
|
||||
canvas.getByRole("button", { name: "Vis under-rader" }),
|
||||
);
|
||||
expect(
|
||||
within(canvas.getByRole("row", { name: /Gamma child/ })).getByRole(
|
||||
"checkbox",
|
||||
),
|
||||
).toBeChecked();
|
||||
await userEvent.click(
|
||||
canvas.getByRole("button", { name: "Vis under-rader" }),
|
||||
);
|
||||
expect(
|
||||
within(canvas.getByRole("row", { name: /Gamma grandchild/ })).getByRole(
|
||||
"checkbox",
|
||||
),
|
||||
).toBeChecked();
|
||||
await userEvent.click(
|
||||
canvas.getByRole("checkbox", {
|
||||
name: "Fjern alle synlige valgte rader",
|
||||
}),
|
||||
);
|
||||
expect(selectedIds.textContent).toBe("[]");
|
||||
for (const checkbox of canvas.getAllByRole("checkbox")) {
|
||||
expect(checkbox).not.toBeChecked();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
export const StickySelection: Story = {
|
||||
render: () => {
|
||||
return (
|
||||
@@ -763,6 +904,36 @@ export const SortableColumnsUncontrolled: Story = {
|
||||
},
|
||||
};
|
||||
|
||||
export const TestControlledSortResetsPage: Story = {
|
||||
render: ResetPageOnSort.render,
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const navigation = within(
|
||||
canvas.getByRole("navigation", { name: "Sidenavigering" }),
|
||||
);
|
||||
|
||||
for (const direction of ["ascending", "descending", "none"]) {
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Neste side" }));
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Neste side" }));
|
||||
expect(navigation.getByText("Side 3 av 14")).toBeInTheDocument();
|
||||
await userEvent.click(canvas.getByRole("button", { name: "Id" }));
|
||||
|
||||
expect(navigation.getByText("Side 1 av 14")).toBeInTheDocument();
|
||||
expect(canvas.getByRole("columnheader", { name: "Id" })).toHaveAttribute(
|
||||
"aria-sort",
|
||||
direction,
|
||||
);
|
||||
const firstRow = canvas.getAllByRole("row")[1];
|
||||
expect(
|
||||
within(firstRow).getByRole("cell", {
|
||||
name: direction === "descending" ? "137" : "1",
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
expect(canvas.getAllByRole("row")).toHaveLength(11);
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
const rowClickSpy = fn();
|
||||
|
||||
const rowClickColumnDef: ColumnDefinitions<UserDataTest> = [
|
||||
|
||||
Loaded 100 of 220 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user