Compare commits

..
Author SHA1 Message Date
Ken b30b193384 test: Add story for testing inline use of svg 2026-11-03 11:18:09 +01:00
Ken c0f635459d fix: Add print and forced-color support 2026-11-02 15:52:08 +01:00
Ken 1fcf982453 feat: Added css-imports for every icon 2026-11-02 15:33:01 +01:00
122 changed files with 1367 additions and 1281 deletions

No files matched your search

+6
View File
@@ -0,0 +1,6 @@
---
"@navikt/ds-react": patch
"@navikt/aksel-icons": patch
---
ESM: Mark ESM output as side-effect free, so bundlers like webpack, Turbopack and esbuild can tree-shake unused components and icons.
+5
View File
@@ -0,0 +1,5 @@
---
"@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.
+1 -7
View File
@@ -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 }}
+2
View File
@@ -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
-10
View File
@@ -1,15 +1,5 @@
# @navikt/aksel-icons
## 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
+31
View File
@@ -59,6 +59,37 @@ 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
@@ -0,0 +1,44 @@
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=/);
});
});
+95
View File
@@ -0,0 +1,95 @@
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,
};
+79
View File
@@ -2,6 +2,12 @@ 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",
@@ -25,3 +31,76 @@ 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
-3
View File
@@ -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

-8
View File
@@ -1,8 +0,0 @@
name: Gift
category: Home
sub_category: Toys
keywords:
- gave
- presang
variant: Stroke
created_at: 31.10.2026
-3
View File
@@ -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

-8
View File
@@ -1,8 +0,0 @@
name: GiftFill
category: Home
sub_category: Toys
keywords:
- gave
- presang
variant: Fill
created_at: 31.10.2026
+1 -1
View File
@@ -2,6 +2,6 @@ name: Keyboard
category: Interface
sub_category: Devices
keywords:
- Tastatur
- Keyboard
variant: Stroke
created_at: 26.10.2026
+1 -1
View File
@@ -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
+1 -4
View File
@@ -2,9 +2,6 @@ name: TableCross
category: Interface
sub_category: Table
keywords:
- tabell
- datagrid
- fjern
- slett
- TableCross
variant: Stroke
created_at: 26.10.2026
+1 -4
View File
@@ -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
+1 -4
View File
@@ -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
+1 -2
View File
@@ -2,7 +2,6 @@ name: TableGrid
category: Interface
sub_category: Table
keywords:
- tabell
- datagrid
- TableGrid
variant: Stroke
created_at: 26.10.2026
+1 -2
View File
@@ -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
+1 -4
View File
@@ -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
+1 -4
View File
@@ -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
+9 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "8.22.0",
"version": "8.21.0",
"author": "Aksel | Nav",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
@@ -23,7 +23,9 @@
"access": "public",
"provenance": true
},
"sideEffects": false,
"sideEffects": [
"*.css"
],
"license": "MIT",
"main": "./dist/react/cjs/index.js",
"module": "./dist/react/esm/index.js",
@@ -44,6 +46,7 @@
"default": "./dist/metadata.js"
},
"./svg/*.svg": "./dist/svg/*.svg",
"./css/*.css": "./dist/css/*.css",
"./package.json": "./package.json"
},
"typesVersions": {
@@ -62,10 +65,11 @@
"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",
"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",
"fetch-new:icons": "tsx --env-file=.env config/figma/index.ts",
"zip:icons": "node config/zip.js",
"test": "vitest run",
@@ -87,10 +91,11 @@
"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"
}
}
-2
View File
@@ -1,7 +1,5 @@
# @navikt/aksel-stylelint
## 8.22.0
## 8.21.0
## 8.20.1
+4 -4
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "8.22.0",
"version": "8.21.0",
"type": "module",
"engines": {
"node": ">=18.12.0"
@@ -43,13 +43,13 @@
"dev": "yarn watch:lint"
},
"devDependencies": {
"@navikt/ds-css": "^8.22.0",
"@navikt/ds-tokens": "^8.22.0",
"@navikt/ds-css": "^8.21.0",
"@navikt/ds-tokens": "^8.21.0",
"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"
}
-2
View File
@@ -1,7 +1,5 @@
# @navikt/aksel
## 8.22.0
## 8.21.0
## 8.20.1
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "8.22.0",
"version": "8.21.0",
"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": {
-2
View File
@@ -1,7 +1,5 @@
# @navikt/ds-css
## 8.22.0
## 8.21.0
### Minor Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "8.22.0",
"version": "8.21.0",
"description": "CSS for Nav Designsystem",
"author": "Aksel | Nav designsystem team",
"keywords": [
@@ -32,7 +32,7 @@
"css:get-version": "node config/get-version.js"
},
"devDependencies": {
"@navikt/ds-tokens": "^8.22.0",
"@navikt/ds-tokens": "^8.21.0",
"browserslist": "^4.28.7",
"esbuild": "^0.28.2",
"fast-glob": "3.3.3",
-18
View File
@@ -1,23 +1,5 @@
# @navikt/ds-react
## 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: 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))
- 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
+5 -5
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "8.22.0",
"version": "8.21.0",
"description": "React components from the Norwegian Labour and Welfare Administration.",
"author": "Aksel, a team part of the Norwegian Labour and Welfare Administration.",
"license": "MIT",
@@ -788,8 +788,8 @@
"dependencies": {
"@floating-ui/react": "0.27.20",
"@floating-ui/react-dom": "^2.1.9",
"@navikt/aksel-icons": "^8.22.0",
"@navikt/ds-tokens": "^8.22.0",
"@navikt/aksel-icons": "^8.21.0",
"@navikt/ds-tokens": "^8.21.0",
"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",
@@ -1,5 +1,4 @@
import type { Locale } from "date-fns";
import { format } from "date-fns/format";
import { type Locale, format } from "date-fns";
import React, { useEffect, useRef } from "react";
import type { CalendarDay, Modifiers } from "react-day-picker";
import { cl } from "../../utils/helpers";
@@ -1,10 +1,12 @@
import type { Locale } from "date-fns";
import { format } from "date-fns/format";
import { getMonth } from "date-fns/getMonth";
import { getYear } from "date-fns/getYear";
import { setMonth } from "date-fns/setMonth";
import { setYear } from "date-fns/setYear";
import { startOfMonth } from "date-fns/startOfMonth";
import {
type Locale,
format,
getMonth,
getYear,
setMonth,
setYear,
startOfMonth,
} from "date-fns";
import React, { type ChangeEvent, useCallback } from "react";
import { type CalendarMonth, useDayPicker } from "react-day-picker";
import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons";
@@ -1,4 +1,4 @@
import { isWeekend } from "date-fns/isWeekend";
import { isWeekend } from "date-fns";
import React, { useCallback, useState } from "react";
import {
type ClassNames,
@@ -1,4 +1,4 @@
import { isSameDay } from "date-fns/isSameDay";
import { isSameDay } from "date-fns";
import React, { forwardRef, useRef, useState } from "react";
import type { DateRange } from "react-day-picker";
import { useId } from "../../utils-external";
@@ -1,5 +1,4 @@
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
import { isWeekend } from "date-fns/isWeekend";
import { differenceInCalendarDays, isWeekend } from "date-fns";
import React, { useCallback, useState } from "react";
import { type DayEventHandler, dateMatchModifiers } from "react-day-picker";
import { getLocaleFromString } from "../../utils/components/date/Date.locale";
@@ -1,6 +1,4 @@
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
import { isSameDay } from "date-fns/isSameDay";
import { isWeekend } from "date-fns/isWeekend";
import { differenceInCalendarDays, isSameDay, isWeekend } from "date-fns";
import React, { useState } from "react";
import { dateMatchModifiers } from "react-day-picker";
import { getLocaleFromString } from "../../utils/components/date/Date.locale";
@@ -343,21 +343,18 @@ export const EnterNotSubmittingForm: StoryObj<{
args: {
onSubmit: fn(),
},
render: ({ onSubmit }) => (
<form
onSubmit={(event) => {
event.preventDefault();
onSubmit(event);
}}
>
<UNSAFE_Combobox
options={options}
label="Hva er dine favorittfrukter?"
isMultiSelect
allowNewValues
/>
</form>
),
render: ({ onSubmit }) => {
return (
<form action="https://www.aksel.nav.no" method="get" onSubmit={onSubmit}>
<UNSAFE_Combobox
options={options}
label="Hva er dine favorittfrukter?"
isMultiSelect
allowNewValues
/>
</form>
);
},
play: async ({ canvasElement, args }) => {
args.onSubmit.mockClear();
const canvas = within(canvasElement);
@@ -115,7 +115,6 @@ const FilteredOptions = () => {
ref={setFilteredOptionsRef}
// biome-ignore lint/a11y/noNoninteractiveElementToInteractiveRole: The <ul> element acts as a listbox in the combobox.
role="listbox"
tabIndex={-1}
className="aksel-combobox__list-options"
>
{isValueNew && !maxSelected.isLimitReached && allowNewValues && (
@@ -1,6 +1,6 @@
import { fireEvent, render, screen, waitFor } from "@testing-library/react";
import React, { useState } from "react";
import { afterEach, describe, expect, test, vi } from "vitest";
import { describe, expect, test } from "vitest";
import { Button } from "../../button";
import { Modal } from "../index";
@@ -18,10 +18,6 @@ const Test = () => {
};
describe("Modal", () => {
afterEach(() => {
vi.restoreAllMocks();
});
test("should be visible", async () => {
render(<Test />);
expect(await screen.findByText("Foobar")).toBeVisible();
@@ -33,37 +29,25 @@ describe("Modal", () => {
expect(screen.getByText("Foobar")).not.toBeVisible();
});
test.each([false, true])(
"should toggle scroll lock (scrollbar-gutter support: %s)",
async (supportsScrollbarGutter) => {
vi.spyOn(CSS, "supports").mockReturnValue(supportsScrollbarGutter);
render(<Test />);
test("should toggle scroll lock", async () => {
render(<Test />);
const lockedElement = supportsScrollbarGutter
? document.body
: document.documentElement;
await waitFor(() => {
expect(document.documentElement.style.overflowX).toBe("hidden");
});
await waitFor(() => {
expect(document.documentElement.style.overflowY).toBe("hidden");
});
await waitFor(() => {
expect(document.documentElement.style.scrollBehavior).toBe("unset");
});
await waitFor(() => {
expect(document.documentElement.style.scrollbarGutter).toBe("stable");
});
await waitFor(() => {
expect(lockedElement.style.overflowX).toBe("hidden");
});
await waitFor(() => {
expect(lockedElement.style.overflowY).toBe("hidden");
});
await waitFor(() => {
expect(document.documentElement.style.scrollBehavior).toBe(
supportsScrollbarGutter ? "" : "unset",
);
});
await waitFor(() => {
expect(document.documentElement.style.scrollbarGutter).toBe("stable");
});
fireEvent.click(screen.getByText("Close"));
await waitFor(() => {
expect(document.documentElement.style.cssText).toBe("");
expect(lockedElement.style.overflowX).toBe("");
expect(lockedElement.style.overflowY).toBe("");
});
},
);
fireEvent.click(screen.getByText("Close"));
await waitFor(() => {
expect(document.documentElement.style.cssText).toBe("");
});
});
});
@@ -83,25 +83,6 @@ export const DisabledMonths = () => (
/>
);
/**
* Only June is enabled. ArrowRight/ArrowLeft on June keeps focus on June instead
* of searching forever. Keyboard search stops at the
* fromDate/toDate years, or after 100 years when no bounds are set.
*/
export const KeyboardNoEnabledMonth = () => (
<div>
<p>Focus June and press ArrowRight or ArrowLeft. Focus stays on June.</p>
<MonthPicker.Standalone
defaultSelected={new Date(year, 5, 1)}
disabled={[
(date) => !(date.getFullYear() === year && date.getMonth() === 5),
]}
fromDate={new Date(year, 0, 1)}
toDate={new Date(year, 11, 1)}
/>
</div>
);
export const Standalone = () => {
return <MonthPicker.Standalone defaultSelected={new Date(`Jan 1 ${year}`)} />;
};
@@ -1,8 +1,10 @@
import { compareAsc } from "date-fns/compareAsc";
import { compareDesc } from "date-fns/compareDesc";
import { format } from "date-fns/format";
import { isSameMonth } from "date-fns/isSameMonth";
import { setYear } from "date-fns/setYear";
import {
compareAsc,
compareDesc,
format,
isSameMonth,
setYear,
} from "date-fns";
import React, { useEffect, useRef } from "react";
import { dateIsInCurrentMonth, isMatch } from "../../utils/date";
import { cl } from "../../utils/helpers";
@@ -1,9 +1,11 @@
import { format } from "date-fns/format";
import { isAfter } from "date-fns/isAfter";
import { isBefore } from "date-fns/isBefore";
import { setYear } from "date-fns/setYear";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
import {
format,
isAfter,
isBefore,
setYear,
startOfMonth,
startOfYear,
} from "date-fns";
import React from "react";
import { ArrowLeftIcon, ArrowRightIcon } from "@navikt/aksel-icons";
import { Button } from "../../button";
@@ -1,5 +1,4 @@
import type { Locale } from "date-fns";
import { isSameYear } from "date-fns/isSameYear";
import { type Locale, isSameYear } from "date-fns";
import React from "react";
import { clampDisplayYear } from "../../utils/date";
import { createStrictContext } from "../../utils/helpers";
@@ -1,108 +0,0 @@
import { isSameMonth } from "date-fns/isSameMonth";
import { describe, expect, test, vi } from "vitest";
import { nextEnabled } from "./MonthPicker.util";
const months = Array.from({ length: 12 }, (_, i) => new Date(2024, i, 1));
const june2024 = new Date(2024, 5, 1);
/* Disables every month except `enabled`. Throws instead of hanging if the search becomes unbounded. */
const onlyEnabled = (...enabled: Date[]) => {
let calls = 0;
return (date: Date) => {
calls += 1;
if (calls > 5000) {
throw new Error("Month search did not terminate");
}
return !enabled.some((month) => isSameMonth(month, date));
};
};
const search = (
key: string,
disabled: (date: Date) => boolean,
options: { dropdownCaption?: boolean; fromDate?: Date; toDate?: Date } = {},
) => {
const setYearState = vi.fn();
const result = nextEnabled(
months,
key,
[disabled],
june2024,
setYearState,
june2024,
options.dropdownCaption ?? false,
options.fromDate,
options.toDate,
);
return { result, setYearState };
};
describe("nextEnabled", () => {
test.each(["ArrowRight", "ArrowLeft", "ArrowDown", "ArrowUp"])(
"keeps focus when no month is enabled in direction (%s, no bounds)",
(key) => {
const { result, setYearState } = search(key, onlyEnabled(june2024));
expect(result).toEqual(june2024);
expect(setYearState).not.toHaveBeenCalled();
},
);
test.each([false, true])(
"stops at fromDate/toDate years (dropdownCaption: %s)",
(dropdownCaption) => {
const bounds = {
dropdownCaption,
fromDate: new Date(2023, 0, 1),
toDate: new Date(2025, 11, 1),
};
const disabled = onlyEnabled(
june2024,
new Date(2022, 5, 1),
new Date(2026, 5, 1),
);
for (const key of ["ArrowRight", "ArrowLeft"]) {
const { result, setYearState } = search(key, disabled, bounds);
expect(result).toEqual(june2024);
expect(setYearState).not.toHaveBeenCalled();
}
},
);
test("skips disabled months/years and moves to the next enabled year", () => {
const target = new Date(2027, 2, 1);
const { result, setYearState } = search(
"ArrowRight",
onlyEnabled(june2024, target),
);
expect(result).toEqual(target);
expect(setYearState).toHaveBeenCalledTimes(1);
expect(setYearState.mock.calls[0][0].getFullYear()).toBe(2027);
});
test("finds previous enabled month within bounds", () => {
const target = new Date(2023, 10, 1);
const { result, setYearState } = search(
"ArrowLeft",
onlyEnabled(june2024, target),
{ fromDate: new Date(2023, 0, 1) },
);
expect(result).toEqual(target);
expect(setYearState.mock.calls[0][0].getFullYear()).toBe(2023);
});
test("searches at most 100 years without bounds", () => {
const within = new Date(2124, 0, 1);
expect(search("ArrowRight", onlyEnabled(june2024, within)).result).toEqual(
within,
);
const beyond = new Date(2125, 0, 1);
expect(search("ArrowRight", onlyEnabled(june2024, beyond)).result).toEqual(
june2024,
);
});
});
@@ -1,4 +1,4 @@
import { setYear } from "date-fns/setYear";
import { setYear } from "date-fns";
import type { Matcher } from "../../utils/components/date/Date.typeutils";
import { isMatch } from "../../utils/date";
@@ -56,14 +56,14 @@ export const nextEnabled = (
}
}
if (key === "ArrowRight") {
const nextMonth = searchEnabledMonth(
1,
const nextMonth = loopForward(
currentIndex,
months,
disabled,
yearState,
setYearState,
disabled,
false,
dropdownCaption,
fromDate,
toDate,
);
@@ -72,14 +72,14 @@ export const nextEnabled = (
}
}
if (key === "ArrowLeft") {
const prevMonth = searchEnabledMonth(
-1,
currentIndex,
const prevMonth = loopBack(
currentIndex - 1,
months,
disabled,
yearState,
setYearState,
false,
dropdownCaption,
fromDate,
toDate,
);
@@ -98,14 +98,14 @@ export const nextEnabled = (
return setYear(months[currentIndex + 4], yearState.getFullYear());
}
const fallbackNext = searchEnabledMonth(
1,
const fallbackNext = loopForward(
currentIndex,
months,
disabled,
yearState,
setYearState,
disabled,
true,
dropdownCaption,
fromDate,
toDate,
);
@@ -127,14 +127,14 @@ export const nextEnabled = (
)
)
return setYear(months[currentIndex - 4], Number(yearState.getFullYear()));
const fallbackPrev = searchEnabledMonth(
-1,
const fallbackPrev = loopBack(
currentIndex,
months,
disabled,
yearState,
setYearState,
true,
dropdownCaption,
fromDate,
toDate,
);
@@ -144,59 +144,90 @@ export const nextEnabled = (
return currentMonth;
};
/**
* Keyboard search for an enabled month never crosses fromDate/toDate years and
* gives up after this many years when no bounds are set, so a matcher that
* disables every remaining month cannot hang the page.
*/
const MAX_SEARCH_YEARS = 100;
const searchEnabledMonth = (
direction: 1 | -1,
const loopBack = (
currentIndex: number,
months: Date[],
disabled: Matcher[],
yearState: Date,
setYearState: (date: Date) => void,
rowCheck: boolean,
dropdownCaption: boolean,
fromDate?: Date,
toDate?: Date,
): { index: number; year: number } | undefined => {
const currentYear = yearState.getFullYear();
if (Number.isNaN(currentYear)) {
return;
}
const boundYear =
direction === 1 ? toDate?.getFullYear() : fromDate?.getFullYear();
const horizonYear = currentYear + direction * MAX_SEARCH_YEARS;
const lastYear = !boundYear
? horizonYear
: direction === 1
? Math.min(boundYear, horizonYear)
: Math.max(boundYear, horizonYear);
let nextYear = currentYear;
let monthIndex = currentIndex;
while (true) {
monthIndex += direction;
if (monthIndex < 0 || monthIndex > 11) {
if (nextYear === lastYear || (nextYear - lastYear) * direction > 0) {
let currentYear = setYear(yearState, Number(yearState.getFullYear()));
for (let i = currentIndex; i >= -1; i--) {
if (i === -1) {
if (
isOutOfRange(
dropdownCaption,
setYear(currentYear, Number(currentYear.getFullYear() - 1)),
fromDate,
toDate,
)
)
return;
}
nextYear += direction;
monthIndex = direction === 1 ? 0 : 11;
currentYear = setYear(currentYear, Number(currentYear.getFullYear() - 1));
setYearState(currentYear);
i = 11;
}
const month = months[i];
const isDisabled = !isMatch(
setYear(month, Number(currentYear.getFullYear())),
disabled,
);
if (rowCheck) {
if (isDisabled && getRow(i) !== getRow(currentIndex)) {
return { index: i, year: Number(currentYear.getFullYear()) };
}
} else {
if (isDisabled) {
return { index: i, year: Number(currentYear.getFullYear()) };
}
}
}
};
const isEnabled = !isMatch(setYear(months[monthIndex], nextYear), disabled);
if (
isEnabled &&
(!rowCheck || getRow(monthIndex) !== getRow(currentIndex))
) {
if (nextYear !== currentYear) {
setYearState(setYear(yearState, nextYear));
const loopForward = (
currentIndex: number,
months: Date[],
yearState: Date,
setYearState: (date: Date) => void,
disabled: Matcher[],
rowCheck: boolean,
dropdownCaption: boolean,
fromDate?: Date,
toDate?: Date,
): { index: number; year: number } | undefined => {
let currentYear = setYear(yearState, Number(yearState.getFullYear()));
for (let i = currentIndex + 1; i < months.length + 1; i++) {
if (i === 12) {
if (
isOutOfRange(
dropdownCaption,
setYear(currentYear, Number(currentYear.getFullYear() + 1)),
fromDate,
toDate,
)
)
return;
currentYear = setYear(currentYear, Number(currentYear.getFullYear() + 1));
setYearState(currentYear);
i = 0;
}
const month = months[i];
const isDisabled = !isMatch(
setYear(month, Number(currentYear.getFullYear())),
disabled,
);
if (rowCheck) {
if (isDisabled && getRow(i) !== getRow(currentIndex)) {
return { index: i, year: Number(currentYear.getFullYear()) };
}
} else {
if (isDisabled) {
return { index: i, year: Number(currentYear.getFullYear()) };
}
return { index: monthIndex, year: nextYear };
}
}
};
@@ -207,6 +238,25 @@ const getRow = (index: number): number => {
return 3;
};
const isOutOfRange = (
dropdownCaption: boolean,
year: Date,
fromDate?: Date,
toDate?: Date,
): boolean => {
if (
dropdownCaption &&
fromDate &&
toDate &&
(year.getFullYear() < fromDate?.getFullYear() ||
year.getFullYear() > toDate?.getFullYear())
) {
return true;
}
return false;
};
const nextOnRow = (
currentIndex: number,
months: Date[],
@@ -1,7 +1,4 @@
import { isSameMonth } from "date-fns/isSameMonth";
import { setMonth } from "date-fns/setMonth";
import { setYear } from "date-fns/setYear";
import { startOfMonth } from "date-fns/startOfMonth";
import { isSameMonth, setMonth, setYear, startOfMonth } from "date-fns";
import React, { useState } from "react";
import { BodyShort } from "../../typography";
import { isMatch } from "../../utils/date";
@@ -123,13 +123,9 @@ const PopoverRoot = forwardRef<HTMLDivElement, PopoverProps>(
],
});
/**
* Only give Floating UI a reference while open. Otherwise every closed instance
* computes (and commits) a position
*/
useClientLayoutEffect(() => {
refs.setReference(open ? anchorEl : null);
}, [anchorEl, open, refs]);
refs.setReference(anchorEl);
}, [anchorEl, refs]);
const floatingRef = useMergeRefs(refs.setFloating, ref);
@@ -1,6 +1,6 @@
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, test } from "vitest";
import { bench, describe } from "vitest";
import { HStack, VStack } from "../stack";
import { Box } from "./Box";
@@ -15,86 +15,82 @@ import { Box } from "./Box";
*/
describe("Box – single element", () => {
test("render throughput", async ({ bench }) => {
await bench("bare (no props)", () => {
renderToStaticMarkup(<Box>Hei</Box>);
}).run();
bench("bare (no props)", () => {
renderToStaticMarkup(<Box>Hei</Box>);
});
await bench("styling props only (background/border/radius/shadow)", () => {
renderToStaticMarkup(
<Box
background="neutral-soft"
borderColor="neutral-subtle"
borderWidth="1"
borderRadius="12"
shadow="dialog"
>
Hei
</Box>,
);
}).run();
bench("styling props only (background/border/radius/shadow)", () => {
renderToStaticMarkup(
<Box
background="neutral-soft"
borderColor="neutral-subtle"
borderWidth="1"
borderRadius="12"
shadow="dialog"
>
Hei
</Box>,
);
});
await bench("single responsive/primitive prop (padding)", () => {
renderToStaticMarkup(<Box padding="space-16">Hei</Box>);
}).run();
bench("single responsive/primitive prop (padding)", () => {
renderToStaticMarkup(<Box padding="space-16">Hei</Box>);
});
await bench("many primitive props (padding/margin/width/position/inset)", () => {
renderToStaticMarkup(
<Box
padding="space-16"
margin="space-8"
width="200px"
maxWidth="400px"
position="relative"
inset="space-4"
>
Hei
</Box>,
);
}).run();
bench("many primitive props (padding/margin/width/position/inset)", () => {
renderToStaticMarkup(
<Box
padding="space-16"
margin="space-8"
width="200px"
maxWidth="400px"
position="relative"
inset="space-4"
>
Hei
</Box>,
);
});
await bench("responsive object prop (padding per breakpoint)", () => {
renderToStaticMarkup(
<Box
padding={{
xs: "space-8",
sm: "space-12",
md: "space-16",
lg: "space-20",
xl: "space-24",
}}
>
Hei
</Box>,
);
}).run();
bench("responsive object prop (padding per breakpoint)", () => {
renderToStaticMarkup(
<Box
padding={{
xs: "space-8",
sm: "space-12",
md: "space-16",
lg: "space-20",
xl: "space-24",
}}
>
Hei
</Box>,
);
});
await bench("asChild (Slot merge path)", () => {
renderToStaticMarkup(
<Box padding="space-16" asChild>
<section>Hei</section>
</Box>,
);
}).run();
bench("asChild (Slot merge path)", () => {
renderToStaticMarkup(
<Box padding="space-16" asChild>
<section>Hei</section>
</Box>,
);
});
});
describe("Box – realistic tree (24 children)", () => {
const children = Array.from({ length: 24 }, (_, i) => i);
test("render throughput", async ({ bench }) => {
await bench("VStack > Box grid of cards", () => {
renderToStaticMarkup(
<VStack gap="space-16">
{children.map((i) => (
<HStack key={i} gap="space-8" align="center">
<Box padding="space-16" background="neutral-soft" borderRadius="8">
Card {i}
</Box>
</HStack>
))}
</VStack>,
);
}).run();
bench("VStack > Box grid of cards", () => {
renderToStaticMarkup(
<VStack gap="space-16">
{children.map((i) => (
<HStack key={i} gap="space-8" align="center">
<Box padding="space-16" background="neutral-soft" borderRadius="8">
Card {i}
</Box>
</HStack>
))}
</VStack>,
);
});
});
@@ -1,17 +1,19 @@
import type { Locale } from "date-fns";
import { addDays } from "date-fns/addDays";
import { addMonths } from "date-fns/addMonths";
import { addYears } from "date-fns/addYears";
import { differenceInDays } from "date-fns/differenceInDays";
import { differenceInMonths } from "date-fns/differenceInMonths";
import { differenceInYears } from "date-fns/differenceInYears";
import { endOfMonth } from "date-fns/endOfMonth";
import { endOfYear } from "date-fns/endOfYear";
import { format } from "date-fns/format";
import { startOfDay } from "date-fns/startOfDay";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
import { subDays } from "date-fns/subDays";
import {
type Locale,
addDays,
addMonths,
addYears,
differenceInDays,
differenceInMonths,
differenceInYears,
endOfMonth,
endOfYear,
format,
startOfDay,
startOfMonth,
startOfYear,
subDays,
} from "date-fns";
import React, { useCallback, useMemo } from "react";
import { Detail } from "../../typography/Detail";
import { useDateLocale, useI18n } from "../../utils/i18n/i18n.hooks";
@@ -1,4 +1,4 @@
import { isBefore } from "date-fns/isBefore";
import { isBefore } from "date-fns";
import React, { forwardRef } from "react";
import { useTimelineContext } from "../root/TimelineRoot.context";
import {
@@ -12,7 +12,7 @@ import {
useHover,
useInteractions,
} from "@floating-ui/react";
import { format } from "date-fns/format";
import { format } from "date-fns";
import React, { forwardRef, useMemo, useState } from "react";
import { useMergeRefs } from "../../utils/hooks";
import { useI18n } from "../../utils/i18n/i18n.hooks";
@@ -1,6 +1,4 @@
import { endOfDay } from "date-fns/endOfDay";
import { isSameDay } from "date-fns/isSameDay";
import { startOfDay } from "date-fns/startOfDay";
import { endOfDay, isSameDay, startOfDay } from "date-fns";
import React, { forwardRef, useMemo, useState } from "react";
import { consoleWarning } from "../../utils/helpers/consoleWarning";
import { TimelineAxisLabelsInternal } from "../axis-labels/TimelineAxisLabelsInternal";
@@ -1,6 +1,5 @@
import { renderHook } from "@testing-library/react";
import { addDays } from "date-fns/addDays";
import { isSameDay } from "date-fns/isSameDay";
import { addDays, isSameDay } from "date-fns";
import { describe, expect, test } from "vitest";
import {
useEarliestDate,
@@ -1,8 +1,4 @@
import { addDays } from "date-fns/addDays";
import { endOfDay } from "date-fns/endOfDay";
import { isAfter } from "date-fns/isAfter";
import { startOfDay } from "date-fns/startOfDay";
import { subDays } from "date-fns/subDays";
import { addDays, endOfDay, isAfter, startOfDay, subDays } from "date-fns";
import { useMemo } from "react";
import { horizontalPositionAndWidth } from "../utils/calc";
import { invisiblePeriods, withinADay } from "../utils/filter";
@@ -1,4 +1,4 @@
import { format } from "date-fns/format";
import { format } from "date-fns";
import React, { forwardRef } from "react";
import { BodyShort } from "../../typography/BodyShort";
import { cl } from "../../utils/helpers";
@@ -1,4 +1,4 @@
import { differenceInMilliseconds } from "date-fns/differenceInMilliseconds";
import { differenceInMilliseconds } from "date-fns";
const position = (date: Date, start: Date, endInclusive: Date) => {
const diff = differenceInMilliseconds(endInclusive, start);
@@ -1,4 +1,4 @@
import { differenceInDays } from "date-fns/differenceInDays";
import { differenceInDays } from "date-fns";
import type { PeriodProps } from "../Timeline.types";
import type { Spatial } from "./types.internal";
@@ -1,4 +1,4 @@
import { format } from "date-fns/format";
import { format } from "date-fns";
import { cl } from "../../utils/helpers";
import type { TFunction } from "../../utils/i18n/i18n.types";
import type { PeriodProps } from "../Timeline.types";
@@ -1,4 +1,4 @@
import { differenceInMilliseconds } from "date-fns/differenceInMilliseconds";
import { differenceInMilliseconds } from "date-fns";
import type { Period, PositionedPeriod } from "./types.internal";
const lastDate = (a: Date, b: Date): number => differenceInMilliseconds(a, b);
@@ -1,9 +1,11 @@
import { addMonths } from "date-fns/addMonths";
import { addYears } from "date-fns/addYears";
import { format } from "date-fns/format";
import { isSameDay } from "date-fns/isSameDay";
import { subMonths } from "date-fns/subMonths";
import { subYears } from "date-fns/subYears";
import {
addMonths,
addYears,
format,
isSameDay,
subMonths,
subYears,
} from "date-fns";
import React, { forwardRef } from "react";
import { Detail } from "../../typography/Detail";
import { useI18n } from "../../utils/i18n/i18n.hooks";
@@ -8,9 +8,6 @@ interface ListboxInputSlotProps {
virtuallyFocusedOptionId: string;
}
/**
* Remember that role=combobox requires aria-expanded
*/
const ListboxInputSlot = ({
children,
virtuallyFocusedOptionId,
@@ -1,6 +1,4 @@
import { enGB } from "date-fns/locale/en-GB";
import { nb } from "date-fns/locale/nb";
import { nn } from "date-fns/locale/nn";
import { enGB, nb, nn } from "date-fns/locale";
import { createStrictContext } from "../../helpers";
import type { TFunction } from "../../i18n/i18n.types";
import en_translations from "../../i18n/locales/en";
@@ -1,9 +1,11 @@
import { addYears } from "date-fns/addYears";
import { endOfMonth } from "date-fns/endOfMonth";
import { endOfYear } from "date-fns/endOfYear";
import { startOfDay } from "date-fns/startOfDay";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
import {
addYears,
endOfMonth,
endOfYear,
startOfDay,
startOfMonth,
startOfYear,
} from "date-fns";
import { describe, expect, test } from "vitest";
import { calendarRange } from "./calendar-range";
@@ -1,9 +1,11 @@
import { addYears } from "date-fns/addYears";
import { endOfMonth } from "date-fns/endOfMonth";
import { endOfYear } from "date-fns/endOfYear";
import { startOfDay } from "date-fns/startOfDay";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
import {
addYears,
endOfMonth,
endOfYear,
startOfDay,
startOfMonth,
startOfYear,
} from "date-fns";
/**
* Generates the min and max-dates possible to show and navigate to in the calendar.
@@ -1,4 +1,4 @@
import { setYear } from "date-fns/setYear";
import { setYear } from "date-fns";
import { describe, expect, test } from "vitest";
import { dateIsInCurrentMonth, isValidDate } from "./check-dates";
@@ -1,7 +1,9 @@
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
import { isThisMonth } from "date-fns/isThisMonth";
import { setYear } from "date-fns/setYear";
import { startOfDay } from "date-fns/startOfDay";
import {
differenceInCalendarDays,
isThisMonth,
setYear,
startOfDay,
} from "date-fns";
export const dateIsInCurrentMonth = (
date: Date,
@@ -1,5 +1,4 @@
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
import { startOfMonth, startOfYear } from "date-fns";
import { describe, expect, test } from "vitest";
import { isDateOutsideRange } from "./check-dates";
import { clampDisplayMonth, clampDisplayYear } from "./clamp-dates";
@@ -1,7 +1,4 @@
import { isAfter } from "date-fns/isAfter";
import { isBefore } from "date-fns/isBefore";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
import { isAfter, isBefore, startOfMonth, startOfYear } from "date-fns";
/**
* Makes sure the month is within the min and max daterange to avoid showing disabled months
@@ -1,4 +1,4 @@
import { isSameDay } from "date-fns/isSameDay";
import { isSameDay } from "date-fns";
import { isDateRange } from "../components/date/Date.typeutils";
export const disableDate = (
@@ -1,4 +1,4 @@
import { nb } from "date-fns/locale/nb";
import { nb } from "date-fns/locale";
import { describe, expect, test } from "vitest";
import { getMonthOptions, getYearOptions } from "./dropdown-options";
@@ -1,14 +1,16 @@
import type { Locale } from "date-fns";
import { addYears } from "date-fns/addYears";
import { eachMonthOfInterval } from "date-fns/eachMonthOfInterval";
import { endOfYear } from "date-fns/endOfYear";
import { format } from "date-fns/format";
import { getMonth } from "date-fns/getMonth";
import { getYear } from "date-fns/getYear";
import { isBefore } from "date-fns/isBefore";
import { isSameYear } from "date-fns/isSameYear";
import { startOfMonth } from "date-fns/startOfMonth";
import { startOfYear } from "date-fns/startOfYear";
import {
type Locale,
addYears,
eachMonthOfInterval,
endOfYear,
format,
getMonth,
getYear,
isBefore,
isSameYear,
startOfMonth,
startOfYear,
} from "date-fns";
/** Return the months to show in the dropdown. */
export function getMonthOptions({
@@ -1,5 +1,4 @@
import type { Locale } from "date-fns";
import { format } from "date-fns/format";
import { type Locale, format } from "date-fns";
import {
INPUT_DATE_STRING_FORMAT_DATE,
INPUT_DATE_STRING_FORMAT_MONTH,
@@ -1,4 +1,4 @@
import { nb } from "date-fns/locale/nb";
import { nb } from "date-fns/locale";
import { describe, expect, test } from "vitest";
import { formatDateForInput } from "./format-date";
import { parseDate } from "./parse-date";
@@ -1,7 +1,9 @@
import { differenceInCalendarDays } from "date-fns/differenceInCalendarDays";
import { isDate } from "date-fns/isDate";
import { isSameDay } from "date-fns/isSameDay";
import { isSameMonth } from "date-fns/isSameMonth";
import {
differenceInCalendarDays,
isDate,
isSameDay,
isSameMonth,
} from "date-fns";
import {
type DateRange,
type Matcher,
@@ -1,7 +1,4 @@
import type { Locale } from "date-fns";
import { isBefore } from "date-fns/isBefore";
import { parse } from "date-fns/parse";
import { sub } from "date-fns/sub";
import { type Locale, isBefore, parse, sub } from "date-fns";
import { isValidDate } from "./check-dates";
export const INPUT_DATE_STRING_FORMAT_DATE = "dd.MM.yyyy";
@@ -1,5 +1,5 @@
import { getMonth } from "date-fns/getMonth";
import { nb } from "date-fns/locale/nb";
import { getMonth } from "date-fns";
import { nb } from "date-fns/locale";
import { describe, expect, test } from "vitest";
import { isValidDate } from "./check-dates";
import { parseDate } from "./parse-date";
@@ -1,5 +1,4 @@
import { isBefore } from "date-fns/isBefore";
import { isSameDay } from "date-fns/isSameDay";
import { isBefore, isSameDay } from "date-fns";
import type { DateRange } from "../components/date/Date.typeutils";
/**
@@ -1,6 +1,6 @@
import { renderHook } from "@testing-library/react";
import React from "react";
import { describe, test } from "vitest";
import { bench, describe } from "vitest";
import { Provider } from "../../provider";
import { get } from "./get";
import { useI18n } from "./i18n.hooks";
@@ -8,16 +8,14 @@ import type { ComponentTranslation } from "./i18n.types";
import { nb } from "./locales";
describe("translate function throughput", () => {
test("translation lookups", async ({ bench }) => {
const { result } = renderHook(() => useI18n("FileUpload"));
const { result } = renderHook(() => useI18n("FileUpload"));
await bench("simple key lookup (no replacements)", () => {
result.current("item.uploading");
}).run();
bench("simple key lookup (no replacements)", () => {
result.current("item.uploading");
});
await bench("key lookup with replacements", () => {
result.current("item.uploading", { name: "Test", count: 5 } as never);
}).run();
bench("key lookup with replacements", () => {
result.current("item.uploading", { name: "Test", count: 5 } as never);
});
});
@@ -25,13 +23,10 @@ describe("translate with local overrides", () => {
const local = {
item: { uploading: "Hello {name}, you have {count} files" },
};
const { result } = renderHook(() => useI18n("FileUpload", local));
test("local override lookup", async ({ bench }) => {
const { result } = renderHook(() => useI18n("FileUpload", local));
await bench("local override with replacements", () => {
result.current("item.uploading", { name: "Test", count: 5 });
}).run();
bench("local override with replacements", () => {
result.current("item.uploading", { name: "Test", count: 5 });
});
});
@@ -43,21 +38,18 @@ describe("translate with multiple context objects", () => {
{children}
</Provider>
);
const { result } = renderHook(() => useI18n("FileUpload"), { wrapper });
test("context lookups", async ({ bench }) => {
const { result } = renderHook(() => useI18n("FileUpload"), { wrapper });
bench("hit in first context object", () => {
result.current("dropzone.button");
});
await bench("hit in first context object", () => {
result.current("dropzone.button");
}).run();
bench("hit in second context object", () => {
result.current("item.uploading");
});
await bench("hit in second context object", () => {
result.current("item.uploading");
}).run();
await bench("fallback to locale default", () => {
result.current("item.downloading");
}).run();
bench("fallback to locale default", () => {
result.current("item.downloading");
});
});
@@ -69,21 +61,19 @@ describe("get() function throughput", () => {
nb.FileUpload,
];
test("translation lookup", async ({ bench }) => {
await bench("shallow key hit in first object", () => {
get("item.uploading", objects);
}).run();
bench("shallow key hit in first object", () => {
get("item.uploading", objects);
});
await bench("key hit in second object (skip first)", () => {
get("item.downloading", objects);
}).run();
bench("key hit in second object (skip first)", () => {
get("item.downloading", objects);
});
await bench("deep fallback to last object", () => {
get("dropzone.button", objects);
}).run();
bench("deep fallback to last object", () => {
get("dropzone.button", objects);
});
await bench("long key dot notation parsing", () => {
get("dropzone.dragAndDropMultiple", [nb.FileUpload]);
}).run();
bench("long key dot notation parsing", () => {
get("dropzone.dragAndDropMultiple", [nb.FileUpload]);
});
});
Loaded 100 of 122 files, more files were not shown because too many files have changed in this diff. Show more