mirror of
https://github.com/navikt/aksel.git
synced 2026-10-13 09:12:03 +00:00
Compare commits
112
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6324d63745 | ||
|
|
debfb3fd15 | ||
|
|
a3e8457baf | ||
|
|
0560f84acd | ||
|
|
663dfbe8a4 | ||
|
|
536f088b97 | ||
|
|
f659599787 | ||
|
|
ed34bc034a | ||
|
|
2a2028fd95 | ||
|
|
386145d63a | ||
|
|
1ed809c872 | ||
|
|
e2359ddf33 | ||
|
|
069e3346ec | ||
|
|
c941af9de5 | ||
|
|
a273a22172 | ||
|
|
0e94031f1e | ||
|
|
64a9762286 | ||
|
|
a05dbadfa4 | ||
|
|
7c468b18d1 | ||
|
|
cfcd78a450 | ||
|
|
9e677697a7 | ||
|
|
8a8bf0a274 | ||
|
|
1e40d96510 | ||
|
|
db56141b2e | ||
|
|
ec351ba27f | ||
|
|
2e98a09540 | ||
|
|
2d438e1d37 | ||
|
|
8b373e8b80 | ||
|
|
179eb51221 | ||
|
|
d98624618c | ||
|
|
ecc501a0a8 | ||
|
|
fec3b12d5f | ||
|
|
8f99d3a102 | ||
|
|
7fa7719c24 | ||
|
|
bc1b9fdad9 | ||
|
|
755d75903a | ||
|
|
5ebc71470a | ||
|
|
17daddab86 | ||
|
|
20ced9b084 | ||
|
|
6d2115cdc9 | ||
|
|
17ae19c5fe | ||
|
|
a60298f232 | ||
|
|
5866992e7d | ||
|
|
8ab241a980 | ||
|
|
363d76b9b2 | ||
|
|
5da970fd62 | ||
|
|
296e43cab2 | ||
|
|
85fec974dc | ||
|
|
6ddea4eaf2 | ||
|
|
acea9ebb78 | ||
|
|
fdbd634012 | ||
|
|
dcf5452d28 | ||
|
|
0a8c33bbbe | ||
|
|
18b0cf5fa5 | ||
|
|
374a679bc3 | ||
|
|
a2e020d4f3 | ||
|
|
074c8da536 | ||
|
|
bdbd6c24cc | ||
|
|
f0e6c944d8 | ||
|
|
22be148832 | ||
|
|
aac12fe44f | ||
|
|
324358d086 | ||
|
|
946cbeb016 | ||
|
|
f4d22dae68 | ||
|
|
ce2f5a9b90 | ||
|
|
cf167064eb | ||
|
|
73a6ae1bf1 | ||
|
|
95c84e1d01 | ||
|
|
4675857e8f | ||
|
|
2a6e416b5c | ||
|
|
d3c3cafc97 | ||
|
|
678fa6692b | ||
|
|
87aeb94c64 | ||
|
|
4714a5ff32 | ||
|
|
ca1d9be88f | ||
|
|
2654da8b64 | ||
|
|
8d6418a594 | ||
|
|
8269091fb8 | ||
|
|
fd82184057 | ||
|
|
39f2cb8614 | ||
|
|
4bf298f363 | ||
|
|
3a239e4e17 | ||
|
|
4fcfaf3deb | ||
|
|
5dbc66daaa | ||
|
|
555a46b5da | ||
|
|
73b2bca156 | ||
|
|
01411dc11f | ||
|
|
b1b0d23863 | ||
|
|
cf59ce81bf | ||
|
|
00c0ff31e1 | ||
|
|
d278060e59 | ||
|
|
e5d5b67262 | ||
|
|
246f25cbaa | ||
|
|
b2a22438ad | ||
|
|
8b8b724e40 | ||
|
|
9f9bdcc645 | ||
|
|
82bdf8075a | ||
|
|
410e543825 | ||
|
|
40308e0e77 | ||
|
|
fff309b573 | ||
|
|
fe7c2de8fa | ||
|
|
6fedea0ba4 | ||
|
|
9811327509 | ||
|
|
cf2034f914 | ||
|
|
9e5e7f8fdc | ||
|
|
e9dc171d74 | ||
|
|
544b80eeae | ||
|
|
4358b53095 | ||
|
|
de57099b09 | ||
|
|
5a97cb422d | ||
|
|
67fb3bb3a1 | ||
|
|
7bdd5cb748 |
No files matched your search
+27
-12
@@ -1,4 +1,11 @@
|
||||
// TODO: Convert to new format: https://eslint.org/docs/latest/use/configure/migration-guide
|
||||
/**
|
||||
* TODO:
|
||||
* - Convert to new format (v9): https://eslint.org/docs/latest/use/configure/migration-guide
|
||||
* - Look into opinionated stylistic plugin
|
||||
* - Replace @typescript-eslint/recommended with @typescript-eslint/recommended-type-checked
|
||||
* - We should add "plugin:react/jsx-runtime", but not sure if this will cause problems for projects not using the new JSX transform
|
||||
* - Consider adding the rule "id-length"
|
||||
*/
|
||||
module.exports = {
|
||||
env: {
|
||||
browser: true,
|
||||
@@ -8,15 +15,13 @@ module.exports = {
|
||||
extends: [
|
||||
"eslint:recommended",
|
||||
"plugin:react/recommended",
|
||||
//"plugin:react/jsx-runtime", // Not sure if this will cause problems for projects not using the new JSX transform
|
||||
"plugin:react-hooks/recommended",
|
||||
"plugin:jsx-a11y/recommended",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript",
|
||||
"plugin:storybook/recommended",
|
||||
// TODO: Look into opinionated stylistic plugin
|
||||
// TODO: Replace @typescript-eslint/recommended with @typescript-eslint/recommended-type-checked
|
||||
],
|
||||
/* Allows eslint-plugin-react to detect installed react-version */
|
||||
settings: { react: { version: "detect" } },
|
||||
rules: {
|
||||
"import/no-unresolved": "off",
|
||||
@@ -26,12 +31,6 @@ module.exports = {
|
||||
],
|
||||
"array-callback-return": "error",
|
||||
"object-shorthand": "error",
|
||||
// TODO: Consider adding the rule "id-length"
|
||||
// Temporary:
|
||||
"react/prop-types": "off",
|
||||
"react/display-name": "off",
|
||||
"import/no-named-as-default": "off",
|
||||
// "import/no-cycle": [1],
|
||||
"no-else-return": "error",
|
||||
"no-console": [
|
||||
"warn",
|
||||
@@ -53,7 +52,12 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
/* Temporarily turned off until code is updated */
|
||||
"react/prop-types": "off",
|
||||
"react/display-name": "off",
|
||||
"import/no-named-as-default": "off",
|
||||
},
|
||||
/* https://eslint.org/docs/latest/use/configure/rules#report-unused-eslint-disable-comments */
|
||||
reportUnusedDisableDirectives: true,
|
||||
overrides: [
|
||||
{
|
||||
@@ -66,17 +70,27 @@ module.exports = {
|
||||
files: ["**/*.ts?(x)"],
|
||||
extends: ["plugin:@typescript-eslint/recommended"],
|
||||
rules: {
|
||||
"@typescript-eslint/no-shadow": ["error", { hoist: "all" }], // TODO: Consider { builtinGlobals: true }
|
||||
/*
|
||||
* TODO:
|
||||
* - Consider { builtinGlobals: true }
|
||||
*/
|
||||
"@typescript-eslint/no-shadow": ["error", { hoist: "all" }],
|
||||
"@typescript-eslint/no-explicit-any": "off", // Temporary
|
||||
"@typescript-eslint/array-type": "error",
|
||||
"@typescript-eslint/no-unused-expressions": [
|
||||
"error",
|
||||
/* https://eslint.org/docs/latest/rules/no-unused-expressions#allowshortcircuit-and-allowternary */
|
||||
{ allowShortCircuit: true, allowTernary: true },
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ["**/*.test.*", "**/__tests__/*"],
|
||||
plugins: ["@vitest"],
|
||||
extends: [
|
||||
"plugin:@vitest/legacy-recommended",
|
||||
"plugin:testing-library/react",
|
||||
/* Needed to support v8 eslint config-syntax */
|
||||
"plugin:@vitest/legacy-recommended",
|
||||
],
|
||||
},
|
||||
|
||||
@@ -156,5 +170,6 @@ module.exports = {
|
||||
"**/codemod/**/*.js",
|
||||
"!.storybook",
|
||||
"**/playwright-report/**",
|
||||
"**/tokens/**/plugin.js",
|
||||
],
|
||||
};
|
||||
@@ -34,6 +34,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Create empty .env file (Can be removed when env-file-if-exists is implemented in node v22)
|
||||
run: echo "" > ./aksel.nav.no/website/.env
|
||||
|
||||
- name: Build application
|
||||
env:
|
||||
SANITY_READ: ${{ secrets.SANITY_READ }}
|
||||
|
||||
@@ -32,6 +32,9 @@ jobs:
|
||||
yarn boot
|
||||
yarn docgen
|
||||
|
||||
- name: Create empty .env file (Can be removed when env-file-if-exists is implemented in node v22)
|
||||
run: echo "" > ./aksel.nav.no/website/.env
|
||||
|
||||
- name: Update sanity sync
|
||||
env:
|
||||
SANITY_WRITE: ${{ secrets.SANITY_WRITE }}
|
||||
@@ -68,6 +71,9 @@ jobs:
|
||||
- name: Tests
|
||||
run: yarn workspace website test
|
||||
|
||||
- name: Create empty .env file (Can be removed when env-file-if-exists is implemented in node v22)
|
||||
run: echo "" > ./aksel.nav.no/website/.env
|
||||
|
||||
- name: Build application
|
||||
env:
|
||||
SANITY_READ: ${{ secrets.SANITY_READ }}
|
||||
|
||||
@@ -23,6 +23,9 @@ jobs:
|
||||
- name: Install dependencies
|
||||
run: cd aksel.nav.no/website && yarn
|
||||
|
||||
- name: Create empty .env file (Can be removed when env-file-if-exists is implemented in node v22)
|
||||
run: echo "" > ./aksel.nav.no/website/.env
|
||||
|
||||
- name: Export dataset
|
||||
env:
|
||||
SANITY_READ: ${{ secrets.SANITY_READ }}
|
||||
|
||||
@@ -13,7 +13,7 @@ jobs:
|
||||
playwright:
|
||||
runs-on: ubuntu-latest
|
||||
container:
|
||||
image: mcr.microsoft.com/playwright:v1.35.1-jammy
|
||||
image: mcr.microsoft.com/playwright:v1.48.1-jammy
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
@@ -31,6 +31,9 @@ jobs:
|
||||
run: |
|
||||
yarn boot
|
||||
|
||||
- name: Create empty .env file (Can be removed when env-file-if-exists is implemented in node v22)
|
||||
run: echo "" > ./aksel.nav.no/website/.env
|
||||
|
||||
- name: Build application
|
||||
env:
|
||||
SANITY_READ: ${{ secrets.SANITY_READ }}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
name: "Update remote token config for figma variables plugin"
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
push:
|
||||
branches:
|
||||
- "main"
|
||||
paths:
|
||||
- "@navikt/core/tokens/**"
|
||||
|
||||
jobs:
|
||||
upload:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
contents: "read"
|
||||
id-token: "write"
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: yarn
|
||||
- name: set-env for yarn install to @navikt scope
|
||||
shell: bash
|
||||
run: echo "NPM_AUTH_TOKEN=${{ secrets.READER_TOKEN }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Install dependencies
|
||||
run: yarn
|
||||
|
||||
- name: Build tokens
|
||||
run: yarn workspace @navikt/ds-tokens build
|
||||
|
||||
- id: upload
|
||||
uses: nais/deploy/actions/cdn-upload/v2@master
|
||||
with:
|
||||
team: designsystem
|
||||
source: "./@navikt/core/tokens/figma-config.json"
|
||||
destination: "./@navikt/tokens"
|
||||
project_id: ${{ vars.NAIS_MANAGEMENT_PROJECT_ID }}
|
||||
identity_provider: ${{ secrets.NAIS_WORKLOAD_IDENTITY_PROVIDER }}
|
||||
source_keep_parent_name: false
|
||||
no_cache_paths: "@navikt/tokens/figma-config.json"
|
||||
|
||||
- run: echo uploaded file ${{ steps.upload.outputs.uploaded }}
|
||||
shell: bash
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
NPM_AUTH_TOKEN: ${{ secrets.READER_TOKEN }}
|
||||
- run: yarn boot
|
||||
- run: cd ./examples/referansesider && yarn && yarn build
|
||||
- uses: navikt/frontend/actions/spa-deploy/v2@main
|
||||
- uses: nais/deploy/actions/spa-deploy/v2@master
|
||||
with:
|
||||
app: aksel-referansesider
|
||||
team: designsystem
|
||||
|
||||
@@ -5,24 +5,11 @@ jobs:
|
||||
execute:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
packages: "write"
|
||||
contents: "read"
|
||||
id-token: "write"
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 1
|
||||
- uses: denoland/setup-deno@v1
|
||||
with:
|
||||
deno-version: v1.x
|
||||
- name: Restore cache
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: "**/node_modules"
|
||||
key: ${{ runner.os }}-build-${{ github.sha }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-build-
|
||||
|
||||
- uses: nais/docker-build-push@v0
|
||||
id: docker-push
|
||||
with:
|
||||
|
||||
@@ -136,34 +136,34 @@ import Setup from "./assets/setup.png";
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
grid-gap: var(--a-spacing-6);
|
||||
grid-gap: 1.5rem;
|
||||
}
|
||||
|
||||
.sb-link{
|
||||
text-decoration: underline;
|
||||
color: var(--a-text-default);
|
||||
color: #111;
|
||||
}
|
||||
|
||||
img {
|
||||
object-fit: cover;
|
||||
padding-bottom: var(--a-spacing-5);
|
||||
padding-bottom: 1.25rem;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sb-section-title {
|
||||
margin-bottom: var(--a-spacing-8);
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
.sb-section-item-heading {
|
||||
padding-bottom: var(--a-spacing-1);
|
||||
padding-bottom: 0.25rem
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.sb-section-item-paragraph {
|
||||
margin: 0;
|
||||
padding-bottom: var(--a-spacing-3);
|
||||
padding-bottom: 0.75rem;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
|
||||
@@ -53,6 +53,7 @@ export default {
|
||||
"@storybook/addon-a11y",
|
||||
"@whitespace/storybook-addon-html",
|
||||
"@storybook/addon-interactions",
|
||||
"@storybook/addon-themes",
|
||||
{
|
||||
name: "@storybook/addon-storysource",
|
||||
options: {
|
||||
|
||||
+51
-30
@@ -1,5 +1,7 @@
|
||||
import React from "react";
|
||||
import "@navikt/ds-css/index.css";
|
||||
import { withThemeByClassName } from "@storybook/addon-themes";
|
||||
import React, { useLayoutEffect } from "react";
|
||||
import darksideCss from "@navikt/ds-css/darkside/index.css?inline";
|
||||
import defaultCss from "@navikt/ds-css/index.css?inline";
|
||||
import "./layout.css";
|
||||
|
||||
export const parameters = {
|
||||
@@ -16,44 +18,63 @@ export const parameters = {
|
||||
},
|
||||
},
|
||||
layout: "centered",
|
||||
backgrounds: {
|
||||
values: [
|
||||
{
|
||||
name: "Canvas",
|
||||
value: "#ffffff",
|
||||
},
|
||||
{
|
||||
name: "Darkmode",
|
||||
value: "#23262a",
|
||||
},
|
||||
],
|
||||
},
|
||||
backgrounds: { disable: true },
|
||||
};
|
||||
|
||||
export const globalTypes = {
|
||||
theme: {
|
||||
name: "Theme",
|
||||
mode: {
|
||||
name: "Darkside",
|
||||
toolbar: {
|
||||
icon: "circlehollow",
|
||||
items: [
|
||||
{ value: "light", icon: "circlehollow", title: "light" },
|
||||
{ value: "dark", icon: "circle", title: "dark" },
|
||||
{
|
||||
value: "default",
|
||||
icon: "hourglass",
|
||||
title: "Default (old)",
|
||||
},
|
||||
{ value: "darkside", icon: "beaker", title: "Darkside (new)" },
|
||||
],
|
||||
showName: true,
|
||||
dynamicTitle: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const withTheme = (StoryFn, context) => {
|
||||
return (
|
||||
<div
|
||||
data-theme={context.parameters.theme || context.globals.theme || "light"}
|
||||
lang="no"
|
||||
id="root"
|
||||
>
|
||||
<StoryFn />
|
||||
</div>
|
||||
);
|
||||
export const initialGlobals = {
|
||||
mode: "default",
|
||||
};
|
||||
|
||||
export const decorators = [withTheme];
|
||||
const ModeDecorator = ({ children, mode }) => {
|
||||
useLayoutEffect(() => {
|
||||
const style = document.createElement("style");
|
||||
style.innerHTML = mode === "darkside" ? darksideCss : defaultCss;
|
||||
document.head.appendChild(style);
|
||||
|
||||
if (mode === "darkside") {
|
||||
document.body.style.setProperty("background", "var(--a-bg-default)");
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.head.removeChild(style);
|
||||
document.body.style.removeProperty("background");
|
||||
};
|
||||
}, [mode]);
|
||||
|
||||
return children;
|
||||
};
|
||||
|
||||
export const decorators = [
|
||||
withThemeByClassName({
|
||||
themes: {
|
||||
light: "light",
|
||||
dark: "dark",
|
||||
},
|
||||
defaultTheme: "light",
|
||||
}),
|
||||
(StoryFn, context) => {
|
||||
return (
|
||||
<ModeDecorator mode={context.globals.mode}>
|
||||
<StoryFn />
|
||||
</ModeDecorator>
|
||||
);
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,15 @@
|
||||
# @navikt/aksel-icons
|
||||
|
||||
## 7.3.0
|
||||
|
||||
## 7.2.1
|
||||
|
||||
## 7.2.0
|
||||
|
||||
## 7.1.2
|
||||
|
||||
## 7.1.1
|
||||
|
||||
## 7.1.0
|
||||
|
||||
## 7.0.1
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { splitIntoParts } from "../config/figma/fetch-icons";
|
||||
|
||||
describe("splitIntoParts", () => {
|
||||
test("should split array into parts correctly with odd count array", () => {
|
||||
const arr = ["a", "b", "c", "d", "e"];
|
||||
const result = splitIntoParts(arr, 4);
|
||||
expect(result).toEqual(["a,b", "c,d", "e"]);
|
||||
});
|
||||
|
||||
test("should split array into parts correctly with even count array", () => {
|
||||
const arr = ["a", "b", "c", "d", "e", "f"];
|
||||
const result = splitIntoParts(arr, 4);
|
||||
expect(result).toEqual(["a,b", "c,d", "e,f"]);
|
||||
});
|
||||
});
|
||||
@@ -1,9 +1,9 @@
|
||||
/* eslint-disable import/no-named-as-default-member */
|
||||
import fastglob from "fast-glob";
|
||||
import fs from "fs";
|
||||
import jsYaml from "js-yaml";
|
||||
import { JSON_SCHEMA, load } from "js-yaml";
|
||||
import path from "path";
|
||||
import { describe, expect, test } from "vitest";
|
||||
import { IconYml } from "../config/figma/make-configs";
|
||||
|
||||
const basePath = path.resolve(__dirname, "../icons");
|
||||
|
||||
@@ -14,9 +14,10 @@ const ymlList = fastglob
|
||||
describe(`Each icons YML-config is valid`, () => {
|
||||
ymlList.forEach((file) => {
|
||||
test(`${file} has valid YML-config`, () => {
|
||||
const ymlData = jsYaml.load(fs.readFileSync(`${basePath}/${file}`), {
|
||||
schema: jsYaml.JSON_SCHEMA,
|
||||
});
|
||||
const ymlData = load(fs.readFileSync(`${basePath}/${file}`).toString(), {
|
||||
schema: JSON_SCHEMA,
|
||||
}) as IconYml;
|
||||
|
||||
expect(ymlData.name).toBeTruthy();
|
||||
expect(ymlData.category).toBeTruthy();
|
||||
expect(ymlData.sub_category).toBeTruthy();
|
||||
@@ -31,7 +32,7 @@ describe(`Each icons YML-config is valid`, () => {
|
||||
});
|
||||
});
|
||||
|
||||
function isDate(dateStr) {
|
||||
function isDate(dateStr: string) {
|
||||
return !Number.isNaN(
|
||||
new Date(dateStr.split(".").reverse().join(".")).getDate(),
|
||||
);
|
||||
|
||||
@@ -21,8 +21,7 @@ describe(`Each icons has a valid code`, () => {
|
||||
|
||||
test(`has valid attributes on root-node`, () => {
|
||||
const root = select(":root", iconAst);
|
||||
const properties =
|
||||
root && root.properties ? Object.keys(root.properties).sort() : [];
|
||||
const properties = Object.keys(root?.properties ?? []).sort();
|
||||
|
||||
expect(properties).toStrictEqual(
|
||||
["viewBox", "xmlns", "height", "width", "fill"].sort(),
|
||||
@@ -31,45 +30,41 @@ describe(`Each icons has a valid code`, () => {
|
||||
|
||||
test(`has valid xml-attr`, () => {
|
||||
const root = select(":root", iconAst);
|
||||
const xmlns = root && root.properties ? root.properties.xmlns : null;
|
||||
expect(xmlns).toBe("http://www.w3.org/2000/svg");
|
||||
expect(root?.properties?.xmlns).toBe("http://www.w3.org/2000/svg");
|
||||
});
|
||||
|
||||
test(`has valid viewbox`, () => {
|
||||
const root = select(":root", iconAst);
|
||||
const viewbox =
|
||||
root && root.properties ? root.properties.viewBox : null;
|
||||
expect(viewbox).toBe("0 0 24 24");
|
||||
expect(root?.properties?.viewBox).toBe("0 0 24 24");
|
||||
});
|
||||
|
||||
test(`root fill is none`, () => {
|
||||
const root = select(":root", iconAst);
|
||||
const fill = root && root.properties ? root.properties.fill : null;
|
||||
expect(fill).toBe("none");
|
||||
expect(root?.properties?.fill).toBe("none");
|
||||
});
|
||||
|
||||
test(`has valid width and height`, () => {
|
||||
const root = select(":root", iconAst);
|
||||
const width = root && root.properties ? root.properties.width : null;
|
||||
const height = root && root.properties ? root.properties.height : null;
|
||||
expect(width).toBe("24");
|
||||
expect(height).toBe("24");
|
||||
expect(root?.properties?.width).toBe("24");
|
||||
expect(root?.properties?.height).toBe("24");
|
||||
});
|
||||
|
||||
test(`has valid stroke`, () => {
|
||||
const nodes = selectAll("*", iconAst);
|
||||
|
||||
nodes.forEach((n) => {
|
||||
n.properties?.stroke &&
|
||||
if (n.properties?.stroke) {
|
||||
expect(n.properties.stroke).toEqual("#23262A");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
test(`has valid stroke-width`, () => {
|
||||
const nodes = selectAll("*", iconAst);
|
||||
nodes.forEach((n) => {
|
||||
n.properties?.strokeWidth &&
|
||||
if (n.properties?.strokeWidth) {
|
||||
expect(n.properties?.strokeWidth).toEqual("1.5");
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
const file_key = "wEdyFjCQSBR3U7FvrMbPXa";
|
||||
|
||||
export const fetchIcons = async () => {
|
||||
const data = await fetch(
|
||||
`https://api.figma.com/v1/files/${file_key}/components`,
|
||||
{
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"X-FIGMA-TOKEN": process.env.FIGMA_TOKEN,
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.catch((e) => {
|
||||
throw e.message;
|
||||
});
|
||||
|
||||
if (!data?.meta?.components || data?.meta?.components.length === 0) {
|
||||
throw new Error("Fant ingen publiserte ikoner");
|
||||
}
|
||||
|
||||
return data?.meta?.components;
|
||||
};
|
||||
|
||||
export const fetchDownloadUrls = async (idArray) => {
|
||||
const urls = { images: null };
|
||||
|
||||
for (let ids of idArray) {
|
||||
await fetch(
|
||||
`https://api.figma.com/v1/images/${file_key}/?ids=${ids}&format=svg`,
|
||||
{
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"X-FIGMA-TOKEN": process.env.FIGMA_TOKEN,
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.then((x) => {
|
||||
urls.images = {
|
||||
...urls.images,
|
||||
...x.images,
|
||||
};
|
||||
})
|
||||
.catch((e) => {
|
||||
throw e.message;
|
||||
});
|
||||
}
|
||||
return urls;
|
||||
};
|
||||
|
||||
export const fetchIcon = async (url) => {
|
||||
return await fetch(url)
|
||||
.then((x) => x.text())
|
||||
.catch((e) => {
|
||||
throw e.message;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
import {
|
||||
BadRequestErrorResponseWithErrMessage,
|
||||
ErrorResponsePayloadWithErrMessage,
|
||||
GetFileComponentsResponse,
|
||||
GetImagesResponse,
|
||||
PublishedComponent,
|
||||
} from "@figma/rest-api-spec";
|
||||
|
||||
/* https://www.figma.com/developers/api#library-items */
|
||||
const FIGMA_FILE_KEY = "wEdyFjCQSBR3U7FvrMbPXa";
|
||||
|
||||
export const fetchIcons = async (): Promise<PublishedComponent[]> => {
|
||||
console.group("Fetching list of published icons from Figma...");
|
||||
const data:
|
||||
| GetFileComponentsResponse
|
||||
| BadRequestErrorResponseWithErrMessage = await fetch(
|
||||
`https://api.figma.com/v1/files/${FIGMA_FILE_KEY}/components`,
|
||||
{
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"X-FIGMA-TOKEN": process.env.FIGMA_TOKEN ?? "",
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.catch((e) => {
|
||||
throw e.message;
|
||||
});
|
||||
|
||||
if (data.status !== 200) {
|
||||
throw new Error(data.err);
|
||||
}
|
||||
|
||||
if (data.meta.components.length === 0) {
|
||||
throw new Error("No published icons found");
|
||||
}
|
||||
|
||||
/* Filtes out any 'random' icons not meant for the icon-library */
|
||||
const validIcons = data.meta.components.filter((icon) => {
|
||||
const validIcon =
|
||||
!!icon.containing_frame?.pageName && !!icon.containing_frame?.name;
|
||||
|
||||
if (!validIcon) {
|
||||
console.warn(`Ignoring icon '${icon.name}' as it is missing metadata`);
|
||||
}
|
||||
return validIcon;
|
||||
});
|
||||
|
||||
console.info(
|
||||
`Found ${validIcons.length} published icons in Figma. ${
|
||||
data.meta.components.length - validIcons.length
|
||||
} icons were ignored due to missing metadata.`,
|
||||
);
|
||||
|
||||
console.groupEnd();
|
||||
|
||||
return validIcons;
|
||||
};
|
||||
|
||||
export const fetchDownloadUrls = async (
|
||||
nodeIds: string[],
|
||||
): Promise<Record<string, string>> => {
|
||||
/* Would just using Record be better here? Yes. But is a self-referencing return kinda cool? Also yes */
|
||||
const idUrlPairs: Awaited<ReturnType<typeof fetchDownloadUrls>> = {};
|
||||
|
||||
/* Figma accepts a comma-separated list of ids: GET /v1/images/:key?ids=1:2,1:3,1:4 */
|
||||
const paginatedNodeIds = splitIntoParts(nodeIds, 5);
|
||||
|
||||
for (const ids of paginatedNodeIds) {
|
||||
const data: GetImagesResponse | ErrorResponsePayloadWithErrMessage =
|
||||
await fetch(
|
||||
`https://api.figma.com/v1/images/${FIGMA_FILE_KEY}/?ids=${ids}&format=svg`,
|
||||
{
|
||||
headers: {
|
||||
"Content-type": "application/json",
|
||||
"X-FIGMA-TOKEN": process.env.FIGMA_TOKEN ?? "",
|
||||
},
|
||||
},
|
||||
)
|
||||
.then((x) => x.json())
|
||||
.catch((e) => {
|
||||
throw e.message;
|
||||
});
|
||||
|
||||
if (data.err !== null) {
|
||||
throw new Error(data.err);
|
||||
}
|
||||
|
||||
for (const [key, value] of Object.entries(data.images)) {
|
||||
if (!value) {
|
||||
console.warn(`No image found for ${key}`);
|
||||
continue;
|
||||
}
|
||||
idUrlPairs[key] = value;
|
||||
}
|
||||
}
|
||||
return idUrlPairs;
|
||||
};
|
||||
|
||||
/**
|
||||
* Splits an array of strings into parts where each part is a comma-separated string of all the strings for that part.
|
||||
* @returns An array of comma-separated strings.
|
||||
*/
|
||||
export function splitIntoParts(arr: string[], parts: number): string[] {
|
||||
const partSize = Math.ceil(arr.length / parts);
|
||||
const blocks: string[] = [];
|
||||
|
||||
for (let i = 0; i < parts && i * partSize < arr.length; i++) {
|
||||
const part = arr.slice(i * partSize, (i + 1) * partSize);
|
||||
blocks.push(part.join(","));
|
||||
}
|
||||
|
||||
return blocks;
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import camelCase from "lodash.camelCase";
|
||||
import startCase from "lodash.startcase";
|
||||
|
||||
export const resolveName = (icon) => {
|
||||
const isVariant = icon.name.startsWith("Variant=");
|
||||
|
||||
if (isVariant) {
|
||||
const variant = icon.name.replace("Variant=", "").replace("Stroke", "");
|
||||
const actualName = icon["containing_frame"]?.["containingStateGroup"]?.name;
|
||||
const name = `${actualName}${variant}`;
|
||||
return `${startCase(camelCase(name)).replace(/ /g, "")}.svg`;
|
||||
}
|
||||
|
||||
return `${startCase(camelCase(icon.name)).replace(/ /g, "")}.svg`;
|
||||
};
|
||||
@@ -0,0 +1,17 @@
|
||||
import { PublishedComponent } from "@figma/rest-api-spec";
|
||||
import { camelCase, startCase } from "lodash";
|
||||
|
||||
export const resolveName = (icon: PublishedComponent) => {
|
||||
const isVariant = icon.name.startsWith("Variant=");
|
||||
|
||||
if (isVariant) {
|
||||
const variant = icon.name.replace("Variant=", "").replace("Stroke", "");
|
||||
|
||||
/* @ts-expect-error Figma now includes the key:value pair `containingStateGroup: { name: string, nodeId: string }` */
|
||||
const actualName = icon.containing_frame?.containingStateGroup?.name;
|
||||
const name = `${actualName}${variant}`;
|
||||
return `${startCase(camelCase(name)).replace(/ /g, "")}.svg`;
|
||||
}
|
||||
|
||||
return `${startCase(camelCase(icon.name)).replace(/ /g, "")}.svg`;
|
||||
};
|
||||
@@ -1,65 +0,0 @@
|
||||
import { config } from "dotenv";
|
||||
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import { fetchDownloadUrls, fetchIcon, fetchIcons } from "./fetch-icons.mjs";
|
||||
import { resolveName } from "./icon-name.mjs";
|
||||
import { makeConfig } from "./make-configs.mjs";
|
||||
import { paginate } from "./paginate.mjs";
|
||||
|
||||
config();
|
||||
/* https://www.figma.com/file/wEdyFjCQSBR3U7FvrMbPXa/Core-Icons-Next?node-id=277%3A1221&t=mUJzFvnsceYYXNL5-0 */
|
||||
|
||||
const iconFolder = "./icons";
|
||||
|
||||
if (!process.env.FIGMA_TOKEN) {
|
||||
throw new Error("FIGMA_TOKEN not set in .env");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
let icons = await fetchIcons();
|
||||
|
||||
const totalIcons = icons.length;
|
||||
|
||||
const { images } = await fetchDownloadUrls(
|
||||
paginate(
|
||||
icons.map((x) => x.node_id),
|
||||
400,
|
||||
).map((x) => x.join(",")),
|
||||
);
|
||||
|
||||
if (existsSync(iconFolder)) {
|
||||
rmSync(iconFolder, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
mkdirSync(iconFolder);
|
||||
|
||||
console.info(`Laster ned ${Object.keys(images).length} ikoner`);
|
||||
|
||||
for (const [key, value] of Object.entries(images)) {
|
||||
const icon = await fetchIcon(value).catch((e) => {
|
||||
throw e;
|
||||
});
|
||||
await new Promise((r) => setTimeout(r, 20));
|
||||
|
||||
icon &&
|
||||
writeFileSync(
|
||||
resolve(iconFolder, resolveName(icons.find((x) => x.node_id === key))),
|
||||
icon,
|
||||
{
|
||||
encoding: "utf8",
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
makeConfig(icons, iconFolder);
|
||||
|
||||
readdirSync(iconFolder, (_, files) => {
|
||||
if (files.length * 2 !== totalIcons) {
|
||||
throw new Error(`Ikoner mangler!`);
|
||||
}
|
||||
});
|
||||
|
||||
console.info(`Success! ${Object.keys(images).length} ikoner lastet ned`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,106 @@
|
||||
import { existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import { fetchDownloadUrls, fetchIcons } from "./fetch-icons";
|
||||
import { resolveName } from "./icon-name";
|
||||
import { makeConfig } from "./make-configs";
|
||||
|
||||
/**
|
||||
* https://www.figma.com/file/wEdyFjCQSBR3U7FvrMbPXa/Core-Icons-Next?node-id=277%3A1221&t=mUJzFvnsceYYXNL5-0
|
||||
* This is the current source for all icons we are working with.
|
||||
*/
|
||||
|
||||
const iconFolder = "./icons";
|
||||
|
||||
if (!process.env.FIGMA_TOKEN) {
|
||||
throw new Error("FIGMA_TOKEN not set in .env");
|
||||
}
|
||||
|
||||
main();
|
||||
|
||||
async function main() {
|
||||
console.info("Started icon-update from Figma");
|
||||
/* Icons are published as 'components' in Figma */
|
||||
const publishedIconComponents = await fetchIcons();
|
||||
|
||||
/* When we have all the published icons, we can ask figma for URL's for downloading them as assets */
|
||||
const imagesUrls = await fetchDownloadUrls(
|
||||
publishedIconComponents.map((x) => x.node_id),
|
||||
);
|
||||
|
||||
/* Lets do a clean install */
|
||||
if (existsSync(iconFolder)) {
|
||||
rmSync(iconFolder, { recursive: true, force: true });
|
||||
}
|
||||
mkdirSync(iconFolder);
|
||||
|
||||
console.group(`Processing ${Object.keys(imagesUrls).length} icons...`);
|
||||
|
||||
const fileNames = new Set<string>();
|
||||
|
||||
await Promise.all(
|
||||
Object.entries(imagesUrls).map(async ([nodeId, iconUrl]) => {
|
||||
/* Each icon is now a raw string for a complete SVG */
|
||||
const iconSvg = await fetch(iconUrl)
|
||||
.then((x) => x.text())
|
||||
.catch((e) => {
|
||||
throw e.message;
|
||||
});
|
||||
|
||||
const matchingIcon = publishedIconComponents.find(
|
||||
(x) => x.node_id === nodeId,
|
||||
);
|
||||
|
||||
if (!matchingIcon) {
|
||||
throw new Error(
|
||||
`No matching icon found for ${nodeId}. It should not be possible to dowload icon without a matching icon in the list of icons fetched from Figma.`,
|
||||
);
|
||||
}
|
||||
|
||||
const fileName = resolveName(matchingIcon);
|
||||
|
||||
/*
|
||||
* In some cases if multiple icons are published in Figma with the same name,
|
||||
* we will end up overwriting the icon with the same name.
|
||||
*/
|
||||
if (fileNames.has(fileName)) {
|
||||
console.warn(`Duplicate name detected: ${fileName}.`);
|
||||
}
|
||||
|
||||
fileNames.add(fileName);
|
||||
|
||||
writeFileSync(resolve(iconFolder, resolveName(matchingIcon)), iconSvg, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
}),
|
||||
);
|
||||
|
||||
if (fileNames.size !== publishedIconComponents.length) {
|
||||
console.warn(
|
||||
`Duplicate icon names from Figma leads to them being overwritten. This will cause the icon-library to be out of sync with Figma.`,
|
||||
);
|
||||
}
|
||||
|
||||
console.info(`Completed processing 🎉`);
|
||||
console.groupEnd();
|
||||
|
||||
makeConfig(publishedIconComponents, iconFolder);
|
||||
|
||||
const filesInDir = readdirSync(iconFolder);
|
||||
const svgFiles = filesInDir.filter((x) => x.endsWith(".svg"));
|
||||
const ymlFiles = filesInDir.filter((x) => x.endsWith(".yml"));
|
||||
|
||||
if (
|
||||
svgFiles.length !== publishedIconComponents.length ||
|
||||
ymlFiles.length !== publishedIconComponents.length
|
||||
) {
|
||||
throw new Error(
|
||||
`Icons (${svgFiles.length}) and configs (${ymlFiles.length}) written to directory does not match the expected amount of icons located in Figma (${publishedIconComponents.length}).\nThis is most likely caused by duplicate icon names from figma.`,
|
||||
);
|
||||
}
|
||||
|
||||
console.info(
|
||||
`Success! A total of ${
|
||||
Object.keys(imagesUrls).length
|
||||
} were fetched and downloaded from Figma 🎉`,
|
||||
);
|
||||
}
|
||||
+35
-8
@@ -1,17 +1,40 @@
|
||||
import { PublishedComponent } from "@figma/rest-api-spec";
|
||||
import { writeFileSync } from "fs";
|
||||
import jsYaml from "js-yaml";
|
||||
import { dump } from "js-yaml";
|
||||
import { resolve } from "path";
|
||||
import { resolveName } from "./icon-name.mjs";
|
||||
import { resolveName } from "./icon-name";
|
||||
|
||||
export const makeConfig = (icons, folder) => {
|
||||
icons.forEach((icon) => {
|
||||
export type IconYml = {
|
||||
name: string;
|
||||
category: string;
|
||||
sub_category: string;
|
||||
keywords: string[];
|
||||
variant: string;
|
||||
updated_at: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export const makeConfig = (
|
||||
icons: PublishedComponent[],
|
||||
dirLocation: string,
|
||||
) => {
|
||||
console.group("Creating icon yml...");
|
||||
|
||||
let counter = 0;
|
||||
for (const icon of icons) {
|
||||
const name = resolveName(icon).replace(".svg", "");
|
||||
const keywords = icon.description
|
||||
.split(",")
|
||||
.map((x) => x.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const config = {
|
||||
if (!icon.containing_frame?.pageName || !icon.containing_frame?.name) {
|
||||
throw new Error(
|
||||
`Missing containing_frame.pageName or containing_frame.name for icon ${name}`,
|
||||
);
|
||||
}
|
||||
|
||||
const config: IconYml = {
|
||||
name,
|
||||
category: icon.containing_frame.pageName,
|
||||
sub_category: icon.containing_frame.name,
|
||||
@@ -33,14 +56,18 @@ export const makeConfig = (icons, folder) => {
|
||||
.join("."),
|
||||
};
|
||||
|
||||
const yml = jsYaml.dump(config, {
|
||||
const yml = dump(config, {
|
||||
noRefs: true,
|
||||
skipInvalid: false,
|
||||
quotingType: '"',
|
||||
});
|
||||
|
||||
writeFileSync(resolve(folder, `${config.name}.yml`), yml, {
|
||||
writeFileSync(resolve(dirLocation, `${config.name}.yml`), yml, {
|
||||
encoding: "utf8",
|
||||
});
|
||||
});
|
||||
counter++;
|
||||
}
|
||||
|
||||
console.info(`Created ${counter} icon yml files`);
|
||||
console.groupEnd();
|
||||
};
|
||||
@@ -1,12 +0,0 @@
|
||||
export function paginate(arr, size) {
|
||||
return arr.reduce((acc, val, i) => {
|
||||
let idx = Math.floor(i / size);
|
||||
if (!acc[idx]) {
|
||||
acc[idx] = [];
|
||||
}
|
||||
let page = acc[idx];
|
||||
page.push(val);
|
||||
|
||||
return acc;
|
||||
}, []);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ ymlList.forEach((file) => {
|
||||
const ymlData = jsYaml.load(fs.readFileSync(`${basePath}/${file}`), {
|
||||
schema: jsYaml.JSON_SCHEMA,
|
||||
});
|
||||
// biome-ignore lint/complexity/useOptionalChain: optional chain not supported in enviroment. Consider revriting to Typescript
|
||||
if (ymlData.keywords && ymlData.keywords.includes("[ignore-docs]")) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
"react-dom": "^18.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@figma/plugin-typings": "^1.84.0",
|
||||
"@figma/plugin-typings": "^1.100.2",
|
||||
"@svgr/core": "^8.1.0",
|
||||
"@types/node": "^18.15.11",
|
||||
"@types/postcss-url": "^10.0.4",
|
||||
@@ -27,7 +27,7 @@
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"postcss": "^8.4.31",
|
||||
"postcss-url": "^10.1.3",
|
||||
"typescript": "^5.1.6",
|
||||
"typescript": "5.5.4",
|
||||
"vite": "^5.4.6",
|
||||
"vite-plugin-singlefile": "^1.0.0",
|
||||
"vite-plugin-svgr": "^4.2.0"
|
||||
|
||||
@@ -109,7 +109,7 @@ const App = () => {
|
||||
icon.removeEventListener("dragend", dragEvent);
|
||||
}
|
||||
};
|
||||
}, [categories]);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<main tabIndex={-1} className="wrapper">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-icons",
|
||||
"version": "7.1.0",
|
||||
"version": "7.3.0",
|
||||
"author": "Aksel | NAV",
|
||||
"homepage": "https://aksel.nav.no/ikoner",
|
||||
"repository": {
|
||||
@@ -62,28 +62,30 @@
|
||||
"update-metadata": "node config/metadata.js",
|
||||
"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": "node config/figma/index.mjs",
|
||||
"fetch-new:icons": "tsx --env-file=.env config/figma/index.ts",
|
||||
"zip:icons": "node config/zip.js",
|
||||
"test": "vitest run",
|
||||
"test:watch": "vitest watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@figma/rest-api-spec": "^0.19.0",
|
||||
"@svgr/cli": "8.1.0",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/react": "^18.3.11",
|
||||
"adm-zip": "^0.5.10",
|
||||
"concurrently": "7.2.1",
|
||||
"concurrently": "9.0.1",
|
||||
"copyfiles": "^2.4.1",
|
||||
"cross-zip-cli": "1.0.0",
|
||||
"dotenv": "^10.0.0",
|
||||
"fast-glob": "3.2.11",
|
||||
"hast-util-select": "5.0.5",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lodash.camelcase": "4.3.0",
|
||||
"lodash.startcase": "4.4.0",
|
||||
"lodash": "^4.17.21",
|
||||
"prettier": "3.1.1",
|
||||
"react": "^18.0.0",
|
||||
"rehype-parse": "8.0.4",
|
||||
"tsc-alias": "1.8.8",
|
||||
"typescript": "^5.1.6",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "5.5.4",
|
||||
"unified": "10.1.2",
|
||||
"vitest": "^1.2.2"
|
||||
}
|
||||
|
||||
@@ -1,5 +1,19 @@
|
||||
# @navikt/aksel-stylelint
|
||||
|
||||
## 7.3.0
|
||||
|
||||
## 7.2.1
|
||||
|
||||
## 7.2.0
|
||||
|
||||
## 7.1.2
|
||||
|
||||
## 7.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- List: :boom: Removed class names "navds-list--nested" and "navds-list\_\_item-content" ([#3184](https://github.com/navikt/aksel/pull/3184))
|
||||
|
||||
## 7.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel-stylelint",
|
||||
"version": "7.1.0",
|
||||
"version": "7.3.0",
|
||||
"author": "Aksel | NAV",
|
||||
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
|
||||
"repository": {
|
||||
@@ -35,14 +35,14 @@
|
||||
"dev": "yarn watch:lint"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-css": "^7.1.0",
|
||||
"@navikt/ds-tokens": "^7.1.0",
|
||||
"concurrently": "7.2.1",
|
||||
"@navikt/ds-css": "^7.3.0",
|
||||
"@navikt/ds-tokens": "^7.3.0",
|
||||
"concurrently": "9.0.1",
|
||||
"postcss-selector-parser": "^6.0.13",
|
||||
"postcss-value-parser": "^4.2.0",
|
||||
"stylelint": "^14.8.5",
|
||||
"stylelint-test-rule-node": "^0.2.1",
|
||||
"tsx": "^4.7.1",
|
||||
"typescript": "^5.1.6"
|
||||
"typescript": "5.5.4"
|
||||
}
|
||||
}
|
||||
@@ -68,4 +68,8 @@ export const deprecations: DeprecatedList = [
|
||||
],
|
||||
message: "These classes had no effect and were removed in v7.1.0",
|
||||
},
|
||||
{
|
||||
classes: ["navds-list--nested", "navds-list__item-content"],
|
||||
message: "Removed in v7.1.1",
|
||||
},
|
||||
];
|
||||
@@ -1,5 +1,40 @@
|
||||
# @navikt/aksel
|
||||
|
||||
## 7.3.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`20ced9b`](https://github.com/navikt/aksel/commit/20ced9b084a1f4bb56a1e1c07ef92010c9f4d71c), [`536f088`](https://github.com/navikt/aksel/commit/536f088b97ab9ae139d6974deb2a3c69e4fb6db3)]:
|
||||
- @navikt/ds-css@7.3.0
|
||||
|
||||
## 7.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`6ddea4eaf`](https://github.com/navikt/aksel/commit/6ddea4eaf2e37f20f93d75e1a3a327b038fb209b)]:
|
||||
- @navikt/ds-css@7.2.1
|
||||
|
||||
## 7.2.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-css@7.2.0
|
||||
|
||||
## 7.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`95c84e1d0`](https://github.com/navikt/aksel/commit/95c84e1d014d880bc22eb0053a875771e8399540)]:
|
||||
- @navikt/ds-css@7.1.2
|
||||
|
||||
## 7.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies [[`ca1d9be88`](https://github.com/navikt/aksel/commit/ca1d9be88f45f936ee8546af146f8783d8c43db2), [`73b2bca15`](https://github.com/navikt/aksel/commit/73b2bca156072c945c2fc4e4e849f50a00398eb6), [`d278060e5`](https://github.com/navikt/aksel/commit/d278060e59b17638cbda22dc194e89b01dc2113e)]:
|
||||
- @navikt/ds-css@7.1.1
|
||||
|
||||
## 7.1.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/aksel",
|
||||
"version": "7.1.0",
|
||||
"version": "7.3.0",
|
||||
"description": "Aksel command line interface. Handles css-imports, codemods and more",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"license": "MIT",
|
||||
@@ -29,7 +29,7 @@
|
||||
},
|
||||
"homepage": "https://aksel.nav.no/grunnleggende/kode/kommandolinje",
|
||||
"dependencies": {
|
||||
"@navikt/ds-css": "7.1.0",
|
||||
"@navikt/ds-css": "7.3.0",
|
||||
"axios": "1.7.4",
|
||||
"chalk": "4.1.0",
|
||||
"clipboardy": "^2.3.0",
|
||||
@@ -45,7 +45,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"rimraf": "6.0.1",
|
||||
"typescript": "^5.1.6",
|
||||
"typescript": "5.5.4",
|
||||
"vitest": "^1.2.2"
|
||||
},
|
||||
"sideEffects": false,
|
||||
|
||||
@@ -43,7 +43,7 @@ export function validateGit(options: any, program: Command) {
|
||||
try {
|
||||
clean = isGitClean.sync(process.cwd());
|
||||
} catch (err: any) {
|
||||
if (err && err.stderr && err.stderr.indexOf("Not a git repository") >= 0) {
|
||||
if (err?.stderr && err.stderr.indexOf("Not a git repository") >= 0) {
|
||||
clean = true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ export async function getAllVersions() {
|
||||
const npmPackageData = await axios.get(
|
||||
`https://registry.npmjs.org/@navikt/ds-css`,
|
||||
);
|
||||
return Object.keys(npmPackageData.data["versions"]);
|
||||
return Object.keys(npmPackageData.data.versions);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
return [];
|
||||
|
||||
@@ -137,7 +137,7 @@ export async function cssImportsCommand() {
|
||||
|
||||
let foundComponents: string[] = [];
|
||||
|
||||
if (answers["autoscan"] === "yes") {
|
||||
if (answers.autoscan === "yes") {
|
||||
foundComponents = await new Promise((resolve) => {
|
||||
exec(`node ${__dirname}/scan-code.js ${answers.scandir}`, (_, stdout) => {
|
||||
resolve(
|
||||
|
||||
@@ -1,5 +1,39 @@
|
||||
# @navikt/ds-css
|
||||
|
||||
## 7.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ActionMenu: :tada: New component! Replaces DropdownMenu as the go-to solution for floating menus. Includes support for grouping, sub-menus, checkboxes, radios and dividers. Read more in the [documentation](https://aksel.nav.no/komponenter/core/actionmenu). ([#3081](https://github.com/navikt/aksel/pull/3081))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- List.Item: Center icon horizontally inside it's container ([#3253](https://github.com/navikt/aksel/pull/3253))
|
||||
|
||||
## 7.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Combobox: Support PageUp/PageDown in dropdown list. ([#3158](https://github.com/navikt/aksel/pull/3158))
|
||||
|
||||
## 7.2.0
|
||||
|
||||
## 7.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- List: Explicitly set list-style on ol to override reset-CSS ([#3202](https://github.com/navikt/aksel/pull/3202))
|
||||
|
||||
## 7.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- List: Add indentation and remove overly verbose aria attributes ([#3184](https://github.com/navikt/aksel/pull/3184))
|
||||
|
||||
- Datepicker/Select: :lipstick: dim non-interactable icons when readonly is set. ([#3187](https://github.com/navikt/aksel/pull/3187))
|
||||
|
||||
- Combobox: :bug: implement missing readonly and disabled ([#3180](https://github.com/navikt/aksel/pull/3180))
|
||||
|
||||
## 7.1.0
|
||||
|
||||
### Patch Changes
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
.navds-action-menu__content {
|
||||
overflow: hidden;
|
||||
box-shadow: var(--a-shadow-medium);
|
||||
border-radius: var(--a-border-radius-large);
|
||||
}
|
||||
|
||||
/* stylelint-disable csstools/value-no-unknown-custom-properties */
|
||||
.navds-action-menu__content > .navds-action-menu__content-inner {
|
||||
--__ac-action-menu-content-p: var(--a-spacing-2);
|
||||
--__ac-action-menu-item-pr: var(--a-spacing-3);
|
||||
--__ac-action-menu-item-pl: var(--a-spacing-2);
|
||||
--__ac-action-menu-item-height: 2rem;
|
||||
|
||||
border-radius: var(--a-border-radius-large);
|
||||
background-color: var(--a-surface-default);
|
||||
min-width: 128px;
|
||||
max-width: min(95vw, 640px);
|
||||
transform-origin: var(--__ac-action-menu-content-transform-origin);
|
||||
animation-duration: 160ms;
|
||||
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
||||
padding: var(--__ac-action-menu-content-p);
|
||||
overflow: auto;
|
||||
max-height: var(--__ac-action-menu-content-available-height);
|
||||
}
|
||||
|
||||
.navds-action-menu__content:where([data-state="open"]):where([data-side="bottom"]) {
|
||||
animation-name: aksel-action-from-bottom, aksel-action-fade-in;
|
||||
}
|
||||
|
||||
.navds-action-menu__content:where([data-state="open"]):where([data-side="top"]) {
|
||||
animation-name: aksel-action-from-top, aksel-action-fade-in;
|
||||
}
|
||||
|
||||
.navds-action-menu__content:where([data-state="open"]):where([data-side="left"]) {
|
||||
animation-name: aksel-action-from-left, aksel-action-fade-in;
|
||||
}
|
||||
|
||||
.navds-action-menu__content:where([data-state="open"]):where([data-side="right"]) {
|
||||
animation-name: aksel-action-from-right, aksel-action-fade-in;
|
||||
}
|
||||
|
||||
@keyframes aksel-action-from-bottom {
|
||||
from {
|
||||
transform: translateY(-4px);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aksel-action-from-top {
|
||||
from {
|
||||
transform: translateY(4px);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aksel-action-from-left {
|
||||
from {
|
||||
transform: translateX(4px);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aksel-action-from-right {
|
||||
from {
|
||||
transform: translateX(-4px);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes aksel-action-fade-in {
|
||||
from {
|
||||
opacity: 0.01;
|
||||
}
|
||||
|
||||
to {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.navds-action-menu__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--a-spacing-2);
|
||||
min-height: var(--__ac-action-menu-item-height);
|
||||
cursor: default;
|
||||
border-radius: var(--a-border-radius-medium);
|
||||
position: relative;
|
||||
padding-left: var(--__ac-action-menu-item-pl);
|
||||
padding-right: var(--__ac-action-menu-item-pr);
|
||||
font-size: var(--a-font-size-medium);
|
||||
scroll-margin-block: var(--__ac-action-menu-content-p);
|
||||
line-height: 1.5;
|
||||
color: var(--a-text-default);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.navds-action-menu__item svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navds-action-menu__item--has-icon {
|
||||
--__ac-action-menu-item-pl: var(--a-spacing-6);
|
||||
}
|
||||
|
||||
.navds-action-menu__sub-trigger {
|
||||
--__ac-action-menu-item-pr: var(--a-spacing-05);
|
||||
}
|
||||
|
||||
.navds-action-menu__item:focus {
|
||||
outline: none;
|
||||
background-color: var(--a-surface-action-subtle-hover);
|
||||
color: var(--a-text-default);
|
||||
}
|
||||
|
||||
.navds-action-menu__item--danger {
|
||||
color: var(--a-text-danger);
|
||||
}
|
||||
|
||||
.navds-action-menu__item--danger:focus {
|
||||
background-color: var(--a-surface-danger-subtle);
|
||||
}
|
||||
|
||||
.navds-action-menu__marker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--a-spacing-1);
|
||||
}
|
||||
|
||||
.navds-action-menu__marker--right {
|
||||
margin-left: auto;
|
||||
padding-left: var(--a-spacing-4);
|
||||
}
|
||||
|
||||
.navds-action-menu__marker--left {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: var(--__ac-action-menu-item-pl);
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.navds-action-menu__marker-icon svg {
|
||||
font-size: 18px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navds-action-menu__shortcut {
|
||||
background-color: var(--a-surface-neutral-subtle);
|
||||
color: var(--a-text-default);
|
||||
border-radius: var(--a-border-radius-small);
|
||||
padding: 0 var(--a-spacing-1);
|
||||
min-width: 1.125rem;
|
||||
height: 1.125rem;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: var(--a-font-size-small);
|
||||
}
|
||||
|
||||
.navds-action-menu__label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
min-height: calc(var(--__ac-action-menu-item-height) - 6px);
|
||||
padding-right: var(--__ac-action-menu-item-pr);
|
||||
padding-left: var(--__ac-action-menu-item-pl);
|
||||
color: var(--a-text-subtle);
|
||||
border-radius: var(--a-border-radius-medium);
|
||||
user-select: none;
|
||||
cursor: default;
|
||||
font-size: var(--a-font-size-small);
|
||||
}
|
||||
|
||||
.navds-action-menu__divider {
|
||||
height: 1px;
|
||||
margin-block: var(--a-spacing-2);
|
||||
background-color: var(--a-border-divider);
|
||||
}
|
||||
|
||||
.navds-action-menu__indicator {
|
||||
display: grid;
|
||||
place-content: center;
|
||||
}
|
||||
|
||||
.navds-action-menu__indicator-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.navds-action-menu__indicator-icon--unchecked,
|
||||
.navds-action-menu__indicator-icon--checked,
|
||||
.navds-action-menu__indicator-icon--indeterminate {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.navds-action-menu__indicator[data-state="unchecked"] .navds-action-menu__indicator-icon--unchecked {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navds-action-menu__indicator:where([data-state="checked"]) .navds-action-menu__indicator-icon--checked {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navds-action-menu__indicator:where([data-state="indeterminate"]) .navds-action-menu__indicator-icon--indeterminate {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.navds-action-menu__item:where([aria-disabled="true"]) {
|
||||
color: var(--a-text-subtle);
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.navds-action-menu__sub-trigger:where([data-state="open"]) {
|
||||
background-color: var(--a-surface-neutral-subtle);
|
||||
}
|
||||
@@ -47,6 +47,8 @@ legend {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
/* Make elements with the HTML hidden attribute stay hidden by default. */
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
display: none !important;
|
||||
}
|
||||
@@ -69,6 +69,11 @@ const StyleMappings = {
|
||||
dependencies: [typoCss],
|
||||
},
|
||||
{ component: "Alert", main: "alert.css", dependencies: [typoCss] },
|
||||
{
|
||||
component: "ActionMenu",
|
||||
main: "action-menu.css",
|
||||
dependencies: [typoCss],
|
||||
},
|
||||
{
|
||||
component: "BodyLong",
|
||||
main: typoCss,
|
||||
|
||||
@@ -31,7 +31,6 @@
|
||||
@media (forced-colors: active) {
|
||||
.navds-copybutton {
|
||||
background-color: ButtonFace;
|
||||
border-color: ButtonText;
|
||||
border: solid 1px ButtonText;
|
||||
color: ButtonText;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# Template for writing new CSS
|
||||
|
||||
## File setup
|
||||
|
||||
1. Create new CSS-file for component: "alert.darkside.css", "button.darkside.css" etc. Remember to add `.darkside.` for easier lookup.
|
||||
2. Import file in "darkside/index.css": `@import "./alert.darkside.css`
|
||||
|
||||
Files will not be included in `/dist` for now to avoid it potentially polluting production-code. This will need to be revisited when we want to expose the new systems outside the team.
|
||||
|
||||
## Development
|
||||
|
||||
1. Write new CSS for component. Use same prefix `.navds` as before.
|
||||
2. Run storybook and change from "Default (old)" to "Darkside (new)" in toolbar. You are now developing new CSS 🎉
|
||||
|
||||
## Syntax
|
||||
|
||||
- Test using outline + outline-offset for focus-markings.
|
||||
|
||||
## Checklist
|
||||
|
||||
- Add new CSS-files under `core/css/darkside`.
|
||||
- New files should include `.darkside.` for easier lookup.
|
||||
- Import new CSS-files in `core/css/darkside/index.css`
|
||||
- New CSS-selectors should have `.navds` as prefix, same as original code.
|
||||
- Test changes in Storybook by changing mode in toolbar from "default (old)" -> "darkside (new)"
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: "Source Sans 3";
|
||||
font-style: italic;
|
||||
font-weight: 400 600;
|
||||
font-display: swap;
|
||||
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-italic-cyrillic_ext.woff2) format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: "Source Sans 3";
|
||||
font-style: italic;
|
||||
font-weight: 400 600;
|
||||
font-display: swap;
|
||||
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-italic-cyrillic.woff2) format("woff2");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Source Sans 3";
|
||||
font-style: italic;
|
||||
font-weight: 400 600;
|
||||
font-display: swap;
|
||||
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-italic-latin_ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
|
||||
U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Source Sans 3";
|
||||
font-style: italic;
|
||||
font-weight: 400 600;
|
||||
font-display: swap;
|
||||
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-italic.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
|
||||
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* cyrillic-ext */
|
||||
@font-face {
|
||||
font-family: "Source Sans 3";
|
||||
font-style: normal;
|
||||
font-weight: 400 600;
|
||||
font-display: swap;
|
||||
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-normal-cyrillic_ext.woff2) format("woff2");
|
||||
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
|
||||
}
|
||||
|
||||
/* cyrillic */
|
||||
@font-face {
|
||||
font-family: "Source Sans 3";
|
||||
font-style: normal;
|
||||
font-weight: 400 600;
|
||||
font-display: swap;
|
||||
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-normal-cyrillic.woff2) format("woff2");
|
||||
unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Source Sans 3";
|
||||
font-style: normal;
|
||||
font-weight: 400 600;
|
||||
font-display: swap;
|
||||
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-normal-latin_ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
|
||||
U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Source Sans 3";
|
||||
font-style: normal;
|
||||
font-weight: 400 600;
|
||||
font-display: swap;
|
||||
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-normal.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
|
||||
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* Legacy support */
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Source Sans Pro";
|
||||
font-style: italic;
|
||||
font-weight: 400 600;
|
||||
font-display: swap;
|
||||
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-italic-latin_ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
|
||||
U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Source Sans Pro";
|
||||
font-style: italic;
|
||||
font-weight: 400 600;
|
||||
font-display: swap;
|
||||
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-italic.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
|
||||
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
/* latin-ext */
|
||||
@font-face {
|
||||
font-family: "Source Sans Pro";
|
||||
font-style: normal;
|
||||
font-weight: 400 600;
|
||||
font-display: swap;
|
||||
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-normal-latin_ext.woff2) format("woff2");
|
||||
unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113,
|
||||
U+2C60-2C7F, U+A720-A7FF;
|
||||
}
|
||||
|
||||
/* latin */
|
||||
@font-face {
|
||||
font-family: "Source Sans Pro";
|
||||
font-style: normal;
|
||||
font-weight: 400 600;
|
||||
font-display: swap;
|
||||
src: url(https://cdn.nav.no/aksel/fonts/SourceSans3-normal.woff2) format("woff2");
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F,
|
||||
U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
/* ==========================================================================
|
||||
Print styles copied from https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/style.css
|
||||
Inlined to avoid the additional HTTP request:
|
||||
https://www.phpied.com/delay-loading-your-print-css/
|
||||
========================================================================== */
|
||||
|
||||
@media print {
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
background: #fff !important;
|
||||
color: #000 !important;
|
||||
|
||||
/* Black prints faster */
|
||||
box-shadow: none !important;
|
||||
text-shadow: none !important;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
a[href]::after {
|
||||
content: " (" attr(href) ")";
|
||||
}
|
||||
|
||||
abbr[title]::after {
|
||||
content: " (" attr(title) ")";
|
||||
}
|
||||
|
||||
/*
|
||||
* Don't show links that are fragment identifiers,
|
||||
* or use the `javascript:` pseudo protocol
|
||||
*/
|
||||
a[href^="#"]::after,
|
||||
a[href^="javascript:"]::after {
|
||||
content: "";
|
||||
}
|
||||
|
||||
pre {
|
||||
white-space: pre-wrap !important;
|
||||
}
|
||||
|
||||
pre,
|
||||
blockquote {
|
||||
border: 1px solid #999;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
/*
|
||||
* Printing Tables:
|
||||
* https://web.archive.org/web/20180815150934/http://css-discuss.incutio.com/wiki/Printing_Tables
|
||||
*/
|
||||
thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
tr,
|
||||
img {
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
p,
|
||||
h2,
|
||||
h3 {
|
||||
orphans: 3;
|
||||
widows: 3;
|
||||
}
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
page-break-after: avoid;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
/* Modern-normalize: https://github.com/sindresorhus/modern-normalize/blob/main/modern-normalize.css */
|
||||
|
||||
/* Better box-model */
|
||||
*,
|
||||
::before,
|
||||
::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html {
|
||||
font-family: system-ui, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji";
|
||||
line-height: 1.15; /* Correct the line height in all browsers. */
|
||||
-webkit-text-size-adjust: 100%; /* Prevent adjustments of font size after orientation changes in iOS. */
|
||||
tab-size: 4; /* Use a more readable tab size (opinionated). */
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0; /* Remove the margin in all browsers. */
|
||||
}
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
1. Improve consistency of default fonts in all browsers. (https://github.com/sindresorhus/modern-normalize/issues/3)
|
||||
2. Correct the odd 'em' font sizing in all browsers.
|
||||
*/
|
||||
code,
|
||||
kbd,
|
||||
samp,
|
||||
pre {
|
||||
font-family: ui-monospace, SFMono-Regular, Consolas, "Liberation Mono", Menlo, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
Add the correct font size in all browsers.
|
||||
*/
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
Prevent 'sub' and 'sup' elements from affecting the line height in all browsers.
|
||||
*/
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/**
|
||||
Correct table border color inheritance in Chrome and Safari. (https://issues.chromium.org/issues/40615503, https://bugs.webkit.org/show_bug.cgi?id=195016)
|
||||
*/
|
||||
table {
|
||||
border-color: currentColor;
|
||||
}
|
||||
|
||||
/**
|
||||
1. Change the font styles in all browsers.
|
||||
2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
Remove the padding so developers are not caught out when they zero out 'fieldset' elements in all browsers.
|
||||
*/
|
||||
legend {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
Add the correct vertical alignment in Chrome and Firefox.
|
||||
*/
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
Correct the cursor style of increment and decrement buttons in Safari.
|
||||
*/
|
||||
::-webkit-inner-spin-button,
|
||||
::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
1. Correct the odd appearance in Chrome and Safari.
|
||||
2. Correct the outline style in Safari.
|
||||
*/
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
1. Correct the inability to style clickable types in iOS and Safari.
|
||||
2. Change font properties to 'inherit' in Safari.
|
||||
*/
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/*
|
||||
Add the correct display in Chrome and Safari.
|
||||
*/
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/* To support nested theming, we want to declare color on each scoped elements */
|
||||
:root,
|
||||
:host,
|
||||
.light {
|
||||
color: var(--a-text-default);
|
||||
}
|
||||
|
||||
.dark {
|
||||
color: var(--a-text-default);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* https://web.dev/prefers-reduced-motion/ */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*:not(.navds-loader *):not(.navds-loader):not(.navds-progress-bar *):not(.navds-progress-bar),
|
||||
::before,
|
||||
::after {
|
||||
animation-delay: -1ms !important;
|
||||
animation-duration: 1ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
background-attachment: initial !important;
|
||||
scroll-behavior: auto !important;
|
||||
transition-duration: 0s !important;
|
||||
transition-delay: 0s !important;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* From https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/style.css
|
||||
*
|
||||
* Hide only visually, but have it available for screen readers:
|
||||
* https://snook.ca/archives/html_and_css/hiding-content-for-accessibility
|
||||
*
|
||||
* 1. For long content, line feeds are not interpreted as spaces and small width
|
||||
* causes content to wrap 1 word per line:
|
||||
* https://medium.com/@jessebeach/beware-smushed-off-screen-accessible-text-5952a4c2cbfe
|
||||
*/
|
||||
|
||||
.sr-only,
|
||||
.navds-sr-only {
|
||||
border: 0;
|
||||
clip: rect(0, 0, 0, 0);
|
||||
clip-path: inset(50%);
|
||||
height: 1px;
|
||||
margin: -1px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
position: absolute;
|
||||
white-space: nowrap;
|
||||
width: 1px;
|
||||
}
|
||||
|
||||
/*
|
||||
* From https://github.com/h5bp/html5-boilerplate/blob/master/dist/css/style.css
|
||||
*
|
||||
* Extends the .sr-only class to allow the element
|
||||
* to be focusable when navigated to via the keyboard:
|
||||
* https://www.drupal.org/node/897638
|
||||
*/
|
||||
|
||||
.sr-only.focusable:active,
|
||||
.sr-only.focusable:focus,
|
||||
.navds-sr-only.focusable:active,
|
||||
.navds-sr-only.focusable:focus {
|
||||
clip: auto;
|
||||
clip-path: none;
|
||||
height: auto;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
position: static;
|
||||
white-space: inherit;
|
||||
width: auto;
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
/* stylelint-disable csstools/value-no-unknown-custom-properties */
|
||||
/* stylelint-disable aksel/design-token-exists */
|
||||
|
||||
.navds-button {
|
||||
background-color: var(--a-bg-accent-strong);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
@charset "UTF-8";
|
||||
|
||||
@layer aksel.reset, aksel.typography, aksel.theme, aksel.components, aksel.utilities;
|
||||
|
||||
/* CSS foundations */
|
||||
@import url("./baseline/reset.darkside.css") layer(aksel.reset);
|
||||
@import url("./baseline/print.darkside.css") layer(aksel.reset);
|
||||
@import url("./baseline/fonts.darkside.css") layer(aksel.typography);
|
||||
@import url("./baseline/theme.darkside.css") layer(aksel.theme);
|
||||
@import url("./typography.darkside.css") layer(aksel.typography);
|
||||
@import url("./baseline/utilities.darkside.css") layer(aksel.utilities);
|
||||
|
||||
/* Theming */
|
||||
@import url("@navikt/ds-tokens/dist/darkside/tokens.css") layer(aksel.theme);
|
||||
|
||||
/* Components */
|
||||
@import url("./button.darkside.css") layer(aksel.components);
|
||||
@@ -0,0 +1,6 @@
|
||||
body,
|
||||
:host {
|
||||
font-family: "Source Sans 3", "Source Sans Pro", Arial, sans-serif;
|
||||
line-height: 1.333;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
@@ -281,6 +281,7 @@
|
||||
/* Readonly */
|
||||
.navds-date__field--readonly .navds-date__field-button {
|
||||
cursor: default;
|
||||
color: var(--a-gray-500);
|
||||
}
|
||||
|
||||
.navds-date__caption-button {
|
||||
|
||||
@@ -20,6 +20,36 @@
|
||||
--__ac-combobox-input-height: 1.5rem;
|
||||
}
|
||||
|
||||
.navds-combobox--disabled {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.navds-combobox--disabled .navds-combobox__wrapper *:hover {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.navds-combobox--disabled .navds-text-field__input {
|
||||
border: 1px solid var(--a-border-default);
|
||||
}
|
||||
|
||||
.navds-combobox--disabled .navds-combobox__selected-options {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.navds-combobox--readonly {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.navds-combobox--readonly .navds-combobox__button-toggle-list {
|
||||
color: var(--a-gray-500);
|
||||
}
|
||||
|
||||
.navds-combobox--readonly .navds-text-field__input,
|
||||
.navds-combobox--readonly .navds-combobox__input {
|
||||
background-color: var(--a-surface-subtle);
|
||||
border-color: var(--a-border-subtle);
|
||||
}
|
||||
|
||||
.navds-combobox__button-clear svg,
|
||||
.navds-combobox__button-toggle-list svg,
|
||||
.navds-combobox__list svg {
|
||||
@@ -233,6 +263,7 @@
|
||||
border-radius: var(--a-border-radius-medium);
|
||||
background-color: var(--ac-combobox-list-bg, var(--a-surface-default));
|
||||
color: var(--ac-combobox-list-text, var(--a-text-default));
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.navds-combobox__list--closed {
|
||||
|
||||
@@ -109,3 +109,7 @@
|
||||
border-color: var(--a-border-subtle);
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.navds-select--readonly .navds-select__chevron {
|
||||
color: var(--a-gray-500);
|
||||
}
|
||||
@@ -8,6 +8,7 @@
|
||||
@import "chips.css";
|
||||
@import "copybutton.css";
|
||||
@import "dropdown.css";
|
||||
@import "action-menu.css";
|
||||
@import "expansioncard.css";
|
||||
@import "guide-panel.css";
|
||||
@import "form/index.css";
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
@media (forced-colors: active) {
|
||||
.navds-internalheader {
|
||||
background-color: ButtonFace;
|
||||
border-color: ButtonText;
|
||||
border: solid 1px ButtonText;
|
||||
color: ButtonText;
|
||||
}
|
||||
|
||||
+61
-71
@@ -4,92 +4,82 @@
|
||||
margin-block: var(--a-spacing-4);
|
||||
}
|
||||
|
||||
.navds-list--nested ul,
|
||||
.navds-list--nested ol {
|
||||
margin-block: var(--a-spacing-2);
|
||||
}
|
||||
|
||||
.navds-list--small ul,
|
||||
.navds-list--small ol {
|
||||
margin-block: var(--a-spacing-3);
|
||||
}
|
||||
|
||||
.navds-list li {
|
||||
margin-block-end: var(--a-spacing-2);
|
||||
}
|
||||
|
||||
.navds-list:last-child > ul > li:last-child,
|
||||
.navds-list:last-child > ol > li:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.navds-list ul .navds-list__item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--a-spacing-3);
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.navds-list ul .navds-list__item::marker {
|
||||
content: none;
|
||||
}
|
||||
|
||||
.navds-list ol .navds-list__item-marker--icon {
|
||||
grid-column: 1 / 2;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.navds-list .navds-list__item-marker--bullet {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--a-font-line-height-xlarge);
|
||||
grid-column: 1 / 2;
|
||||
color: var(--ac-list-marker-ul-color, var(--ac-list-marker-color, var(--a-icon-default)));
|
||||
}
|
||||
|
||||
.navds-list--small .navds-list__item-marker--bullet {
|
||||
height: var(--a-font-line-height-large);
|
||||
}
|
||||
|
||||
.navds-list__item-marker--icon {
|
||||
color: var(--ac-list-marker-icon-color, var(--ac-list-marker-color, var(--a-icon-default)));
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--a-font-line-height-xlarge);
|
||||
padding-block-end: 1px;
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.navds-list--small .navds-list__item-marker--icon {
|
||||
height: var(--a-font-line-height-large);
|
||||
}
|
||||
|
||||
.navds-list__item-marker--icon svg {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.navds-list ul .navds-list__item-content {
|
||||
grid-column: 2 / 3;
|
||||
.navds-list .navds-list :where(ul, ol) {
|
||||
margin-block: var(--a-spacing-2) 0;
|
||||
}
|
||||
|
||||
.navds-list ol {
|
||||
padding-left: var(--a-spacing-2);
|
||||
list-style: decimal;
|
||||
list-style: decimal; /* This is the default value, but some frameworks have `ol,ul { list-style:none }` */
|
||||
padding-left: 1.7rem;
|
||||
}
|
||||
|
||||
.navds-list ol li {
|
||||
padding-left: var(--a-spacing-1);
|
||||
margin-left: var(--a-spacing-2);
|
||||
.navds-list ol > .navds-list__item {
|
||||
padding-left: 0.3rem;
|
||||
}
|
||||
|
||||
/* SAFARI HACK START */
|
||||
/* stylelint-disable selector-type-no-unknown */
|
||||
|
||||
_::-webkit-full-page-media,
|
||||
_:future,
|
||||
:root .navds-list ol {
|
||||
padding-left: 2.1rem;
|
||||
}
|
||||
|
||||
_::-webkit-full-page-media,
|
||||
_:future,
|
||||
:root .navds-list ol .navds-list__item {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
/* SAFARI HACK END */
|
||||
/* stylelint-enable selector-type-no-unknown */
|
||||
|
||||
.navds-list__item {
|
||||
margin-block-end: var(--a-spacing-2);
|
||||
}
|
||||
|
||||
.navds-list__item:last-child {
|
||||
margin-block-end: 0;
|
||||
}
|
||||
|
||||
.navds-list ul > .navds-list__item {
|
||||
display: grid;
|
||||
grid-template-columns: auto 1fr;
|
||||
gap: var(--a-spacing-2);
|
||||
}
|
||||
|
||||
.navds-list ol li::marker {
|
||||
font-weight: var(--a-font-weight-bold);
|
||||
font-size: var(--a-font-size-large);
|
||||
color: var(--ac-list-marker-ol-color, var(--ac-list-marker-color, var(--a-icon-default)));
|
||||
line-height: var(--a-font-line-height-xlarge);
|
||||
}
|
||||
|
||||
.navds-list--small ol li::marker {
|
||||
font-size: var(--a-font-size-small);
|
||||
line-height: var(--a-font-line-height-large);
|
||||
.navds-list__item-marker {
|
||||
width: 1.5rem;
|
||||
}
|
||||
|
||||
.navds-list ul > li > .navds-list__item-marker {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: var(--a-font-line-height-xlarge);
|
||||
}
|
||||
|
||||
.navds-list--small ul > li > .navds-list__item-marker {
|
||||
height: var(--a-font-line-height-large);
|
||||
}
|
||||
|
||||
.navds-list__item-marker--bullet {
|
||||
padding-left: 0.8rem;
|
||||
color: var(--ac-list-marker-ul-color, var(--ac-list-marker-color, var(--a-icon-default)));
|
||||
}
|
||||
|
||||
.navds-list__item-marker--icon {
|
||||
font-size: 1.5rem;
|
||||
justify-content: center;
|
||||
color: var(--ac-list-marker-icon-color, var(--ac-list-marker-color, var(--a-icon-default)));
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-css",
|
||||
"version": "7.1.0",
|
||||
"version": "7.3.0",
|
||||
"description": "CSS for NAV Designsystem",
|
||||
"author": "Aksel | NAV designsystem team",
|
||||
"keywords": [
|
||||
@@ -27,10 +27,11 @@
|
||||
"css:get-version": "node config/get-version.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@navikt/ds-tokens": "^7.1.0",
|
||||
"@navikt/ds-tokens": "^7.3.0",
|
||||
"autoprefixer": "^10.4.20",
|
||||
"cssnano": "6.0.0",
|
||||
"fast-glob": "3.2.11",
|
||||
"lodash": "4.17.21",
|
||||
"lodash": "^4.17.21",
|
||||
"nodemon": "^3.0.1",
|
||||
"normalize.css": "^8.0.1",
|
||||
"postcss": "^8.4.31",
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
@media (forced-colors: active) {
|
||||
.navds-read-more__button {
|
||||
background-color: ButtonFace;
|
||||
border-color: ButtonText;
|
||||
border: solid 1px ButtonText;
|
||||
color: ButtonText;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,69 @@
|
||||
# @navikt/ds-react
|
||||
|
||||
## 7.3.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- ActionMenu: :tada: New component! Replaces DropdownMenu as the go-to solution for floating menus. Includes support for grouping, sub-menus, checkboxes, radios and dividers. Read more in the [documentation](https://aksel.nav.no/komponenter/core/actionmenu). ([#3081](https://github.com/navikt/aksel/pull/3081))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Accordion: Remove redundant icon title ([#3230](https://github.com/navikt/aksel/pull/3230))
|
||||
|
||||
- Modal: Close button now avoids triggering when repeated keystrokes are detected. This helps stop accidental closing of Modal when 'Enter'-key is held down. ([#3081](https://github.com/navikt/aksel/pull/3081))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@7.3.0
|
||||
- @navikt/aksel-icons@7.3.0
|
||||
|
||||
## 7.2.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Combobox: Disable autocomple in Firefox on Android to prevent bug ([#3201](https://github.com/navikt/aksel/pull/3201))
|
||||
|
||||
- Combobox: Support PageUp/PageDown in dropdown list. ([#3158](https://github.com/navikt/aksel/pull/3158))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@7.2.1
|
||||
- @navikt/aksel-icons@7.2.1
|
||||
|
||||
## 7.2.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- FileUpload: :boom: Remove UNSAFE-prefix ([#3205](https://github.com/navikt/aksel/pull/3205))
|
||||
|
||||
- List: :sparkles: New size 'large' ([#3207](https://github.com/navikt/aksel/pull/3207))
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Performance: :zap: Optimized memoization for rerendring in some components. ([#3197](https://github.com/navikt/aksel/pull/3197))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@7.2.0
|
||||
- @navikt/aksel-icons@7.2.0
|
||||
|
||||
## 7.1.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@7.1.2
|
||||
- @navikt/aksel-icons@7.1.2
|
||||
|
||||
## 7.1.1
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- List: Add indentation and remove overly verbose aria attributes ([#3184](https://github.com/navikt/aksel/pull/3184))
|
||||
|
||||
- Combobox: :bug: implement missing readonly and disabled ([#3180](https://github.com/navikt/aksel/pull/3180))
|
||||
|
||||
- Updated dependencies []:
|
||||
- @navikt/ds-tokens@7.1.1
|
||||
- @navikt/aksel-icons@7.1.1
|
||||
|
||||
## 7.1.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@navikt/ds-react",
|
||||
"version": "7.1.0",
|
||||
"version": "7.3.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",
|
||||
@@ -50,6 +50,16 @@
|
||||
"default": "./cjs/accordion/index.js"
|
||||
}
|
||||
},
|
||||
"./ActionMenu": {
|
||||
"import": {
|
||||
"types": "./esm/overlays/action-menu/index.d.ts",
|
||||
"default": "./esm/overlays/action-menu/index.js"
|
||||
},
|
||||
"require": {
|
||||
"types": "./cjs/overlays/action-menu/index.d.ts",
|
||||
"default": "./cjs/overlays/action-menu/index.js"
|
||||
}
|
||||
},
|
||||
"./Alert": {
|
||||
"import": {
|
||||
"types": "./esm/alert/index.d.ts",
|
||||
@@ -583,7 +593,7 @@
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"scripts": {
|
||||
"docgen": "yarn ts-node ../../../scripts/docgen.ts",
|
||||
"docgen": "yarn tsx ../../../scripts/docgen.ts",
|
||||
"write-packagejson": "echo '{\"type\": \"module\"}' > esm/package.json",
|
||||
"clean": "rimraf cjs esm",
|
||||
"build": "concurrently \"tsc -p tsconfig.build.json\" \"tsc -p tsconfig.esm.json && tsc-alias -p tsconfig.esm.json && yarn write-packagejson\" ",
|
||||
@@ -594,8 +604,8 @@
|
||||
"dependencies": {
|
||||
"@floating-ui/react": "0.25.4",
|
||||
"@floating-ui/react-dom": "^2.0.9",
|
||||
"@navikt/aksel-icons": "^7.1.0",
|
||||
"@navikt/ds-tokens": "^7.1.0",
|
||||
"@navikt/aksel-icons": "^7.3.0",
|
||||
"@navikt/ds-tokens": "^7.3.0",
|
||||
"clsx": "^2.1.0",
|
||||
"date-fns": "^3.0.0",
|
||||
"react-day-picker": "8.10.0"
|
||||
@@ -605,18 +615,16 @@
|
||||
"@testing-library/jest-dom": "^5.16.0",
|
||||
"@testing-library/react": "^15.0.7",
|
||||
"@testing-library/user-event": "^14.2.0",
|
||||
"@types/faker": "5.5.8",
|
||||
"concurrently": "7.2.1",
|
||||
"faker": "5.5.3",
|
||||
"concurrently": "9.0.1",
|
||||
"fast-glob": "3.2.11",
|
||||
"jsdom": "24.0.0",
|
||||
"react-dom": "^18.0.0",
|
||||
"react-router-dom": "^6.3.0",
|
||||
"rimraf": "6.0.1",
|
||||
"swr": "^1.1.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsc-alias": "1.8.8",
|
||||
"typescript": "^5.1.6",
|
||||
"tsx": "^4.19.1",
|
||||
"typescript": "5.5.4",
|
||||
"vitest": "^1.2.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
|
||||
@@ -38,7 +38,6 @@ const AccordionHeader = forwardRef<HTMLButtonElement, AccordionHeaderProps>(
|
||||
<span className="navds-accordion__icon-wrapper">
|
||||
<ChevronDownIcon
|
||||
className="navds-accordion__header-chevron"
|
||||
title="Vis mer"
|
||||
aria-hidden
|
||||
/>
|
||||
</span>
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
} from "@navikt/aksel-icons";
|
||||
import { Button } from "../button";
|
||||
import { BodyLong } from "../typography";
|
||||
import { useI18n } from "../util/i18n/i18n.context";
|
||||
|
||||
export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
/**
|
||||
@@ -54,25 +55,13 @@ export interface AlertProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
const Icon = ({ variant, ...props }) => {
|
||||
switch (variant) {
|
||||
case "error":
|
||||
return <XMarkOctagonFillIcon title="Feil" {...props} />;
|
||||
case "warning":
|
||||
return <ExclamationmarkTriangleFillIcon title="Advarsel" {...props} />;
|
||||
case "info":
|
||||
return <InformationSquareFillIcon title="Informasjon" {...props} />;
|
||||
case "success":
|
||||
return <CheckmarkCircleFillIcon title="Suksess" {...props} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
const IconMap = {
|
||||
error: XMarkOctagonFillIcon,
|
||||
warning: ExclamationmarkTriangleFillIcon,
|
||||
info: InformationSquareFillIcon,
|
||||
success: CheckmarkCircleFillIcon,
|
||||
};
|
||||
|
||||
export interface AlertContextProps {
|
||||
size: "medium" | "small";
|
||||
}
|
||||
|
||||
/**
|
||||
* A component for displaying alerts
|
||||
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/alert)
|
||||
@@ -98,6 +87,8 @@ export const Alert = forwardRef<HTMLDivElement, AlertProps>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const translate = useI18n("Alert");
|
||||
const Icon = IconMap[variant];
|
||||
return (
|
||||
<div
|
||||
{...rest}
|
||||
@@ -114,7 +105,7 @@ export const Alert = forwardRef<HTMLDivElement, AlertProps>(
|
||||
},
|
||||
)}
|
||||
>
|
||||
<Icon variant={variant} className="navds-alert__icon" />
|
||||
<Icon title={translate(variant)} className="navds-alert__icon" />
|
||||
<BodyLong
|
||||
as="div"
|
||||
size={size}
|
||||
@@ -137,8 +128,8 @@ export const Alert = forwardRef<HTMLDivElement, AlertProps>(
|
||||
<XMarkIcon
|
||||
title={
|
||||
["error", "warning"].includes(variant)
|
||||
? "Lukk varsel"
|
||||
: "Lukk melding"
|
||||
? translate("closeAlert")
|
||||
: translate("closeMessage")
|
||||
}
|
||||
/>
|
||||
}
|
||||
|
||||
@@ -190,3 +190,19 @@ export const ChromaticForcedColors: Story = {
|
||||
chromatic: { disable: false, forcedColors: "active" },
|
||||
},
|
||||
};
|
||||
|
||||
export const ChromaticDarksideDark: Story = {
|
||||
render: Chromatic.render,
|
||||
parameters: {
|
||||
chromatic: { disable: false },
|
||||
},
|
||||
globals: { theme: "dark", mode: "darkside" },
|
||||
};
|
||||
|
||||
export const ChromaticDarksideLight: Story = {
|
||||
render: Chromatic.render,
|
||||
parameters: {
|
||||
chromatic: { disable: false },
|
||||
},
|
||||
globals: { theme: "light", mode: "darkside" },
|
||||
};
|
||||
@@ -58,7 +58,7 @@ export const Size: Story = {
|
||||
render: () => (
|
||||
<VStack gap="4">
|
||||
{SIZES.map((size) => (
|
||||
<>
|
||||
<React.Fragment key={size}>
|
||||
<h3>{size}</h3>
|
||||
<Chat
|
||||
avatar="ON"
|
||||
@@ -75,7 +75,7 @@ export const Size: Story = {
|
||||
laboris labore nisi ut.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
</>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</VStack>
|
||||
),
|
||||
@@ -85,7 +85,7 @@ export const Variants: Story = {
|
||||
render: () => (
|
||||
<VStack gap="4">
|
||||
{VARIANTS.map((variant) => (
|
||||
<>
|
||||
<React.Fragment key={variant}>
|
||||
<h3>{variant}</h3>
|
||||
<Chat
|
||||
avatar="NAV"
|
||||
@@ -97,7 +97,7 @@ export const Variants: Story = {
|
||||
Aute minim nisi sunt mollit duis sunt nulla minim non proident.
|
||||
</Chat.Bubble>
|
||||
</Chat>
|
||||
</>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</VStack>
|
||||
),
|
||||
|
||||
@@ -2,6 +2,7 @@ import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import { XMarkIcon } from "@navikt/aksel-icons";
|
||||
import { composeEventHandlers } from "../util/composeEventHandlers";
|
||||
import { useI18n } from "../util/i18n/i18n.context";
|
||||
|
||||
export interface ChipsRemovableProps
|
||||
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
@@ -11,15 +12,15 @@ export interface ChipsRemovableProps
|
||||
* @default "action"
|
||||
*/
|
||||
variant?: "action" | "neutral";
|
||||
/**
|
||||
* Replaces label read for screen-readers
|
||||
* @default "slett filter"
|
||||
*/
|
||||
removeLabel?: string;
|
||||
/**
|
||||
* Click callback
|
||||
*/
|
||||
onDelete?: () => void;
|
||||
/**
|
||||
* Replaces label read for screen-readers
|
||||
* @default "slett"
|
||||
*/
|
||||
removeLabel?: string;
|
||||
}
|
||||
|
||||
export const RemovableChips = forwardRef<
|
||||
@@ -28,17 +29,18 @@ export const RemovableChips = forwardRef<
|
||||
>(
|
||||
(
|
||||
{
|
||||
className,
|
||||
children,
|
||||
variant = "action",
|
||||
removeLabel = "slett",
|
||||
onDelete,
|
||||
type = "button",
|
||||
removeLabel,
|
||||
className,
|
||||
onClick,
|
||||
type = "button",
|
||||
...rest
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const translate = useI18n("Chips");
|
||||
return (
|
||||
<button
|
||||
{...rest}
|
||||
@@ -49,7 +51,9 @@ export const RemovableChips = forwardRef<
|
||||
className,
|
||||
`navds-chips__removable--${variant}`,
|
||||
)}
|
||||
aria-label={`${children} ${removeLabel}`}
|
||||
aria-label={`${children} ${
|
||||
removeLabel ?? translate("Removable.labelSuffix")
|
||||
}`}
|
||||
onClick={composeEventHandlers(onClick, onDelete)}
|
||||
>
|
||||
<span className="navds-chips__chip-text">{children}</span>
|
||||
|
||||
@@ -27,8 +27,8 @@ interface DatePickerComponent
|
||||
* ```jsx
|
||||
* <DatePicker.Standalone
|
||||
* dropdownCaption
|
||||
* fromDate={new Date("2020-10-01")}
|
||||
* toDate={new Date("2024-10-01")}
|
||||
* fromDate={new Date("2022-10-01")}
|
||||
* toDate={new Date("2026-10-01")}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
|
||||
@@ -73,40 +73,35 @@ export const Default: StoryObj<DefaultStoryProps> = {
|
||||
locale={props.locale}
|
||||
disableWeekends={props.disableWeekends}
|
||||
>
|
||||
{!props.standalone && (
|
||||
<>
|
||||
{props.inputfield && props.mode !== "multiple" ? (
|
||||
<>
|
||||
{props.mode === "range" ? (
|
||||
<div style={{ display: "flex", gap: "1rem" }}>
|
||||
<DatePicker.Input
|
||||
label="Fra"
|
||||
size={props?.size}
|
||||
{...rangeCtx.fromInputProps}
|
||||
/>
|
||||
<DatePicker.Input
|
||||
label="Til"
|
||||
size={props?.size}
|
||||
{...rangeCtx.toInputProps}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<DatePicker.Input
|
||||
label="Velg dato"
|
||||
size={props?.size}
|
||||
{...singleCtx.inputProps}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={() => setOpen((x) => !x)}>
|
||||
Åpne datovelger
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!props.standalone &&
|
||||
(props.inputfield && props.mode !== "multiple" ? (
|
||||
<>
|
||||
{props.mode === "range" ? (
|
||||
<div style={{ display: "flex", gap: "1rem" }}>
|
||||
<DatePicker.Input
|
||||
label="Fra"
|
||||
size={props?.size}
|
||||
{...rangeCtx.fromInputProps}
|
||||
/>
|
||||
<DatePicker.Input
|
||||
label="Til"
|
||||
size={props?.size}
|
||||
{...rangeCtx.toInputProps}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<DatePicker.Input
|
||||
label="Velg dato"
|
||||
size={props?.size}
|
||||
{...singleCtx.inputProps}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<Button onClick={() => setOpen((x) => !x)}>
|
||||
Åpne datovelger
|
||||
</Button>
|
||||
))}
|
||||
</Comp>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -19,8 +19,8 @@ interface MonthPickerComponent
|
||||
* ```jsx
|
||||
* <MonthPicker.Standalone
|
||||
* dropdownCaption
|
||||
* fromDate={new Date("1 Oct 2020")}
|
||||
* toDate={new Date("1 Oct 2024")}
|
||||
* fromDate={new Date("1 Oct 2022")}
|
||||
* toDate={new Date("1 Oct 2026")}
|
||||
* />
|
||||
* ```
|
||||
*/
|
||||
|
||||
@@ -82,7 +82,7 @@ export const Menu = forwardRef<HTMLDivElement, DropdownMenuProps>(
|
||||
open={isOpen}
|
||||
onClose={() => {
|
||||
handleToggle(false);
|
||||
onClose && onClose();
|
||||
onClose?.();
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import faker from "faker";
|
||||
import React from "react";
|
||||
import { describe, expect, test, vi } from "vitest";
|
||||
import { Checkbox, CheckboxGroup } from ".";
|
||||
@@ -10,8 +9,8 @@ const firstArgumentOfFirstCall = (fn: ReturnType<typeof vi.fn>) =>
|
||||
test("checkbox group chains onChange calls", async () => {
|
||||
const onGroupChange = vi.fn();
|
||||
const onChange = vi.fn();
|
||||
const value = faker.datatype.string();
|
||||
const label = faker.datatype.string();
|
||||
const value = "Checkbox value";
|
||||
const label = "My pretty label";
|
||||
|
||||
render(
|
||||
<CheckboxGroup legend="legend" onChange={onGroupChange}>
|
||||
|
||||
@@ -44,8 +44,8 @@ const useCheckbox = (props: CheckboxProps) => {
|
||||
if (readOnly) {
|
||||
return;
|
||||
}
|
||||
props.onChange && props.onChange(e);
|
||||
checkboxGroup && checkboxGroup.toggleValue(props.value);
|
||||
props.onChange?.(e);
|
||||
checkboxGroup?.toggleValue(props.value);
|
||||
},
|
||||
onClick: (e) => {
|
||||
if (readOnly) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef } from "react";
|
||||
import { BodyShort, ErrorMessage, Label } from "../../typography";
|
||||
import { ReadOnlyIcon } from "../ReadOnlyIcon";
|
||||
import ComboboxWrapper from "./ComboboxWrapper";
|
||||
import FilteredOptions from "./FilteredOptions/FilteredOptions";
|
||||
import { useFilteredOptionsContext } from "./FilteredOptions/filteredOptionsContext";
|
||||
@@ -27,6 +28,7 @@ export const Combobox = forwardRef<
|
||||
inputProps,
|
||||
showErrorMsg,
|
||||
size = "medium",
|
||||
readOnly,
|
||||
} = useInputContext();
|
||||
|
||||
return (
|
||||
@@ -44,6 +46,7 @@ export const Combobox = forwardRef<
|
||||
"navds-sr-only": hideLabel,
|
||||
})}
|
||||
>
|
||||
<ReadOnlyIcon nativeReadOnly={false} readOnly={readOnly} />
|
||||
{label}
|
||||
</Label>
|
||||
{!!description && (
|
||||
|
||||
@@ -36,6 +36,7 @@ const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
|
||||
children,
|
||||
defaultValue,
|
||||
disabled,
|
||||
readOnly,
|
||||
error,
|
||||
errorId,
|
||||
filteredOptions: externalFilteredOptions,
|
||||
@@ -50,19 +51,28 @@ const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
|
||||
value,
|
||||
onChange,
|
||||
onClear,
|
||||
shouldAutocomplete,
|
||||
shouldAutocomplete: externalShouldAutocomplete,
|
||||
size,
|
||||
...rest
|
||||
} = props;
|
||||
const options = mapToComboboxOptionArray(externalOptions) || [];
|
||||
const filteredOptions = mapToComboboxOptionArray(externalFilteredOptions);
|
||||
const selectedOptions = mapToComboboxOptionArray(externalSelectedOptions);
|
||||
|
||||
const userAgent =
|
||||
typeof navigator === "undefined" ? "" : navigator.userAgent;
|
||||
const isFirefoxOnAndroid =
|
||||
userAgent.includes("Android") && userAgent.includes("Firefox/");
|
||||
const shouldAutocomplete =
|
||||
!isFirefoxOnAndroid && externalShouldAutocomplete;
|
||||
|
||||
return (
|
||||
<InputContextProvider
|
||||
value={{
|
||||
defaultValue,
|
||||
description: rest.description,
|
||||
disabled,
|
||||
readOnly,
|
||||
error,
|
||||
errorId,
|
||||
id,
|
||||
|
||||
@@ -9,6 +9,7 @@ type ComboboxWrapperProps = {
|
||||
inputProps: {
|
||||
disabled?: boolean;
|
||||
};
|
||||
readOnly?: boolean;
|
||||
inputSize: string;
|
||||
toggleIsListOpen: (isListOpen: boolean) => void;
|
||||
};
|
||||
@@ -21,7 +22,7 @@ const ComboboxWrapper = ({
|
||||
inputSize,
|
||||
toggleIsListOpen,
|
||||
}: ComboboxWrapperProps) => {
|
||||
const { toggleOpenButtonRef, clearInput } = useInputContext();
|
||||
const { toggleOpenButtonRef, clearInput, readOnly } = useInputContext();
|
||||
|
||||
const wrapperRef = useRef<HTMLDivElement | null>(null);
|
||||
const [hasFocusWithin, setHasFocusWithin] = useState(false);
|
||||
@@ -55,6 +56,8 @@ const ComboboxWrapper = ({
|
||||
"navds-combobox--error": hasError,
|
||||
"navds-combobox--disabled": !!inputProps.disabled,
|
||||
"navds-combobox--focused": hasFocusWithin,
|
||||
"navds-combobox--readonly": readOnly,
|
||||
"navds-form-field--readonly": readOnly,
|
||||
},
|
||||
)}
|
||||
onFocus={onFocusInsideWrapper}
|
||||
|
||||
@@ -54,6 +54,7 @@ const FilteredOptions = () => {
|
||||
)}
|
||||
|
||||
{shouldRenderFilteredOptionsList && (
|
||||
/* biome-ignore lint/a11y/useFocusableInteractive: Interaction is not handeled by listbox itself. */
|
||||
<ul
|
||||
ref={setFilteredOptionsRef}
|
||||
role="listbox"
|
||||
|
||||
@@ -58,7 +58,8 @@ const FilteredOptionsProvider = ({
|
||||
useState<HTMLUListElement | null>(null);
|
||||
const virtualFocus = useVirtualFocus(filteredOptionsRef);
|
||||
const {
|
||||
inputProps: { "aria-describedby": partialAriaDescribedBy, id },
|
||||
inputProps: { "aria-describedby": partialAriaDescribedBy, id, disabled },
|
||||
readOnly,
|
||||
value,
|
||||
searchTerm,
|
||||
setValue,
|
||||
@@ -136,7 +137,10 @@ const FilteredOptionsProvider = ({
|
||||
|
||||
const toggleIsListOpen = useCallback(
|
||||
(newState?: boolean) => {
|
||||
virtualFocus.moveFocusToTop();
|
||||
if (disabled || readOnly) {
|
||||
return;
|
||||
}
|
||||
virtualFocus.resetFocus();
|
||||
if (newState ?? !isInternalListOpen) {
|
||||
setHideCaret(!!maxSelected?.isLimitReached);
|
||||
}
|
||||
@@ -147,6 +151,8 @@ const FilteredOptionsProvider = ({
|
||||
maxSelected?.isLimitReached,
|
||||
isInternalListOpen,
|
||||
setHideCaret,
|
||||
disabled,
|
||||
readOnly,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
export type VirtualFocusType = {
|
||||
activeElement: HTMLElement | undefined;
|
||||
@@ -10,6 +10,9 @@ export type VirtualFocusType = {
|
||||
moveFocusToElement: (id: string) => void;
|
||||
moveFocusToTop: () => void;
|
||||
moveFocusToBottom: () => void;
|
||||
moveFocusUpBy: (numberOfElements: number) => void;
|
||||
moveFocusDownBy: (numberOfElements: number) => void;
|
||||
resetFocus: () => void;
|
||||
};
|
||||
|
||||
const useVirtualFocus = (
|
||||
@@ -40,11 +43,6 @@ const useVirtualFocus = (
|
||||
: false;
|
||||
};
|
||||
|
||||
const _moveFocusAndScrollTo = (_element?: HTMLElement) => {
|
||||
setActiveElement(_element);
|
||||
_element?.scrollIntoView?.({ block: "nearest" });
|
||||
};
|
||||
|
||||
const moveFocusUp = () => {
|
||||
if (!activeElement) {
|
||||
return;
|
||||
@@ -55,14 +53,14 @@ const useVirtualFocus = (
|
||||
if (_currentIndex === 0) {
|
||||
setActiveElement(undefined);
|
||||
} else {
|
||||
_moveFocusAndScrollTo(elementAbove);
|
||||
setActiveElement(elementAbove);
|
||||
}
|
||||
};
|
||||
|
||||
const moveFocusDown = () => {
|
||||
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
|
||||
if (!activeElement) {
|
||||
_moveFocusAndScrollTo(elementsAbleToReceiveFocus[0]);
|
||||
setActiveElement(elementsAbleToReceiveFocus[0]);
|
||||
return;
|
||||
}
|
||||
const _currentIndex = elementsAbleToReceiveFocus.indexOf(activeElement);
|
||||
@@ -70,13 +68,17 @@ const useVirtualFocus = (
|
||||
return;
|
||||
}
|
||||
|
||||
_moveFocusAndScrollTo(elementsAbleToReceiveFocus[_currentIndex + 1]);
|
||||
setActiveElement(elementsAbleToReceiveFocus[_currentIndex + 1]);
|
||||
};
|
||||
|
||||
const moveFocusToTop = () => _moveFocusAndScrollTo(undefined);
|
||||
const resetFocus = () => setActiveElement(undefined);
|
||||
const moveFocusToTop = () => {
|
||||
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
|
||||
setActiveElement(elementsAbleToReceiveFocus[0]);
|
||||
};
|
||||
const moveFocusToBottom = () => {
|
||||
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
|
||||
return _moveFocusAndScrollTo(
|
||||
setActiveElement(
|
||||
elementsAbleToReceiveFocus[elementsAbleToReceiveFocus.length - 1],
|
||||
);
|
||||
};
|
||||
@@ -89,6 +91,32 @@ const useVirtualFocus = (
|
||||
}
|
||||
};
|
||||
|
||||
const moveFocusUpBy = (numberOfElements: number) => {
|
||||
if (!activeElement) {
|
||||
return;
|
||||
}
|
||||
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
|
||||
const currentIndex = elementsAbleToReceiveFocus.indexOf(activeElement);
|
||||
const newIndex = Math.max(currentIndex - numberOfElements, 0);
|
||||
setActiveElement(elementsAbleToReceiveFocus[newIndex]);
|
||||
};
|
||||
|
||||
const moveFocusDownBy = (numberOfElements: number) => {
|
||||
const elementsAbleToReceiveFocus = getElementsAbleToReceiveFocus();
|
||||
const currentIndex = activeElement
|
||||
? elementsAbleToReceiveFocus.indexOf(activeElement)
|
||||
: -1;
|
||||
const newIndex = Math.min(
|
||||
currentIndex + numberOfElements,
|
||||
elementsAbleToReceiveFocus.length - 1,
|
||||
);
|
||||
setActiveElement(elementsAbleToReceiveFocus[newIndex]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
activeElement?.scrollIntoView?.({ block: "nearest" });
|
||||
}, [activeElement]);
|
||||
|
||||
return {
|
||||
activeElement,
|
||||
getElementById,
|
||||
@@ -99,6 +127,9 @@ const useVirtualFocus = (
|
||||
moveFocusToElement,
|
||||
moveFocusToTop,
|
||||
moveFocusToBottom,
|
||||
moveFocusUpBy,
|
||||
moveFocusDownBy,
|
||||
resetFocus,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -32,6 +32,7 @@ interface Props {
|
||||
defaultValue: ComboboxProps["defaultValue"];
|
||||
description: ComboboxProps["description"];
|
||||
disabled: ComboboxProps["disabled"];
|
||||
readOnly: ComboboxProps["readOnly"];
|
||||
error: ComboboxProps["error"];
|
||||
errorId: ComboboxProps["errorId"];
|
||||
id: ComboboxProps["id"];
|
||||
@@ -48,6 +49,7 @@ const InputProvider = ({ children, value: props }: Props) => {
|
||||
defaultValue = "",
|
||||
description,
|
||||
disabled,
|
||||
readOnly,
|
||||
error,
|
||||
errorId,
|
||||
id: externalId,
|
||||
@@ -61,6 +63,7 @@ const InputProvider = ({ children, value: props }: Props) => {
|
||||
{
|
||||
description,
|
||||
disabled,
|
||||
readOnly,
|
||||
error,
|
||||
errorId,
|
||||
id: externalId,
|
||||
@@ -96,7 +99,7 @@ const InputProvider = ({ children, value: props }: Props) => {
|
||||
setInternalValue("");
|
||||
setSearchTerm("");
|
||||
},
|
||||
[externalOnChange, onClear, setInternalValue],
|
||||
[externalOnChange, onClear],
|
||||
);
|
||||
|
||||
const focusInput = useCallback(() => {
|
||||
|
||||
@@ -49,6 +49,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
setValue,
|
||||
hideCaret,
|
||||
setHideCaret,
|
||||
readOnly,
|
||||
} = useInputContext();
|
||||
const {
|
||||
selectedOptions,
|
||||
@@ -133,14 +134,6 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
case "Accept":
|
||||
onEnter(e);
|
||||
break;
|
||||
case "Home":
|
||||
toggleIsListOpen(false);
|
||||
virtualFocus.moveFocusToTop();
|
||||
break;
|
||||
case "End":
|
||||
toggleIsListOpen(true);
|
||||
virtualFocus.moveFocusToBottom();
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -149,6 +142,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
setIsMouseLastUsedInputDevice(false);
|
||||
if (readOnly) {
|
||||
return;
|
||||
}
|
||||
if (e.key === "Backspace") {
|
||||
if (value === "" && shouldShowSelectedOptions) {
|
||||
const lastSelectedOption =
|
||||
@@ -198,6 +194,24 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
}
|
||||
virtualFocus.moveFocusUp();
|
||||
}
|
||||
} else if (e.key === "Home") {
|
||||
e.preventDefault();
|
||||
virtualFocus.moveFocusToTop();
|
||||
} else if (e.key === "End") {
|
||||
e.preventDefault();
|
||||
if (virtualFocus.activeElement === null || !isListOpen) {
|
||||
toggleIsListOpen(true);
|
||||
}
|
||||
virtualFocus.moveFocusToBottom();
|
||||
} else if (e.key === "PageUp") {
|
||||
e.preventDefault();
|
||||
virtualFocus.moveFocusUpBy(6);
|
||||
} else if (e.key === "PageDown") {
|
||||
e.preventDefault();
|
||||
if (virtualFocus.activeElement === null || !isListOpen) {
|
||||
toggleIsListOpen(true);
|
||||
}
|
||||
virtualFocus.moveFocusDownBy(6);
|
||||
}
|
||||
},
|
||||
[
|
||||
@@ -214,6 +228,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
setValue,
|
||||
searchTerm,
|
||||
shouldShowSelectedOptions,
|
||||
readOnly,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -225,10 +240,9 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
} else if (filteredOptions.length === 0) {
|
||||
toggleIsListOpen(false);
|
||||
}
|
||||
virtualFocus.moveFocusToTop();
|
||||
onChange(newValue);
|
||||
},
|
||||
[filteredOptions.length, virtualFocus, onChange, toggleIsListOpen],
|
||||
[filteredOptions.length, onChange, toggleIsListOpen],
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -262,6 +276,7 @@ const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
aria-activedescendant={activeDecendantId}
|
||||
aria-describedby={ariaDescribedBy}
|
||||
aria-invalid={inputProps["aria-invalid"]}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -45,6 +45,7 @@ export const InputController = forwardRef<
|
||||
size = "medium",
|
||||
inputRef,
|
||||
toggleOpenButtonRef,
|
||||
readOnly,
|
||||
} = useInputContext();
|
||||
|
||||
const { activeDecendantId } = useFilteredOptionsContext();
|
||||
@@ -65,6 +66,7 @@ export const InputController = forwardRef<
|
||||
id={inputProps.id}
|
||||
ref={mergedInputRef}
|
||||
inputClassName={inputClassName}
|
||||
readOnly={readOnly}
|
||||
{...rest}
|
||||
/>
|
||||
) : (
|
||||
@@ -74,6 +76,7 @@ export const InputController = forwardRef<
|
||||
ref={mergedInputRef}
|
||||
inputClassName={inputClassName}
|
||||
shouldShowSelectedOptions={shouldShowSelectedOptions}
|
||||
readOnly={readOnly}
|
||||
{...rest}
|
||||
/>
|
||||
</SelectedOptions>
|
||||
|
||||
@@ -12,7 +12,7 @@ interface SelectedOptionsProps {
|
||||
|
||||
const Option = ({ option }: { option: ComboboxOption }) => {
|
||||
const { isMultiSelect, removeSelectedOption } = useSelectedOptionsContext();
|
||||
const { focusInput } = useInputContext();
|
||||
const { focusInput, readOnly, inputProps } = useInputContext();
|
||||
|
||||
const onClick = (e) => {
|
||||
e.stopPropagation();
|
||||
@@ -28,7 +28,13 @@ const Option = ({ option }: { option: ComboboxOption }) => {
|
||||
);
|
||||
}
|
||||
|
||||
return <Chips.Removable onClick={onClick}>{option.label}</Chips.Removable>;
|
||||
return readOnly || inputProps.disabled ? (
|
||||
<Chips.Toggle variant="neutral" checkmark={false} as="div">
|
||||
{option.label}
|
||||
</Chips.Toggle>
|
||||
) : (
|
||||
<Chips.Removable onClick={onClick}>{option.label}</Chips.Removable>
|
||||
);
|
||||
};
|
||||
|
||||
const SelectedOptions: React.FC<SelectedOptionsProps> = ({
|
||||
|
||||
@@ -105,9 +105,10 @@ const SelectedOptionsProvider = ({
|
||||
(!!maxSelected?.limit && selectedOptions.length >= maxSelected.limit) ||
|
||||
(!isMultiSelect && selectedOptions.length > 0);
|
||||
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: We explicitly want to run this effect when selectedOptions changes to match the view with the selected options.
|
||||
useEffect(() => {
|
||||
setHideCaret(isLimitReached);
|
||||
}, [selectedOptions, setHideCaret, isLimitReached]);
|
||||
}, [isLimitReached, selectedOptions, setHideCaret]);
|
||||
|
||||
const toggleOption = useCallback(
|
||||
(
|
||||
|
||||
@@ -291,4 +291,40 @@ describe("Render combobox", () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("has keyboard navigation", () => {
|
||||
test("for PageDown and PageUp", async () => {
|
||||
render(<App options={options} />);
|
||||
|
||||
const combobox = screen.getByRole("combobox", {
|
||||
name: "Hva er dine favorittfrukter?",
|
||||
});
|
||||
|
||||
const pressKey = async (key: string) => {
|
||||
await act(async () => {
|
||||
await userEvent.keyboard(`{${key}}`);
|
||||
});
|
||||
};
|
||||
|
||||
const hasVirtualFocus = (option: string) =>
|
||||
expect(combobox.getAttribute("aria-activedescendant")).toBe(
|
||||
screen.getByRole("option", { name: option }).id,
|
||||
);
|
||||
|
||||
await act(async () => {
|
||||
await userEvent.click(combobox);
|
||||
});
|
||||
|
||||
await pressKey("ArrowDown");
|
||||
hasVirtualFocus("apple");
|
||||
await pressKey("PageDown");
|
||||
hasVirtualFocus("mango");
|
||||
await pressKey("PageDown");
|
||||
hasVirtualFocus("watermelon");
|
||||
await pressKey("PageUp");
|
||||
hasVirtualFocus("mango");
|
||||
await pressKey("PageUp");
|
||||
hasVirtualFocus("apple");
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -67,6 +67,9 @@ Default.argTypes = {
|
||||
disabled: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
readOnly: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
isListOpen: {
|
||||
control: { type: "boolean" },
|
||||
},
|
||||
@@ -484,12 +487,50 @@ export const InModal: StoryFn = () => {
|
||||
};
|
||||
|
||||
export const Disabled: StoryFn = () => {
|
||||
const selectedOptionsMultiple = ["napoleonskake", "donut"];
|
||||
const selectedOptionsSingle = ["pushups"];
|
||||
return (
|
||||
<UNSAFE_Combobox
|
||||
options={options}
|
||||
label="Hva er dine favorittfrukter?"
|
||||
disabled
|
||||
/>
|
||||
<VStack gap="2">
|
||||
<UNSAFE_Combobox
|
||||
options={options}
|
||||
label="Hva er dine favorittfrukter?"
|
||||
description="Bare de fruktene som du spiser minst 5 av om dagen teller vi som en favorittfrukt."
|
||||
selectedOptions={selectedOptionsMultiple}
|
||||
isMultiSelect
|
||||
disabled
|
||||
/>
|
||||
<UNSAFE_Combobox
|
||||
options={options}
|
||||
label="Hva er din favoritthobby?"
|
||||
description="Her snakker vi ting du bruker minst én time på hver dag."
|
||||
selectedOptions={selectedOptionsSingle}
|
||||
disabled
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
export const Readonly: StoryFn = () => {
|
||||
const selectedOptionsMultiple = ["napoleonskake", "donut"];
|
||||
const selectedOptionsSingle = ["pushups"];
|
||||
return (
|
||||
<VStack gap="2">
|
||||
<UNSAFE_Combobox
|
||||
options={options}
|
||||
label="Hva er dine favorittfrukter?"
|
||||
description="Bare de fruktene som du spiser minst 5 av om dagen teller vi som en favorittfrukt."
|
||||
selectedOptions={selectedOptionsMultiple}
|
||||
isMultiSelect
|
||||
readOnly
|
||||
/>
|
||||
<UNSAFE_Combobox
|
||||
options={options}
|
||||
label="Hva er din favoritthobby?"
|
||||
description="Her snakker vi ting du bruker minst én time på hver dag."
|
||||
selectedOptions={selectedOptionsSingle}
|
||||
readOnly
|
||||
/>
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -525,6 +566,8 @@ export const Chromatic: StoryFn = () => {
|
||||
<WithError />
|
||||
<H2>Disabled</H2>
|
||||
<Disabled />
|
||||
<H2>Readonly</H2>
|
||||
<Readonly />
|
||||
</VStack>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -35,7 +35,7 @@ const CustomOptionsProvider = ({
|
||||
);
|
||||
focusInput();
|
||||
},
|
||||
[focusInput, setCustomOptions],
|
||||
[focusInput],
|
||||
);
|
||||
|
||||
const addCustomOption = useCallback(
|
||||
@@ -47,7 +47,7 @@ const CustomOptionsProvider = ({
|
||||
}
|
||||
focusInput();
|
||||
},
|
||||
[focusInput, isMultiSelect, setCustomOptions],
|
||||
[focusInput, isMultiSelect],
|
||||
);
|
||||
|
||||
const customOptionsState = {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import faker from "faker";
|
||||
import React from "react";
|
||||
import { expect, test } from "vitest";
|
||||
import { ConfirmationPanel } from ".";
|
||||
|
||||
test("omits error id from input", async () => {
|
||||
const label = faker.datatype.string();
|
||||
const label = "My label";
|
||||
|
||||
render(<ConfirmationPanel label={label} errorId="wat"></ConfirmationPanel>);
|
||||
|
||||
|
||||
@@ -2,11 +2,7 @@ import { Meta, StoryFn, StoryObj } from "@storybook/react";
|
||||
import { fireEvent, within } from "@storybook/test";
|
||||
import React from "react";
|
||||
import { ImageIcon } from "@navikt/aksel-icons";
|
||||
import {
|
||||
type FileObject,
|
||||
UNSAFE_FileUpload as FileUpload,
|
||||
type FilesPartitioned,
|
||||
} from ".";
|
||||
import { type FileObject, FileUpload, type FilesPartitioned } from ".";
|
||||
|
||||
const meta: Meta<typeof FileUpload.Dropzone> = {
|
||||
title: "ds-react/FileUpload/Dropzone",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Meta, StoryFn } from "@storybook/react";
|
||||
import React from "react";
|
||||
import { FileItem, UNSAFE_FileUpload as FileUpload } from ".";
|
||||
import { FileItem, FileUpload } from ".";
|
||||
import { VStack } from "../../layout/stack";
|
||||
|
||||
const meta: Meta<typeof FileUpload.Item> = {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Meta, StoryFn } from "@storybook/react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { UploadIcon } from "@navikt/aksel-icons";
|
||||
import { UNSAFE_FileUpload as FileUpload, FileUploadItemProps } from ".";
|
||||
import { FileUpload, FileUploadItemProps } from ".";
|
||||
import { Alert } from "../../alert";
|
||||
import { Button } from "../../button";
|
||||
import { VStack } from "../../layout/stack";
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"use client";
|
||||
export { default as UNSAFE_FileUpload } from "./FileUpload";
|
||||
export { default as UNSAFE_FileUploadDropzone } from "./parts/dropzone/Dropzone";
|
||||
export { default as FileUpload } from "./FileUpload";
|
||||
export { default as FileUploadDropzone } from "./parts/dropzone/Dropzone";
|
||||
export { type FileUploadDropzoneProps } from "./parts/dropzone/dropzone.types";
|
||||
export {
|
||||
default as UNSAFE_FileUploadTrigger,
|
||||
default as FileUploadTrigger,
|
||||
type FileUploadTriggerProps,
|
||||
} from "./parts/Trigger";
|
||||
export {
|
||||
@@ -15,7 +15,7 @@ export {
|
||||
type FileRejectionReason,
|
||||
} from "./FileUpload.types";
|
||||
export {
|
||||
default as UNSAFE_FileUploadItem,
|
||||
default as FileUploadItem,
|
||||
type FileUploadItemProps,
|
||||
} from "./parts/item/Item";
|
||||
export { type FileItem, type FileMetadata } from "./parts/item/Item.types";
|
||||
@@ -1,14 +1,13 @@
|
||||
import { render, screen } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import faker from "faker";
|
||||
import React from "react";
|
||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { Radio, RadioGroup } from ".";
|
||||
|
||||
const value1 = faker.datatype.string();
|
||||
const label1 = faker.datatype.string();
|
||||
const value2 = faker.datatype.string();
|
||||
const label2 = faker.datatype.string();
|
||||
const value1 = "My first value";
|
||||
const label1 = "World's best radio label";
|
||||
const value2 = "Life changing value";
|
||||
const label2 = "Radio label of the year";
|
||||
|
||||
const Group = (props) => (
|
||||
<RadioGroup {...props} legend="legend">
|
||||
|
||||
@@ -41,8 +41,8 @@ export const useRadio = (props: RadioProps) => {
|
||||
if (readOnly) {
|
||||
return;
|
||||
}
|
||||
props.onChange && props.onChange(e);
|
||||
radioGroup?.onChange && radioGroup.onChange(props.value);
|
||||
props.onChange?.(e);
|
||||
radioGroup?.onChange?.(props.value);
|
||||
},
|
||||
onClick: (e) => {
|
||||
if (readOnly) {
|
||||
|
||||
@@ -144,7 +144,7 @@ export const Search = forwardRef<HTMLInputElement, SearchProps>(
|
||||
(event: SearchClearEvent) => {
|
||||
onClear?.(event);
|
||||
handleChange("");
|
||||
searchRef.current && searchRef.current?.focus?.();
|
||||
searchRef.current?.focus?.();
|
||||
},
|
||||
[handleChange, onClear],
|
||||
);
|
||||
|
||||
@@ -122,7 +122,7 @@ export const Switch = forwardRef<HTMLInputElement, SwitchProps>(
|
||||
return;
|
||||
}
|
||||
setChecked(e.target.checked);
|
||||
props.onChange && props.onChange(e);
|
||||
props.onChange?.(e);
|
||||
}}
|
||||
onClick={(e) => {
|
||||
if (readOnly) {
|
||||
|
||||
@@ -49,7 +49,7 @@ Default.argTypes = {
|
||||
export const Open: StoryFn = () => {
|
||||
const ref = useRef<HTMLButtonElement | null>(null);
|
||||
useEffect(() => {
|
||||
ref.current && ref.current.click();
|
||||
ref.current?.click();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
export { Accordion, type AccordionProps } from "./accordion";
|
||||
export { ActionMenu, type ActionMenuProps } from "./overlays/action-menu";
|
||||
export { Alert, type AlertProps } from "./alert";
|
||||
export { Button, type ButtonProps } from "./button";
|
||||
export { Chat, type ChatProps } from "./chat";
|
||||
@@ -127,7 +128,7 @@ export {
|
||||
export { ErrorSummary, type ErrorSummaryProps } from "./form/error-summary";
|
||||
export { Fieldset, type FieldsetProps } from "./form/fieldset";
|
||||
export {
|
||||
UNSAFE_FileUpload,
|
||||
FileUpload,
|
||||
type FileAccepted,
|
||||
type FileItem,
|
||||
type FileMetadata,
|
||||
|
||||
@@ -5,6 +5,9 @@ import { getResponsiveProps, getResponsiveValue } from "../utilities/css";
|
||||
import { ResponsiveProp, SpacingScale } from "../utilities/types";
|
||||
|
||||
export type PrimitiveProps = {
|
||||
/**
|
||||
* @private Hides prop from documentation
|
||||
*/
|
||||
className?: string;
|
||||
/**
|
||||
* Padding around children.
|
||||
|
||||
@@ -19,42 +19,41 @@ import {
|
||||
SurfaceColorToken,
|
||||
} from "../utilities/types";
|
||||
|
||||
export type BoxProps = PrimitiveProps &
|
||||
PrimitiveAsChildProps &
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
/**
|
||||
* CSS `background-color` property.
|
||||
* Accepts a [background/surface color token](https://aksel.nav.no/grunnleggende/styling/design-tokens#afff774dad80).
|
||||
*/
|
||||
background?: BackgroundColorToken | SurfaceColorToken;
|
||||
/**
|
||||
* CSS `border-color` property.
|
||||
* Accepts a [border color token](https://aksel.nav.no/grunnleggende/styling/design-tokens#adb1767e2f87).
|
||||
*/
|
||||
borderColor?: BorderColorToken;
|
||||
/**
|
||||
* CSS `border-radius` property.
|
||||
* Accepts a [radius token](https://aksel.nav.no/grunnleggende/styling/design-tokens#6d79c5605d31)
|
||||
* or an object of radius tokens for different breakpoints.
|
||||
* @example
|
||||
* borderRadius='full'
|
||||
* borderRadius='0 full large small'
|
||||
* borderRadius={{xs: 'small large', sm: '0', md: 'large', lg: 'full'}}
|
||||
*/
|
||||
borderRadius?: ResponsiveProp<SpaceDelimitedAttribute<BorderRadiiToken>>;
|
||||
/**
|
||||
* CSS `border-width` property. If this is not set there will be no border.
|
||||
* @example
|
||||
* borderWidth='2'
|
||||
* borderWidth='1 2 3 4'
|
||||
*/
|
||||
borderWidth?: SpaceDelimitedAttribute<"0" | "1" | "2" | "3" | "4" | "5">;
|
||||
/** Shadow on box. Accepts a shadow token.
|
||||
* @example
|
||||
* shadow='small'
|
||||
*/
|
||||
shadow?: ShadowToken;
|
||||
};
|
||||
export type BoxProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
/**
|
||||
* CSS `background-color` property.
|
||||
* Accepts a [background/surface color token](https://aksel.nav.no/grunnleggende/styling/design-tokens#afff774dad80).
|
||||
*/
|
||||
background?: BackgroundColorToken | SurfaceColorToken;
|
||||
/**
|
||||
* CSS `border-color` property.
|
||||
* Accepts a [border color token](https://aksel.nav.no/grunnleggende/styling/design-tokens#adb1767e2f87).
|
||||
*/
|
||||
borderColor?: BorderColorToken;
|
||||
/**
|
||||
* CSS `border-radius` property.
|
||||
* Accepts a [radius token](https://aksel.nav.no/grunnleggende/styling/design-tokens#6d79c5605d31)
|
||||
* or an object of radius tokens for different breakpoints.
|
||||
* @example
|
||||
* borderRadius='full'
|
||||
* borderRadius='0 full large small'
|
||||
* borderRadius={{xs: 'small large', sm: '0', md: 'large', lg: 'full'}}
|
||||
*/
|
||||
borderRadius?: ResponsiveProp<SpaceDelimitedAttribute<BorderRadiiToken>>;
|
||||
/**
|
||||
* CSS `border-width` property. If this is not set there will be no border.
|
||||
* @example
|
||||
* borderWidth='2'
|
||||
* borderWidth='1 2 3 4'
|
||||
*/
|
||||
borderWidth?: SpaceDelimitedAttribute<"0" | "1" | "2" | "3" | "4" | "5">;
|
||||
/** Shadow on box. Accepts a shadow token.
|
||||
* @example
|
||||
* shadow='small'
|
||||
*/
|
||||
shadow?: ShadowToken;
|
||||
} & PrimitiveProps &
|
||||
PrimitiveAsChildProps;
|
||||
|
||||
/**
|
||||
* Foundational Layout-primitive for generic encapsulation & styling.
|
||||
|
||||
@@ -10,33 +10,32 @@ import { PrimitiveAsChildProps } from "../base/PrimitiveAsChildProps";
|
||||
import { getResponsiveProps, getResponsiveValue } from "../utilities/css";
|
||||
import { ResponsiveProp, SpacingScale } from "../utilities/types";
|
||||
|
||||
export type HGridProps = PrimitiveProps &
|
||||
PrimitiveAsChildProps &
|
||||
React.HTMLAttributes<HTMLDivElement> & {
|
||||
/**
|
||||
* Number of columns to display. Can be a number, a string, or an object with values for specific breakpoints.
|
||||
* Sets `grid-template-columns`, so `fr`, `minmax` etc. works.
|
||||
* @example
|
||||
* columns={{ sm: 1, md: 1, lg: "1fr auto", xl: "1fr auto"}}
|
||||
* columns={3}
|
||||
* columns="repeat(3, minmax(0, 1fr))"
|
||||
*/
|
||||
columns?: ResponsiveProp<number | string>;
|
||||
/**
|
||||
* Spacing between columns.
|
||||
* Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213)
|
||||
* or an object of spacing tokens for different breakpoints.
|
||||
* @example
|
||||
* gap="6"
|
||||
* gap="8 4"
|
||||
* gap={{ sm: "2", md: "2", lg: "6", xl: "6"}}
|
||||
*/
|
||||
gap?: ResponsiveProp<SpacingScale | `${SpacingScale} ${SpacingScale}`>;
|
||||
/**
|
||||
* Vertical alignment of children. Elements will by default stretch to the height of parent-element.
|
||||
*/
|
||||
align?: "start" | "center" | "end";
|
||||
};
|
||||
export type HGridProps = React.HTMLAttributes<HTMLDivElement> & {
|
||||
/**
|
||||
* Number of columns to display. Can be a number, a string, or an object with values for specific breakpoints.
|
||||
* Sets `grid-template-columns`, so `fr`, `minmax` etc. works.
|
||||
* @example
|
||||
* columns={{ sm: 1, md: 1, lg: "1fr auto", xl: "1fr auto"}}
|
||||
* columns={3}
|
||||
* columns="repeat(3, minmax(0, 1fr))"
|
||||
*/
|
||||
columns?: ResponsiveProp<number | string>;
|
||||
/**
|
||||
* Spacing between columns.
|
||||
* Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213)
|
||||
* or an object of spacing tokens for different breakpoints.
|
||||
* @example
|
||||
* gap="6"
|
||||
* gap="8 4"
|
||||
* gap={{ sm: "2", md: "2", lg: "6", xl: "6"}}
|
||||
*/
|
||||
gap?: ResponsiveProp<SpacingScale | `${SpacingScale} ${SpacingScale}`>;
|
||||
/**
|
||||
* Vertical alignment of children. Elements will by default stretch to the height of parent-element.
|
||||
*/
|
||||
align?: "start" | "center" | "end";
|
||||
} & PrimitiveProps &
|
||||
PrimitiveAsChildProps;
|
||||
/**
|
||||
* Horizontal Grid Primitive with dynamic columns and gap based on breakpoints.
|
||||
*
|
||||
|
||||
@@ -11,60 +11,59 @@ import { PrimitiveAsChildProps } from "../base/PrimitiveAsChildProps";
|
||||
import { getResponsiveProps, getResponsiveValue } from "../utilities/css";
|
||||
import { ResponsiveProp, SpacingScale } from "../utilities/types";
|
||||
|
||||
export type StackProps = PrimitiveProps &
|
||||
PrimitiveAsChildProps &
|
||||
HTMLAttributes<HTMLDivElement> & {
|
||||
/**
|
||||
* CSS `justify-content` property.
|
||||
*
|
||||
* @example
|
||||
* justify='center'
|
||||
* justify={{xs: 'start', sm: 'center', md: 'end', lg: 'space-around', xl: 'space-between'}}
|
||||
*/
|
||||
justify?: ResponsiveProp<
|
||||
| "start"
|
||||
| "center"
|
||||
| "end"
|
||||
| "space-around"
|
||||
| "space-between"
|
||||
| "space-evenly"
|
||||
>;
|
||||
/**
|
||||
* CSS `align-items` property.
|
||||
* @default "stretch"
|
||||
*
|
||||
* @example
|
||||
* align='center'
|
||||
* align={{xs: 'start', sm: 'center', md: 'end', lg: 'baseline', xl: 'stretch'}}
|
||||
*/
|
||||
align?: ResponsiveProp<"start" | "center" | "end" | "baseline" | "stretch">;
|
||||
/**
|
||||
* Sets the CSS `flex-wrap` property.
|
||||
*/
|
||||
wrap?: boolean;
|
||||
/**
|
||||
* CSS `gap` property.
|
||||
* Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213)
|
||||
* or an object of spacing tokens for different breakpoints.
|
||||
*
|
||||
* @example
|
||||
* gap='4'
|
||||
* gap='8 4'
|
||||
* gap={{xs: '2', sm: '3', md: '4', lg: '5', xl: '6'}}
|
||||
*/
|
||||
gap?: ResponsiveProp<SpacingScale | `${SpacingScale} ${SpacingScale}`>;
|
||||
/**
|
||||
* CSS `flex-direction` property.
|
||||
* @default "row"
|
||||
*
|
||||
* @example
|
||||
* direction='row'
|
||||
* direction={{xs: 'row', sm: 'column'}}
|
||||
*/
|
||||
direction?: ResponsiveProp<
|
||||
"row" | "column" | "row-reverse" | "column-reverse"
|
||||
>;
|
||||
};
|
||||
export type StackProps = HTMLAttributes<HTMLDivElement> & {
|
||||
/**
|
||||
* CSS `justify-content` property.
|
||||
*
|
||||
* @example
|
||||
* justify='center'
|
||||
* justify={{xs: 'start', sm: 'center', md: 'end', lg: 'space-around', xl: 'space-between'}}
|
||||
*/
|
||||
justify?: ResponsiveProp<
|
||||
| "start"
|
||||
| "center"
|
||||
| "end"
|
||||
| "space-around"
|
||||
| "space-between"
|
||||
| "space-evenly"
|
||||
>;
|
||||
/**
|
||||
* CSS `align-items` property.
|
||||
* @default "stretch"
|
||||
*
|
||||
* @example
|
||||
* align='center'
|
||||
* align={{xs: 'start', sm: 'center', md: 'end', lg: 'baseline', xl: 'stretch'}}
|
||||
*/
|
||||
align?: ResponsiveProp<"start" | "center" | "end" | "baseline" | "stretch">;
|
||||
/**
|
||||
* Sets the CSS `flex-wrap` property.
|
||||
*/
|
||||
wrap?: boolean;
|
||||
/**
|
||||
* CSS `gap` property.
|
||||
* Accepts a [spacing token](https://aksel.nav.no/grunnleggende/styling/design-tokens#0cc9fb32f213)
|
||||
* or an object of spacing tokens for different breakpoints.
|
||||
*
|
||||
* @example
|
||||
* gap='4'
|
||||
* gap='8 4'
|
||||
* gap={{xs: '2', sm: '3', md: '4', lg: '5', xl: '6'}}
|
||||
*/
|
||||
gap?: ResponsiveProp<SpacingScale | `${SpacingScale} ${SpacingScale}`>;
|
||||
/**
|
||||
* CSS `flex-direction` property.
|
||||
* @default "row"
|
||||
*
|
||||
* @example
|
||||
* direction='row'
|
||||
* direction={{xs: 'row', sm: 'column'}}
|
||||
*/
|
||||
direction?: ResponsiveProp<
|
||||
"row" | "column" | "row-reverse" | "column-reverse"
|
||||
>;
|
||||
} & PrimitiveProps &
|
||||
PrimitiveAsChildProps;
|
||||
|
||||
export const Stack: OverridableComponent<StackProps, HTMLDivElement> =
|
||||
forwardRef(
|
||||
|
||||
@@ -1,15 +1,27 @@
|
||||
import cl from "clsx";
|
||||
import React, { forwardRef, useContext } from "react";
|
||||
import { BodyShort, Heading } from "../typography";
|
||||
import { useId } from "../util/hooks";
|
||||
import { BodyLong, BodyShort, Heading, HeadingProps } from "../typography";
|
||||
import { ListItem } from "./ListItem";
|
||||
import { ListContext } from "./context";
|
||||
import { ListProps } from "./types";
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
import type { ListItemProps, ListProps } from "./types";
|
||||
|
||||
const headingSizeMap: Record<
|
||||
Exclude<ListProps["size"], undefined>,
|
||||
HeadingProps["size"]
|
||||
> = {
|
||||
small: "xsmall",
|
||||
medium: "small",
|
||||
large: "medium",
|
||||
};
|
||||
|
||||
export interface ListComponent
|
||||
extends React.ForwardRefExoticComponent<
|
||||
ListProps & React.RefAttributes<HTMLDivElement>
|
||||
> {
|
||||
/**
|
||||
* @see 🏷️ {@link ListItemProps}
|
||||
*/
|
||||
Item: typeof ListItem;
|
||||
}
|
||||
|
||||
@@ -42,47 +54,31 @@ export const List = forwardRef<HTMLDivElement, ListProps>(
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
const ariaId = useId();
|
||||
const { size: contextSize } = useContext(ListContext);
|
||||
|
||||
const { isNested, size: _size } = useContext(ListContext);
|
||||
|
||||
const listSize = size ?? _size ?? "medium";
|
||||
const listSize = size ?? contextSize;
|
||||
return (
|
||||
<ListContext.Provider
|
||||
value={{
|
||||
listType: ListTag,
|
||||
isNested: isNested === null ? false : true,
|
||||
size: listSize,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
<BodyLong
|
||||
as="div"
|
||||
{...rest}
|
||||
size={listSize}
|
||||
ref={ref}
|
||||
className={cl("navds-list", `navds-list--${listSize}`, className, {
|
||||
"navds-list--nested": isNested === null ? false : true,
|
||||
})}
|
||||
className={cl("navds-list", `navds-list--${listSize}`, className)}
|
||||
>
|
||||
{title && (
|
||||
<Heading
|
||||
id={`tittel-${ariaId}`}
|
||||
size={listSize === "medium" ? "small" : "xsmall"}
|
||||
as={headingTag}
|
||||
>
|
||||
<Heading size={headingSizeMap[listSize]} as={headingTag}>
|
||||
{title}
|
||||
</Heading>
|
||||
)}
|
||||
{description && (
|
||||
<BodyShort size={listSize} id={`description-${ariaId}`}>
|
||||
{description}
|
||||
</BodyShort>
|
||||
)}
|
||||
<ListTag
|
||||
aria-labelledby={title && `tittel-${ariaId}`}
|
||||
aria-describedby={description && `description-${ariaId}`}
|
||||
>
|
||||
{children}
|
||||
</ListTag>
|
||||
</div>
|
||||
{description && <BodyShort size={listSize}>{description}</BodyShort>}
|
||||
<ListTag role="list">{children}</ListTag>
|
||||
</BodyLong>
|
||||
</ListContext.Provider>
|
||||
);
|
||||
},
|
||||
|
||||
Loaded 100 of 330 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user