Compare commits

...
Author SHA1 Message Date
github-actions[bot]andgithub-actions[bot] 6cb25e44de Version Packages (#2172)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-17 14:24:27 +02:00
Ken 5812d6ee87 📝 Oppdatert changesets 2023-08-17 14:14:01 +02:00
Ken a402777c70 🐛 Alignet alert-ikon med tekst (#2179) 2023-08-17 14:02:56 +02:00
Vegard Haugstvedt d4064bd461 Call Combobox.onClear when clearin input field programatically (#2183) 2023-08-17 14:00:05 +02:00
KenandHalvor Haugan 1c482e03b8 Oppdatert list: Small-variant, bugfixes (#2168)
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2023-08-17 13:56:59 +02:00
Vegard HaugstvedtandKen 608e1f3944 Feature/combobox error message (#2182)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2023-08-17 13:26:10 +02:00
Ken 107d6f3d93 CopyButton prop for iconPosition (#2173) 2023-08-17 13:21:54 +02:00
Ken 09d41e9b6d 🎨 Checkbox checkmark er nå SVG, ikke base64 (#2171) 2023-08-17 12:49:33 +02:00
Ken ebacbddd44 🐛 Reverserte major-update av stylelint 2023-08-17 12:47:34 +02:00
Ken e58b072c84 🎨 Oppdatert storybook, fikset chromatic-PR kommentar 2023-08-17 12:02:03 +02:00
Lars Eirik Korsgaard HansenandKen e7cb3753a0 🐛 fix: Combobox , SingleSelect: allow custom options (#2180)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2023-08-17 11:38:31 +02:00
Lars Eirik Korsgaard Hansen 7486ba11f9 🐛 Combobox, SingleSelect: close dropdown menu upon selecting option (#2177)
* isAddedByUser changed to isCustomOption to match language used elsewhere

* list closes when choosing option on SingleSelect

* changeset
2023-08-16 15:43:59 +02:00
Vegard Haugstvedt cf01a6e162 Bugfix/combobox.on clear is fired more times than expected (#2170) 2023-08-16 10:45:02 +02:00
Ken a67f95433f 🐛 Fikset dodont tekst og ikon for god-praksis 2023-08-16 10:44:41 +02:00
github-actions[bot]andgithub-actions[bot] dd03078e33 Version Packages (#2169)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-15 14:12:40 +02:00
Halvor Haugan 72321e3846 💄 ToggleGroup: fjern semibold fra selected button (#2167) 2023-08-15 13:42:20 +02:00
Ken 1cd8ac93ed 🎨 Oppdatert ikonside med forslag fra Slack (#2164) 2023-08-15 10:50:54 +02:00
Ken 48d6f14103 🐛 Fikset for restriktiv validering av sidetittel i sanity 2023-08-15 10:15:11 +02:00
github-actions[bot]andgithub-actions[bot] 44da7fcdc6 Version Packages (#2160)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-14 14:43:22 +02:00
Ken e68ab5cb2c 🎨 Fikset hardkodet border-radius i diverse komponenter (#2159) 2023-08-14 14:40:47 +02:00
Ken c721b392f9 ♻️ Chores Sanity, Storybook
- Oppdatert Storybook, Sanity
- Fikset validering for InputWithCounter i Sanity
- Fikset Accordion-stories rekkefølge
- La til egne knapper for Desktop/mobil-view for eksempler
- Bruker DS-knapper i kode-eksempler på Aksel
2023-08-14 13:40:10 +02:00
Ken 1d0d0ce665 Merge branch 'main' of https://github.com/navikt/aksel 2023-08-13 20:56:42 +02:00
Ken 9d02940543 🔥 Fjernet console.log 2023-08-13 20:50:54 +02:00
Ken 7ff63a2c63 🐛 Fikset validering for sidetittel i sanity 2023-08-13 20:50:13 +02:00
github-actions[bot]andgithub-actions[bot] 49f33a78d8 Version Packages (#2151)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-11 15:59:23 +02:00
Ken 06aaec70f0 📝 Oppdatert changeset for HGrid 2023-08-11 15:43:40 +02:00
Ken 5fbf230e47 🎨 Justert antall viste artikkler i masonary 2023-08-11 13:24:26 +02:00
Vegard Haugstvedt 53265c247e It is not possible to close combobox dropdown after clicking a chip (#2155)
* Fixes bug where it is not possible to close dropdown by pressing escape or clicking outside the combobox after clicking the chips to de-select an option

In short, the focus were lost/reset when the chip disappears, but now is reset to the input instead.
Also fixed issue where clicking the chip while virtual focus is in the dropdown caused a click on both the option and the chip, causing it to be de-selected and instantly re-selected.

* Updated "remove selected"-test to also check that closing the dropdown after clicking a chip is possible.

* Added changeset
2023-08-11 12:38:22 +02:00
Ken d78f33df79 🚚 Markert gammelt grid som deprecated (#2153) 2023-08-11 12:37:02 +02:00
Ken 4375c55ab6 📝 Oppdatert storybook template i workflow 2023-08-11 09:45:02 +02:00
Christian SkrøvsethandKen 0de8238ecf Add semibold (600) fonts (#2150)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2023-08-11 09:01:59 +02:00
KenandHalvor Haugan 647b14a238 [Primitives] HGrid (#1838)
Co-authored-by: Halvor Haugan <83693529+HalvorHaugan@users.noreply.github.com>
2023-08-10 15:38:58 +02:00
Ken 64901adccb 🎨 Oppdatert List-dokumentasjon, fikset bruk av ikoner med text (#2149) 2023-08-10 13:06:48 +02:00
Ken 49ddb2094c Hotfix: Fikset visited state for Link og vertical-align for SVG i tailwind 2023-08-09 17:06:15 +02:00
github-actions[bot]andgithub-actions[bot] 8a5f209735 Version Packages (#2148)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-09 16:30:12 +02:00
Ken d3bd1202ed Hotfix: Fikset visited state for Link og SVG-reset ved bruk av tailwind 2023-08-09 16:19:49 +02:00
github-actions[bot]andgithub-actions[bot] 4bf6027518 Version Packages (#2147)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-09 15:34:18 +02:00
Halvor Haugan c8e203c523 ✏️ Fiks skrivefeil i suggestionblock (#2146) 2023-08-09 15:15:41 +02:00
267799ca0e update links (#2093)
* style links with inline

* tweak css spacing for inline links with SVG

* add break-all prop to link

* wrap link in span to allow for spacing

* more obvious spacing example

* remove spacing prop from link

* implement pr review comments

* add random icon visual fuzz testing in stories

* remove console.log

* changeset

* add examples to aksel.nav.no

* 🎨 make inline-flex version opt-in, improve prop naming

* 🐛 Fixed comments

* 🎨 Oppdatert props

* 🎨 refactor links, add examples (remove underline & inline)

* change is no longer major (pr review comment)

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

* cleaner changeset (pr review comment)

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

* norwegian changeset (pr review comment)

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

* update changeset (pr review comment)

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

* use neutral token for neutral link variant

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

* use neutral token for neutral link variant

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>

* Update .changeset/proud-tigers-share.md

---------

Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
Co-authored-by: Ken <ken.aleksander@gmail.com>
2023-08-09 15:00:44 +02:00
github-actions[bot]andgithub-actions[bot] 42934af3a1 Version Packages (#2145)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-09 14:38:31 +02:00
Vegard Haugstvedt eff8a902c5 Bugfix/combobox bugfixes and tweaks (#2144)
* Clear input field when clicking option in multiselect combobox

Makes the behaviour consistent when clicking with a mouse and when selecting an option with keyboard.

* MultiSelect with "add new" example should actually have the addnewValues prop set...

* Expose through onToggleSelected whether selected values in combobox are added by the user or not.

* Add the additional affordance of bold font-weight for selected options in combobox.

Done to account for screen magnifiers on screens that don't show the background color, where the checkmark is out-of-sight.

* Avoid "jumping" when selecting option in multiselect, stemming from the chips being slightly larger than the padding allowed for (equal to the width of the border)

* Added changeset and updated yarn.lock

* Do not use focus-visible inside fallback for when focus-visible is not supported

* Add fallback focus-styling using JS and a helper class for browsers not supporting :has (Firefox)

* Fix for onBlur in ComboboxWrapper. Without tabindex="-1" event.relatedTarget in onBlur will be null, and we can't check that focus has entirely moved outside the Combobox.
2023-08-09 14:15:26 +02:00
Ken d34a8f45ef 🐛 aria-live fix for button (#2143) 2023-08-09 11:01:45 +02:00
github-actions[bot]andgithub-actions[bot] d1ceb40439 Version Packages (#2141)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-07 13:58:17 +02:00
e033b34c3b Table component: adding click event on expandable table rows (#2127)
Co-authored-by: Vegard Haugstvedt <it.vegard@gmail.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2023-08-07 13:42:07 +02:00
Halvor Haugan de3991d0f0 📝 Fiks skrivefeil i Select-eksempler (#2140) 2023-08-04 18:30:41 +02:00
github-actions[bot]andgithub-actions[bot] 9641215506 Version Packages (#2139)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-04 13:43:34 +02:00
Ken 8a82e1c1f6 [Ny komponent] Stack, Spacer (#2040) 2023-08-04 13:13:46 +02:00
github-actions[bot]andgithub-actions[bot] a2dfc8699b Version Packages (#2138)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-04 12:33:33 +02:00
Ken 492cfc1801 💄 Oppdatert bruk av REM for font-scaling fix (#2126) 2023-08-04 11:33:05 +02:00
Ken 9ca5932fed ⬆️ Oppdatert dependencies for dependabot warnings (#2134) 2023-08-04 11:18:24 +02:00
Vebjørn NordbyandKen 5950673fbf Legger til htmlInputType time på TextField-type (#2137)
Co-authored-by: Ken <ken.aleksander@gmail.com>
2023-08-04 09:15:17 +02:00
Ken 5b42ff07ee 🐛 Fikset oppdateringer av prop-docs på Aksel.nav.no 2023-08-03 13:41:20 +02:00
github-actions[bot]andgithub-actions[bot] 09ba52ea76 Version Packages (#2132)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-08-03 13:30:24 +02:00
a82bccf707 Removable chips type now defaults to button (#2124)
* Removable chips type now defaults to button

* 📝 Changeset

* 📝 Changeset update

* Remove OverridableComponent from Chips.Removable. This component should always be a button, and it is not currently used in any other way.

The reason for making this change now, is that we put type="button" on the Chip.Removable to prevent a submit event when the chip is clicked inside a form.

---------

Co-authored-by: Ken <ken.aleksander@gmail.com>
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
Co-authored-by: Vegard Haugstvedt <it.vegard@gmail.com>
2023-08-03 11:41:31 +02:00
Ken 89ae778c24 🐛 Fikset lasting av aksel-eksempler i storybook 2023-08-03 10:53:25 +02:00
Ken 5b2669d35d ⬆️ Oppdatert storybook til 7.2 (#2128) 2023-08-02 15:35:28 +02:00
Ken 8a82bb4a96 🐛 Fikset layout-bug på komponentsider for enkelte nettlesere 2023-08-02 12:43:19 +02:00
Ken 7871e0f95c 📝 Oppdatert datepicker-eksempler med formatering (#2125) 2023-08-02 12:37:14 +02:00
dependabot[bot] 3182888a07 ⬆️ Bump word-wrap from 1.2.3 to 1.2.4 (#2115)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-08-02 10:39:55 +02:00
Mathias S. Müller f2742da085 Legg til manglende størrelsesbeskrivelse (#2121) 2023-07-27 14:59:08 +02:00
github-actions[bot]andgithub-actions[bot] 92721aefa0 Version Packages (#2119)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-07-21 11:47:05 +02:00
Ken e6fdfdc9b4 🐛 Fikset stepper-bug (#2116) 2023-07-20 19:50:56 +02:00
github-actions[bot]andgithub-actions[bot] 93975bc935 Version Packages (#2118)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-07-19 20:14:53 +02:00
Ken 3a0dac3997 🐛 Fikset datepicker-bug for dynamsik oppdatering av 'month' (#2117) 2023-07-19 20:12:14 +02:00
Andreas Nordahl 1d43710af0 Remove preconnect link to fonts.googleapis.com(#2114) 2023-07-19 17:45:55 +02:00
github-actions[bot]andgithub-actions[bot] 6df7c78e3c Version Packages (#2111)
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2023-07-17 22:35:24 +02:00
Andreas NordahlandKen 476d79e487 Expose timeline axis label templates as a prop (#2109)
Co-authored-by: Ken <ken.aleksander@gmail.com>
2023-07-17 22:32:16 +02:00
dependabot[bot]andLars Eirik Korsgaard Hansen 238a715c92 ⬆️ Bump semver from 5.7.1 to 5.7.2 (#2107)
Bumps [semver](https://github.com/npm/node-semver) from 5.7.1 to 5.7.2.
- [Release notes](https://github.com/npm/node-semver/releases)
- [Changelog](https://github.com/npm/node-semver/blob/v5.7.2/CHANGELOG.md)
- [Commits](https://github.com/npm/node-semver/compare/v5.7.1...v5.7.2)

---
updated-dependencies:
- dependency-name: semver
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Lars Eirik Korsgaard Hansen <71271458+larseirikhansen@users.noreply.github.com>
2023-07-14 15:06:55 +02:00
Lars Eirik Korsgaard Hansen 0fadd33e3e Combobox fixes (#2112)
Combobox post-release tweaks

* Add dynamic height to examples because of the dropdown

* Examples were not working: remove controlled prop for uncontrolled Comboboxes

* Add an example with controlled Combobox
2023-07-14 11:08:47 +02:00
Ken eadc0bce13 📝 Oppdatert changeset for bedre endringslogg 2023-07-13 15:50:29 +02:00
Lars Eirik Korsgaard Hansen 678db9b11b Update spotty-onions-cough.md 2023-07-13 15:39:40 +02:00
Lars Eirik Korsgaard Hansen e3027cc196 ✨ Ny komponent: Combobox (#1868)
✨Ny komponent: Combobox 🎉 

Combobox er en ny komponent som kombinerer tekstfelt og nedtrekksliste. 
- Velg mellom predefinerte alternativer i nedtrekkslisten
- Bruk input-feltet som filter for alternativene i nedtrekkslisten.
- Legg inn nye svar, hvis svaret du ikke finnes i listen
- Velg mellom SingleSelect eller MultiSelect-versjon for henholdsvis ett og flere valg. 
- Autocomplete svar i inputfeltet når man velger SingleSelect

Dokumentasjonen finner du på https://aksel.nav.no/komponenter/core/combobox
2023-07-13 15:17:05 +02:00
Ken ac0fd790e3 ⚰️ Fjernet ubrukt metrics-løsning (#2098) 2023-07-06 17:42:38 +02:00
SjurandKen c344c9beb4 Update removable.tsx (#2101)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2023-07-06 17:22:24 +02:00
SjurandKen 5cb5dea4ff Update toggle-neutral.tsx (#2100)
Co-authored-by: Ken <26967723+KenAJoh@users.noreply.github.com>
2023-07-06 14:42:51 +02:00
Sjur 054c63b161 Update toggle.tsx (#2099) 2023-07-06 14:42:12 +02:00
Julian Nymark f60ba32f97 split default example into separate examples (#2097)
* split default example into separate examples

* also keep the default

* remove unnecessary div wrapper for single elements
2023-07-06 13:28:26 +02:00
Ken 54811b394d ✨ Eksterne sider åpnes nå i ny fane 2023-07-06 13:14:33 +02:00
Ken c6b0570c4c 🔇 Fjernet API-access for tilbakemeldinger 2023-07-06 11:21:05 +02:00
Ken 8baa1ab0b4 🔒 Fjernet kommentarfelt på aksel 2023-07-05 09:24:10 +02:00
Ken 06ceeb3e6f 🐛 Fikset rouge ';' i preview 2023-07-04 21:55:21 +02:00
Ken 02f19858c7 🐛 Fikset sanity-auth for preview og edit-knapp 2023-07-04 21:51:21 +02:00
Ken 799e26a628 🐛 Fikset lenker på komponentside 2023-07-04 17:11:53 +02:00
Ken f5e5843916 Oppdatert sanity, Lister opp alle utdaterte artikler for forfatter (#2091) 2023-07-04 13:59:33 +02:00
233 changed files with 9903 additions and 5153 deletions

No files matched your search

+3 -3
View File
@@ -75,7 +75,7 @@ jobs:
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: github-comment-finder
body-includes: Storybook demo
- name: Create or update comment
if: steps.chromatic_tests.outcome == 'success' && !contains(github.event.head_commit.message, '[ci skip]') && github.event_name != 'push'
@@ -84,7 +84,7 @@ jobs:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
### [<img id="github-comment-finder" src="https://raw.githubusercontent.com/storybookjs/brand/main/logo/logo-storybook-inverse.svg" height="32"/>](${{steps.storybook_url.outputs.replaced}})
## [Storybook demo](${{steps.storybook_url.outputs.replaced}})
[${{ steps.vars.outputs.sha_short }}](https://github.com/navikt/aksel/commit/${{ steps.vars.outputs.sha_short }}) | ${{steps.chromatic_tests.outputs.componentCount}} komponenter | ${{steps.chromatic_tests.outputs.specCount}} stories
edit-mode: replace
@@ -96,7 +96,7 @@ jobs:
comment-id: ${{ steps.fc.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
body: |
### [<img id="github-comment-finder" src="https://raw.githubusercontent.com/storybookjs/brand/main/logo/logo-storybook-inverse.svg" height="32"/>](${{steps.storybook_url.outputs.replaced}})
## [Storybook demo](${{steps.storybook_url.outputs.replaced}})
### [Endringer til review](${{steps.chromatic_tests.outputs.url}}): ${{steps.chromatic_tests.outputs.changeCount}}
+5
View File
@@ -10,6 +10,11 @@ module.exports = (indexers) => {
const exampleIndexer = async (fileName: string, opts) => {
let code = readFileSync(fileName, "utf-8").toString();
code = code
.split("\n")
.filter((line) => !line.includes("withDsExample"))
.join("\n");
code = code.replace("export const args =", "const args =");
code += `\nexport default { title: "Eksempler/${fileName
+3 -8
View File
@@ -1,5 +1,4 @@
const storyIndexers = require("./indexer");
const path = require("path");
const { mergeConfig } = require("vite");
@@ -59,15 +58,11 @@ module.exports = {
},
async viteFinal(config) {
return mergeConfig(config, {
define: { "process.env": {} },
define: {
"process.env": {},
},
resolve: {
alias: [
/*
// Example
{
find: "@components",
replacement: path.resolve(__dirname, "./src/components"),
}, */
{
find: "components",
replacement: path.resolve(
+30
View File
@@ -1,5 +1,35 @@
# @navikt/aksel-icons
## 4.12.0
## 4.11.2
## 4.11.1
## 4.11.0
## 4.10.2
## 4.10.1
## 4.10.0
## 4.9.1
## 4.9.0
## 4.8.0
## 4.7.4
## 4.7.3
## 4.7.2
## 4.7.1
## 4.7.0
## 4.6.1
## 4.6.0
@@ -58,7 +58,7 @@
.icon-button {
min-height: 3rem;
min-width: 3rem;
border-radius: 2px;
border-radius: var(--a-border-radius-small);
display: grid;
place-content: center;
background-color: var(--a-surface-default);
@@ -1,9 +1,9 @@
import meta from "@navikt/aksel-icons/metadata";
const subCategorizeIcons = (
icons: typeof meta[1][]
): { sub_category: string; icons: typeof meta[1][] }[] => {
const categories: { sub_category: string; icons: typeof meta[1][] }[] = [];
icons: (typeof meta)[1][]
): { sub_category: string; icons: (typeof meta)[1][] }[] => {
const categories: { sub_category: string; icons: (typeof meta)[1][] }[] = [];
for (const icon of icons) {
const i = categories.findIndex(
@@ -19,14 +19,14 @@ const subCategorizeIcons = (
};
export const categorizeIcons = (
icons: typeof meta[1][]
icons: (typeof meta)[1][]
): {
category: string;
sub_categories: { sub_category: string; icons: typeof meta[1][] }[];
sub_categories: { sub_category: string; icons: (typeof meta)[1][] }[];
}[] => {
const categories: {
category: string;
icons: typeof meta[1][];
icons: (typeof meta)[1][];
}[] = [];
for (const icon of icons) {
@@ -42,14 +42,14 @@ export const categorizeIcons = (
.map((x) => ({ ...x, sub_categories: subCategorizeIcons(x.icons) }));
};
const noFill = (icon: typeof meta[1], icons: typeof meta[1][]) => {
const noFill = (icon: (typeof meta)[1], icons: (typeof meta)[1][]) => {
const foundFill = icons.find(
(x) => x.name.endsWith("Fill") && x.name.replace("Fill", "") === icon.name
);
return !foundFill;
};
export const getFillIcon = (icons: typeof meta[1][]) => {
export const getFillIcon = (icons: (typeof meta)[1][]) => {
return icons.filter(
(x, _, z) => x.variant.toLowerCase() === "fill" || noFill(x, z)
);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-icons",
"version": "4.6.1",
"version": "4.12.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/ikoner",
"repository": {
+30
View File
@@ -1,5 +1,35 @@
# @navikt/aksel-stylelint
## 4.12.0
## 4.11.2
## 4.11.1
## 4.11.0
## 4.10.2
## 4.10.1
## 4.10.0
## 4.9.1
## 4.9.0
## 4.8.0
## 4.7.4
## 4.7.3
## 4.7.2
## 4.7.1
## 4.7.0
## 4.6.1
## 4.6.0
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel-stylelint",
"version": "4.6.1",
"version": "4.12.0",
"author": "Aksel | NAV",
"homepage": "https://aksel.nav.no/grunnleggende/kode/stylelint",
"repository": {
@@ -35,8 +35,8 @@
"dev": "yarn watch:lint"
},
"devDependencies": {
"@navikt/ds-css": "^4.6.1",
"@navikt/ds-tokens": "^4.6.1",
"@navikt/ds-css": "^4.12.0",
"@navikt/ds-tokens": "^4.12.0",
"@types/jest": "^29.0.0",
"concurrently": "7.2.1",
"copyfiles": "2.4.1",
+105
View File
@@ -1,5 +1,110 @@
# @navikt/aksel
## 4.12.0
### Patch Changes
- Updated dependencies [[`1c482e03b`](https://github.com/navikt/aksel/commit/1c482e03b845c4e6f6f1e174a58992fbeeabb52e), [`107d6f3d9`](https://github.com/navikt/aksel/commit/107d6f3d935cbc9bb94bc15cea63431cd15bbd61), [`09d41e9b6`](https://github.com/navikt/aksel/commit/09d41e9b6da5486a55faaf8088191b4a8374e420), [`a402777c7`](https://github.com/navikt/aksel/commit/a402777c70a18899ef4239b10a6335a671e26167)]:
- @navikt/ds-css@4.12.0
## 4.11.2
### Patch Changes
- Updated dependencies [[`72321e384`](https://github.com/navikt/aksel/commit/72321e3846f785318ebdba9f8e06cf13b09cd524)]:
- @navikt/ds-css@4.11.2
## 4.11.1
### Patch Changes
- Updated dependencies [[`e68ab5cb2`](https://github.com/navikt/aksel/commit/e68ab5cb2cd91c2f0234c864ee71821ed07a8203)]:
- @navikt/ds-css@4.11.1
## 4.11.0
### Patch Changes
- Updated dependencies [[`64901adcc`](https://github.com/navikt/aksel/commit/64901adccb8f19fbe06cdab4a70d47978e90e343), [`0de8238ec`](https://github.com/navikt/aksel/commit/0de8238ecf22e9f1fbc0e1b0cfe7a5cd7254901e), [`647b14a23`](https://github.com/navikt/aksel/commit/647b14a23888f82bacc4806d3afaacb5e39178fa)]:
- @navikt/ds-css@4.11.0
## 4.10.2
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@4.10.2
## 4.10.1
### Patch Changes
- Updated dependencies [[`d3bd1202e`](https://github.com/navikt/aksel/commit/d3bd1202eddb23dd3f1f09c3e4086ff3b19737d6)]:
- @navikt/ds-css@4.10.1
## 4.10.0
### Patch Changes
- Updated dependencies [[`267799ca0`](https://github.com/navikt/aksel/commit/267799ca0ecc0c549e159f94124befb654e29331)]:
- @navikt/ds-css@4.10.0
## 4.9.1
### Patch Changes
- Updated dependencies [[`eff8a902c`](https://github.com/navikt/aksel/commit/eff8a902c50fd5ebc56616e866aa0a61b9eb5efc)]:
- @navikt/ds-css@4.9.1
## 4.9.0
### Patch Changes
- Updated dependencies [[`e033b34c3`](https://github.com/navikt/aksel/commit/e033b34c3b85fb98dd11dd0300bb20075e1b06e8)]:
- @navikt/ds-css@4.9.0
## 4.8.0
### Patch Changes
- Updated dependencies [[`8a82e1c1f`](https://github.com/navikt/aksel/commit/8a82e1c1f66ea38fca77b25d74b14926bd817dd9)]:
- @navikt/ds-css@4.8.0
## 4.7.4
### Patch Changes
- Updated dependencies [[`492cfc180`](https://github.com/navikt/aksel/commit/492cfc1801df4ab16306185345d5089e24a0d659)]:
- @navikt/ds-css@4.7.4
## 4.7.3
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@4.7.3
## 4.7.2
### Patch Changes
- Updated dependencies [[`e6fdfdc9b`](https://github.com/navikt/aksel/commit/e6fdfdc9b44156b6eb8ebc0eda3a9259dce800ff)]:
- @navikt/ds-css@4.7.2
## 4.7.1
### Patch Changes
- Updated dependencies []:
- @navikt/ds-css@4.7.1
## 4.7.0
### Patch Changes
- Updated dependencies [[`e3027cc19`](https://github.com/navikt/aksel/commit/e3027cc1966623433cfc2d28915273486c7bf611)]:
- @navikt/ds-css@4.7.0
## 4.6.1
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/aksel",
"version": "4.6.1",
"version": "4.12.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": "4.6.1",
"@navikt/ds-css": "4.12.0",
"axios": "1.3.6",
"chalk": "4.1.0",
"clipboardy": "^2.3.0",
+90
View File
@@ -1,5 +1,95 @@
# @navikt/ds-css
## 4.12.0
### Minor Changes
- CopyButton: Har nå prop 'iconPosition' for å høyre/venstre aligne ikon ([#2173](https://github.com/navikt/aksel/pull/2173))
### Patch Changes
- List: Fikset sentrering, margins ([#2168](https://github.com/navikt/aksel/pull/2168))
- Checkbox: Checkmark er nå SVG-ikon og ikke Base64 ([#2171](https://github.com/navikt/aksel/pull/2171))
- Alert: Fikset alignment av status-ikon mot tekst ([#2179](https://github.com/navikt/aksel/pull/2179))
## 4.11.2
### Patch Changes
- :lipstick: ToggleGroup: fjern semibold fra selected button ([#2167](https://github.com/navikt/aksel/pull/2167))
## 4.11.1
### Patch Changes
- Border-radius: Fikset hardkodet border-radius i Datepicker, ToggleGroup og Combobox. ([#2159](https://github.com/navikt/aksel/pull/2159))
## 4.11.0
### Minor Changes
- HGrid: Ny komponent 'HGrid' er ute i Beta: [Dokumentasjon](https://aksel.nav.no/komponenter/core/hgrid) ([#1838](https://github.com/navikt/aksel/pull/1838))
### Patch Changes
- List: Fikset alignment av ikoner ([#2149](https://github.com/navikt/aksel/pull/2149))
- Font: La til egen font for semibold italic for bedre skalering cross-browser ([#2150](https://github.com/navikt/aksel/pull/2150))
## 4.10.2
### Patch Changes
- Link: Fikset visited farger og inline SVG-reset ved bruk av tailwind
## 4.10.0
### Minor Changes
- Link: La til 'variant', 'underline' og 'inlineText'-prop ([#2093](https://github.com/navikt/aksel/pull/2093))
## 4.9.1
### Patch Changes
- Tweaks to combobox - updated example, small bugfixes, better affordance for selected options and added flag for "isAddedByUser" to onToggleSelected ([#2144](https://github.com/navikt/aksel/pull/2144))
## 4.9.0
### Minor Changes
- Table: ExpandableRow kan nå åpnes med 'expandOnRowClick'-prop ([#2127](https://github.com/navikt/aksel/pull/2127))
## 4.8.0
### Minor Changes
- Nye komponenter `VStack`, `HStack` og `Spacer` for å enklere kunne lage layout med flexbox og spacing-variabler. ([#2040](https://github.com/navikt/aksel/pull/2040))
## 4.7.4
### Patch Changes
- Oppdatert bruk av REM i komponenter for forbedret utrykk ved font-scaling i browser ([#2126](https://github.com/navikt/aksel/pull/2126))
## 4.7.3
## 4.7.2
### Patch Changes
- Stepper: Fikset `hotizontal`-bug når step var `completed` ([#2116](https://github.com/navikt/aksel/pull/2116))
## 4.7.1
## 4.7.0
### Minor Changes
- Ny komponent Combobox! ([#1868](https://github.com/navikt/aksel/pull/1868))
## 4.6.1
### Patch Changes
+8 -1
View File
@@ -24,10 +24,17 @@
font-size: 1.5rem;
align-self: flex-start;
height: var(--a-font-line-height-xlarge);
margin: var(--a-spacing-1);
margin-inline: var(--a-spacing-1);
margin-block: 0;
margin-block-start: -1px;
background: radial-gradient(circle, var(--a-surface-default) 50%, 0, transparent);
}
.navds-alert--close-button > .navds-alert__icon,
.navds-alert--small > .navds-alert__icon {
margin-block-start: 0;
}
.navds-alert--small > .navds-alert__icon {
height: var(--a-font-line-height-large);
}
+7
View File
@@ -5,6 +5,13 @@
font-display: swap;
src: url("https://cdn.nav.no/aksel/fonts/SourceSans3-italic.woff2") format("woff2");
}
@font-face {
font-family: "Source Sans Pro";
font-style: italic;
font-weight: 600;
font-display: swap;
src: url("https://cdn.nav.no/aksel/fonts/SourceSans3-italic-latin-semibold") format("woff2");
}
@font-face {
font-family: "Source Sans Pro";
font-style: normal;
+2 -2
View File
@@ -25,11 +25,11 @@
padding: 0 var(--a-spacing-3);
text-decoration: none;
border-radius: var(--a-border-radius-full);
min-height: 32px;
min-height: 2rem;
}
.navds-chips--small .navds-chips__chip {
min-height: 24px;
min-height: 1.5rem;
padding: 0 var(--a-spacing-2);
}
+21
View File
@@ -54,6 +54,11 @@ const StyleMappings = {
{ component: "Checkbox", main: formCss, dependencies: [typoCss] },
{ component: "CheckboxGroup", main: formCss, dependencies: [typoCss] },
{ component: "Chips", main: "chips.css", dependencies: [typoCss] },
{
component: "UNSAFE_Combobox",
main: "combobox.css",
dependencies: [typoCss, "chips.css"],
},
{
component: "ConfirmationPanel",
main: formCss,
@@ -94,6 +99,10 @@ const StyleMappings = {
main: "help-text.css",
dependencies: ["popover.css"],
},
{
component: "HGrid",
main: "hgrid.css",
},
{ component: "Ingress", main: typoCss },
{
component: "InternalHeader",
@@ -129,6 +138,18 @@ const StyleMappings = {
{ component: "Select", main: formCss, dependencies: [typoCss] },
{ component: "Skeleton", main: "skeleton.css", dependencies: [] },
{ component: "Stepper", main: "stepper.css", dependencies: [typoCss] },
{
component: "HStack",
main: "stack.css",
},
{
component: "VStack",
main: "stack.css",
},
{
component: "Spacer",
main: "stack.css",
},
{ component: "Switch", main: formCss, dependencies: [typoCss] },
{ component: "Table", main: "table.css", dependencies: [typoCss] },
{ component: "Tabs", main: "tabs.css", dependencies: [typoCss] },
+9 -2
View File
@@ -52,7 +52,14 @@
.navds-copybutton__icon {
font-size: 1.5rem;
display: flex;
margin-left: -4px;
}
.navds-copybutton__icon:first-of-type {
margin-left: -0.25rem;
}
.navds-copybutton__icon:last-of-type {
margin-right: -0.25rem;
}
.navds-copybutton__icon:only-child {
@@ -60,7 +67,7 @@
}
:where(.navds-copybutton--small, .navds-copybutton--xsmall):where(:not(:only-child)) {
margin: -2px;
margin: -0.125rem;
}
.navds-copybutton:focus-visible {
+3 -4
View File
@@ -223,7 +223,7 @@
.navds-date__field-button {
position: absolute;
right: 1px;
right: 0.0625rem;
top: 50%;
transform: translateY(-50%);
color: var(--ac-date-input-button-text, var(--a-text-default));
@@ -233,12 +233,12 @@
text-decoration: none;
border: none;
background: none;
border-radius: 3px;
border-radius: calc(var(--a-border-radius-medium) - 1px);
padding: var(--a-spacing-3);
align-items: center;
justify-content: center;
font-size: 1.5rem;
height: calc(100% - 2px);
height: calc(100% - 0.125rem);
border-start-start-radius: 0;
border-end-start-radius: 0;
}
@@ -284,7 +284,6 @@
}
/* Readonly */
.navds-date__field--readonly .navds-date__field-button {
cursor: default;
}
+1 -1
View File
@@ -66,7 +66,7 @@
*************************/
.navds-expansioncard__title--small {
margin-top: 10px;
margin-top: 0.625rem;
}
.navds-expansioncard__title--medium {
+310
View File
@@ -0,0 +1,310 @@
.navds-combobox__wrapper {
display: flex;
flex-direction: column;
width: 100%;
position: relative;
}
.navds-combobox__wrapper-inner {
position: relative;
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
width: 100%;
padding: calc(var(--a-spacing-2) - 2px) var(--a-spacing-2);
}
.navds-combobox__wrapper-inner > :first-child {
flex: 2;
}
.navds-combobox__wrapper-inner > :last-child {
display: flex;
flex-flow: row nowrap;
}
.navds-form-field--small .navds-combobox__wrapper-inner {
padding: var(--a-spacing-1) calc(var(--a-spacing-3) / 2);
}
.navds-combobox__wrapper-inner:hover {
cursor: text;
}
.navds-combobox__selected-options {
gap: var(--a-spacing-2);
align-items: center;
}
.navds-combobox__selected-options > li {
margin: auto 0;
border-radius: var(--a-border-radius-full);
}
.navds-combobox__selected-options > li:last-of-type {
display: flex;
flex: 1;
}
.navds-combobox__selected-options--no-bg {
font-family: inherit;
font-size: var(--a-font-size-large);
font-weight: var(--a-font-weight-regular);
letter-spacing: 0;
line-height: var(--a-font-line-height-large);
margin: 0;
padding-left: 0.25rem;
}
.navds-combobox__input-wrapper {
width: 100%;
}
.navds-combobox__input {
z-index: 1;
flex: 1;
border: none;
padding: 0;
margin: 0;
min-width: 10ch;
width: 100%;
height: 2rem;
}
.navds-combobox__input:focus-visible {
outline: none;
border: none;
box-shadow: none;
}
.navds-combobox__wrapper-inner:has(.navds-combobox__input:focus-visible) {
box-shadow: 0 0 0 1px var(--a-surface-default) inset, var(--a-shadow-focus);
box-shadow: var(--a-shadow-focus);
}
.navds-combobox__wrapper-inner:has(.navds-combobox__input:focus-visible).navds-combobox__wrapper-inner--virtually-unfocused {
box-shadow: none;
}
@supports not selector(:focus-visible) {
.navds-combobox__input:focus {
outline: none;
border: none;
box-shadow: none;
}
.navds-combobox__wrapper-inner:has(.navds-combobox__input:focus) {
box-shadow: 0 0 0 1px var(--a-surface-default) inset, var(--a-shadow-focus);
box-shadow: var(--a-shadow-focus);
}
.navds-combobox__wrapper-inner:has(.navds-combobox__input:focus).navds-combobox__wrapper-inner--virtually-unfocused {
box-shadow: none;
}
}
@supports not selector(:has) {
.navds-combobox--focused .navds-combobox__wrapper-inner {
box-shadow: 0 0 0 1px var(--a-surface-default) inset, var(--a-shadow-focus);
box-shadow: var(--a-shadow-focus);
}
.navds-combobox--focused .navds-combobox__wrapper-inner.navds-combobox__wrapper-inner--virtually-unfocused {
box-shadow: none;
}
}
.navds-combobox__button-clear {
border-radius: var(--a-border-radius-medium);
color: var(--ac-combobox-clear-icon, var(--a-text-subtle));
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background: none;
border: none;
font-size: 1rem;
padding: 0;
}
.navds-combobox__button-clear svg {
width: 1.5rem;
height: 1.5rem;
}
.navds-combobox__input::-webkit-search-cancel-button {
display: none;
}
.navds-combobox__button-toggle-list {
border-radius: var(--a-border-radius-medium);
color: var(--a-text-default);
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
background: none;
border: none;
font-size: 1rem;
padding: 0;
}
.navds-combobox__button-clear:active:hover,
.navds-combobox__button-toggle-list:active:hover {
color: var(--ac-combobox-clear-icon-active, var(--a-text-action));
}
.navds-combobox__button-clear:hover,
.navds-combobox__button-toggle-list:hover {
color: var(--ac-combobox-clear-icon-hover, var(--a-text-action-selected));
}
.navds-combobox__button-toggle-list:focus-visible {
box-shadow: 0 0 0 1px var(--a-surface-default) inset, var(--a-shadow-focus);
box-shadow: var(--a-shadow-focus);
outline: none;
}
@supports not selector(:focus-visible) {
.navds-combobox__button-toggle-list:focus {
box-shadow: 0 0 0 1px var(--a-surface-default) inset, var(--a-shadow-focus);
box-shadow: var(--a-shadow-focus);
outline: none;
}
}
.navds-combobox__button-toggle-list svg {
width: 1.5rem;
height: 1.5rem;
pointer-events: none;
}
.navds-form-field--small .navds-combobox__button-toggle-list svg,
.navds-form-field--small .navds-combobox__button-clear svg {
width: 1.25rem;
height: 1.25rem;
}
/* dropdown list */
.navds-combobox__list {
max-height: 290px;
overflow-y: auto;
position: absolute;
left: 0;
right: 0;
z-index: 9999;
top: calc(100% + var(--a-spacing-2));
list-style: none;
margin: 0;
border: 1px solid var(--ac-combobox-list-border-color, var(--a-border-divider));
display: flex;
flex-direction: column;
align-items: flex-start;
padding: 0;
box-shadow: var(--a-shadow-small);
border-radius: var(--a-border-radius-medium);
gap: var(--a-spacing-1) 0;
background-color: var(--ac-combobox-list-bg, var(--a-surface-default));
color: var(--ac-combobox-list-text, var(--a-text-default));
}
.navds-combobox__list--closed {
display: none;
}
.navds-combobox__list svg {
height: 1.5rem;
width: 1.5rem;
}
.navds-combobox__list-item,
.navds-combobox__list-item__no-options,
.navds-combobox__list-item__new-option {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: var(--a-spacing-3);
width: 100%;
background-color: var(--ac-combobox-list-item-bg, var(--a-surface-default));
}
.navds-form-field--small .navds-combobox__list-item,
.navds-form-field--small .navds-combobox__list-item__no-options,
.navds-form-field--small .navds-combobox__list-item__new-option {
padding: calc(var(--a-spacing-3) / 2) var(--a-spacing-2);
}
.navds-combobox__list-item--loading {
display: flex;
justify-content: center;
padding: var(--a-spacing-3);
background-color: var(--ac-combobox-list-item-loading-bg, var(--a-surface-default));
width: 100%;
}
.navds-combobox__list-item--focus,
.navds-combobox__list-item:hover {
background-color: var(--ac-combobox-list-item-hover-bg, var(--a-surface-hover));
cursor: pointer;
border-left: 4px solid var(--ac-combobox-list-item-hover-border-left, var(--a-border-strong));
padding-left: calc(var(--a-spacing-3) - 4px);
}
.navds-form-field--small .navds-combobox__list-item--focus,
.navds-form-field--small .navds-combobox__list-item:hover {
padding-left: calc(var(--a-spacing-2) - 4px);
}
.navds-combobox__list-item--selected {
background-color: var(--ac-combobox-list-item-selected-bg, var(--a-surface-selected));
}
.navds-combobox__list-item--selected p {
font-weight: var(--a-font-weight-bold);
}
.navds-combobox__list-item--selected.navds-combobox__list-item--focus,
.navds-combobox__list-item--selected:hover {
background-color: var(--ac-combobox-list-item-selected-hover-bg, var(--a-surface-action-subtle-hover));
border-left: 4px solid var(--ac-combobox-list-item-selected-hover-border-left, var(--a-border-focus));
padding-left: calc(var(--a-spacing-3) - 4px);
}
.navds-combobox__list-item__new-option {
border-bottom: 1px solid var(--a-border-divider);
background: var(--a-surface-neutral-subtle);
cursor: pointer;
justify-content: flex-start;
gap: 0.25rem;
}
.navds-combobox__list-item__new-option:hover {
border-bottom: 1px solid var(--a-border-divider);
background: var(--a-surface-neutral-subtle-hover);
}
.navds-combobox__list-item__new-option--focus {
box-shadow: var(--a-shadow-focus) inset, var(--a-border-action) 0 0 0 5px inset;
border-radius: calc(var(--a-border-radius-medium) - 1px);
}
/* Mobile */
@media (max-width: 479px) {
.navds-combobox__button-toggle-list {
right: 0.5rem;
}
/* add bigger click area for input */
.navds-combobox__input {
min-width: min-content;
padding: 0.75rem 0;
}
.navds-combobox__selected-options {
gap: var(--a-spacing-1);
}
}
+1 -1
View File
@@ -9,7 +9,7 @@
}
.navds-form-field__description {
margin-top: -6px;
margin-top: -0.375rem;
color: var(--ac-form-description, var(--a-text-subtle));
}
+1
View File
@@ -11,3 +11,4 @@
@import "text-field.css";
@import "textarea.css";
@import "search.css";
@import "combobox.css";
+29 -13
View File
@@ -7,10 +7,10 @@
.navds-checkbox__input,
.navds-radio__input {
position: absolute;
width: 48px;
height: 48px;
width: 3rem;
height: 3rem;
top: 0;
left: -12px;
left: -0.75rem;
z-index: 1;
opacity: 0;
cursor: pointer;
@@ -51,15 +51,15 @@
.navds-checkbox--small > .navds-checkbox__input,
.navds-radio--small > .navds-radio__input {
width: 32px;
height: 32px;
width: 2rem;
height: 2rem;
top: 0;
left: -6px;
left: -0.375rem;
}
.navds-checkbox--small > .navds-checkbox__label,
.navds-radio--small > .navds-radio__label {
padding: 6px 0;
padding: 0.375rem 0;
}
.navds-checkbox--small > .navds-checkbox__label::before,
@@ -114,16 +114,29 @@
}
.navds-checkbox__input:checked + .navds-checkbox__label::before {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMyAxMCI+ICAgIDxnPiAgICA8cGF0aCBmaWxsPSIjRkZGRkZGIiBkPSJNNCwxMGMtMC40LDAtMC44LTAuMS0xLjEtMC40TDAuNCw3LjFDMC4xLDYuOCwwLDYuNCwwLDZzMC4yLTAuOCwwLjUtMS4xQzEsNC40LDIsNC40LDIuNSw0LjlMNCw2LjRsNi40LTYgICAgQzEwLjgsMC4xLDExLjEsMCwxMS41LDBjMC40LDAsMC44LDAuMiwxLDAuNWMwLjYsMC42LDAuNSwxLjYtMC4xLDIuMXYwTDUsOS42QzQuNyw5LjksNC40LDEwLDQsMTB6IE0xMS44LDEuOUwxMS44LDEuOSAgICBDMTEuOCwxLjksMTEuOCwxLjksMTEuOCwxLjl6IE0xMS4yLDEuMUMxMS4yLDEuMSwxMS4yLDEuMSwxMS4yLDEuMUwxMS4yLDEuMXoiLz4gICAgPC9nPjwvc3ZnPg==);
background-position: 6px center;
background-repeat: no-repeat;
background-size: 13px;
box-shadow: none;
background-color: var(--ac-radio-checkbox-action, var(--a-surface-action));
}
.navds-checkbox__input:where(:not(:checked)) + .navds-checkbox__label > .navds-checkbox__icon {
display: none;
}
.navds-checkbox__input:checked + .navds-checkbox__label > .navds-checkbox__icon {
color: var(--ac-radio-checkbox-bg, var(--a-surface-default));
position: absolute;
height: 1.5rem;
transform: translate(0.375rem);
pointer-events: none;
}
.navds-checkbox--small .navds-checkbox__input:checked + .navds-checkbox__label > .navds-checkbox__icon {
transform: translate(0.25rem, -10%);
height: 1.25rem;
}
.navds-checkbox--small > .navds-checkbox__input:checked + .navds-checkbox__label::before {
background-position: 4px center;
background-position: 0.25rem center;
}
.navds-checkbox__input:indeterminate:focus-visible + .navds-checkbox__label::before,
@@ -283,11 +296,14 @@
}
.navds-checkbox--readonly > .navds-checkbox__input:checked + .navds-checkbox__label::before {
background-image: url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAxMyAxMCI+ICAgIDxnPiAgICA8cGF0aCBmaWxsPSJyZ2JhKDAsIDAsIDAsIDAuNjUpIiBkPSJNNCwxMGMtMC40LDAtMC44LTAuMS0xLjEtMC40TDAuNCw3LjFDMC4xLDYuOCwwLDYuNCwwLDZzMC4yLTAuOCwwLjUtMS4xQzEsNC40LDIsNC40LDIuNSw0LjlMNCw2LjRsNi40LTYgICAgQzEwLjgsMC4xLDExLjEsMCwxMS41LDBjMC40LDAsMC44LDAuMiwxLDAuNWMwLjYsMC42LDAuNSwxLjYtMC4xLDIuMXYwTDUsOS42QzQuNyw5LjksNC40LDEwLDQsMTB6IE0xMS44LDEuOUwxMS44LDEuOSAgICBDMTEuOCwxLjksMTEuOCwxLjksMTEuOCwxLjl6IE0xMS4yLDEuMUMxMS4yLDEuMSwxMS4yLDEuMSwxMS4yLDEuMUwxMS4yLDEuMXoiLz4gICAgPC9nPjwvc3ZnPg==);
box-shadow: inset 0 0 0 2px var(--__ac-radio-checkbox-readonly-border);
background-color: var(--__ac-radio-checkbox-readonly-bg);
}
.navds-checkbox--readonly > .navds-checkbox__input:checked + .navds-checkbox__label > .navds-checkbox__icon {
color: var(--a-icon-subtle);
}
.navds-radio--readonly > .navds-radio__input:checked + .navds-radio__label::before {
box-shadow: inset 0 0 0 2px var(--__ac-radio-checkbox-readonly-border), inset 0 0 0 4px var(--__ac-radio-checkbox-readonly-bg);
background-color: var(--a-icon-subtle);
+3 -3
View File
@@ -104,8 +104,8 @@
.navds-search__button-search {
flex-shrink: 0;
min-width: 64px;
min-height: 32px;
min-width: 4rem;
min-height: 2rem;
border-radius: 0;
border-top-right-radius: var(--a-border-radius-medium);
border-bottom-right-radius: var(--a-border-radius-medium);
@@ -116,7 +116,7 @@
}
.navds-form-field--small .navds-search__button-search {
min-width: 40px;
min-width: 2.5rem;
}
.navds-search__button-search.navds-button--secondary {
+2 -2
View File
@@ -6,7 +6,7 @@
color: var(--ac-select-text, var(--a-text-default));
width: 100%;
box-sizing: border-box;
min-height: 48px;
min-height: 3rem;
display: inline-block;
position: relative;
padding: 0.5rem;
@@ -45,7 +45,7 @@
}
.navds-form-field--small .navds-select__input {
min-height: 32px;
min-height: 2rem;
padding: 0 2rem 0 0.25rem;
}
+18 -18
View File
@@ -1,6 +1,6 @@
.navds-switch {
position: relative;
min-height: 48px;
min-height: 3rem;
width: fit-content;
}
@@ -10,7 +10,7 @@
.navds-switch--small {
position: relative;
min-height: 32px;
min-height: 2rem;
}
/* Input */
@@ -18,14 +18,14 @@
cursor: pointer;
position: absolute;
z-index: 1;
width: 48px;
height: 48px;
width: 3rem;
height: 3rem;
opacity: 0;
top: 0;
}
.navds-switch--small > .navds-switch__input {
height: 32px;
height: 2rem;
top: 0;
}
@@ -47,11 +47,11 @@
}
.navds-switch--small > .navds-switch__label-wrapper > .navds-switch__content {
padding: calc(var(--a-spacing-2) - 2px) 0 calc(var(--a-spacing-2) - 2px) 3.25rem;
padding: calc(var(--a-spacing-2) - var(--a-spacing-05)) 0 calc(var(--a-spacing-2) - var(--a-spacing-05)) 3.25rem;
}
.navds-switch--right.navds-switch--small > .navds-switch__label-wrapper > .navds-switch__content {
padding: calc(var(--a-spacing-2) - 2px) 3.25rem calc(var(--a-spacing-2) - 2px) 0;
padding: calc(var(--a-spacing-2) - var(--a-spacing-05)) 3.25rem calc(var(--a-spacing-2) - var(--a-spacing-05)) 0;
}
.navds-switch--with-description,
@@ -70,8 +70,8 @@
/* Track */
.navds-switch__track {
width: 44px;
height: 24px;
width: 2.75rem;
height: 1.5rem;
background-color: var(--ac-switch-bg, var(--a-surface-neutral));
position: absolute;
top: var(--a-spacing-3);
@@ -125,12 +125,12 @@
background-color: var(--ac-switch-thumb-bg, var(--a-surface-default));
color: var(--ac-switch-thumb-icon, var(--a-icon-subtle));
border-radius: var(--a-border-radius-full);
width: 20px;
height: 20px;
width: 1.25rem;
height: 1.25rem;
position: absolute;
transform: translateX(0);
left: 2px;
top: 2px;
left: var(--a-spacing-05);
top: var(--a-spacing-05);
transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
@@ -138,17 +138,17 @@
}
.navds-switch__input:checked ~ .navds-switch__track > .navds-switch__thumb {
transform: translateX(20px);
transform: translateX(1.25rem);
color: var(--ac-switch-thumb-icon-checked, var(--a-icon-success));
}
@media (hover: hover) and (pointer: fine) {
.navds-switch__input:hover ~ .navds-switch__track > .navds-switch__thumb {
transform: translateX(2px);
transform: translateX(0.125rem);
}
.navds-switch__input:checked:hover ~ .navds-switch__track > .navds-switch__thumb {
transform: translateX(18px);
transform: translateX(1.125rem);
}
}
@@ -157,7 +157,7 @@
}
.navds-switch__input:checked:disabled:hover ~ .navds-switch__track > .navds-switch__thumb {
transform: translateX(20px);
transform: translateX(1.25rem);
}
/* Loader */
@@ -216,6 +216,6 @@
}
.navds-switch--readonly > .navds-switch__input:checked:hover ~ .navds-switch__track > .navds-switch__thumb {
transform: translateX(20px);
transform: translateX(1.25rem);
}
}
+3 -3
View File
@@ -13,7 +13,7 @@
background-color: var(--ac-textfield-bg, var(--a-surface-default));
border-radius: var(--a-border-radius-medium);
border: 1px solid var(--ac-textfield-border, var(--a-border-default));
min-height: 48px;
min-height: 3rem;
width: 100%;
color: var(--ac-textfield-text, var(--a-text-default));
}
@@ -27,8 +27,8 @@
}
.navds-form-field--small .navds-text-field__input {
padding: 0 0.5rem;
min-height: 32px;
padding: 0 var(--a-spacing-2);
min-height: 2rem;
}
.navds-text-field__input:hover {
+3 -3
View File
@@ -51,7 +51,7 @@
}
.navds-form-field--small .navds-textarea__input {
padding: 6px;
padding: 0.375rem;
}
.navds-form-field--small .navds-textarea--counter.navds-textarea__input {
@@ -64,8 +64,8 @@
font-style: italic;
position: absolute;
text-align: left;
left: 1px;
bottom: 1px;
left: 0.0625rem;
bottom: 0.0625rem;
padding: var(--a-spacing-1) var(--a-spacing-2);
}
+55
View File
@@ -0,0 +1,55 @@
.navds-hgrid {
--__ac-hgrid-columns-xs: initial;
--__ac-hgrid-columns-sm: initial;
--__ac-hgrid-columns-md: initial;
--__ac-hgrid-columns-lg: initial;
--__ac-hgrid-columns-xl: initial;
--__ac-hgrid-columns: var(--__ac-hgrid-columns-xs);
--__ac-hgrid-gap-xs: initial;
--__ac-hgrid-gap-sm: initial;
--__ac-hgrid-gap-md: initial;
--__ac-hgrid-gap-lg: initial;
--__ac-hgrid-gap-xl: initial;
--__ac-hgrid-gap: var(--__ac-hgrid-gap-xs);
display: grid;
grid-template-columns: var(--__ac-hgrid-columns);
gap: var(--__ac-hgrid-gap);
}
@media (min-width: 480px) {
.navds-hgrid {
--__ac-hgrid-columns: var(--__ac-hgrid-columns-sm, var(--__ac-hgrid-columns-xs));
--__ac-hgrid-gap: var(--__ac-hgrid-gap-sm, var(--__ac-hgrid-gap-xs));
}
}
@media (min-width: 768px) {
.navds-hgrid {
--__ac-hgrid-columns: var(--__ac-hgrid-columns-md, var(--__ac-hgrid-columns-sm, var(--__ac-hgrid-columns-xs)));
--__ac-hgrid-gap: var(--__ac-hgrid-gap-md, var(--__ac-hgrid-gap-sm, var(--__ac-hgrid-gap-xs)));
}
}
@media (min-width: 1024px) {
.navds-hgrid {
--__ac-hgrid-columns: var(
--__ac-hgrid-columns-lg,
var(--__ac-hgrid-columns-md, var(--__ac-hgrid-columns-sm, var(--__ac-hgrid-columns-xs)))
);
--__ac-hgrid-gap: var(--__ac-hgrid-gap-lg, var(--__ac-hgrid-gap-md, var(--__ac-hgrid-gap-sm, var(--__ac-hgrid-gap-xs))));
}
}
@media (min-width: 1280px) {
.navds-hgrid {
--__ac-hgrid-columns: var(
--__ac-hgrid-columns-xl,
var(--__ac-hgrid-columns-lg, var(--__ac-hgrid-columns-md, var(--__ac-hgrid-columns-sm, var(--__ac-hgrid-columns-xs))))
);
--__ac-hgrid-gap: var(
--__ac-hgrid-gap-xl,
var(--__ac-hgrid-gap-lg, var(--__ac-hgrid-gap-md, var(--__ac-hgrid-gap-sm, var(--__ac-hgrid-gap-xs))))
);
}
}
+2
View File
@@ -14,6 +14,7 @@
@import "guide-panel.css";
@import "form/index.css";
@import "help-text.css";
@import "hgrid.css";
@import "internalheader.css";
@import "link.css";
@import "loader.css";
@@ -29,6 +30,7 @@
@import "link-panel.css";
@import "read-more.css";
@import "skeleton.css";
@import "stack.css";
@import "stepper.css";
@import "table.css";
@import "tabs.css";
+1 -1
View File
@@ -2,7 +2,7 @@
display: flex;
align-self: stretch;
background: var(--ac-internalheader-bg, var(--a-surface-inverted));
min-height: var(--a-spacing-12);
min-height: 3rem;
--navds-internalheader-focus: inset 0 0 0 2px var(--a-border-focus-on-inverted);
}
+35 -3
View File
@@ -6,15 +6,35 @@
gap: var(--a-spacing-1);
}
.navds-alert .navds-link,
.navds-confirmation-panel .navds-link {
color: var(--a-text-default);
.navds-link.navds-link--inline-text {
display: inline;
}
.navds-link.navds-link--inline-text > svg {
/* stylelint-disable-next-line plugin/declaration-block-no-ignored-properties */
margin: 0.1em 0.1em -0.1em;
transform: translateY(0.05em);
display: inline;
vertical-align: baseline;
}
.navds-link:hover {
text-decoration: none;
}
.navds-link.navds-link--remove-underline {
text-decoration: none;
}
.navds-link.navds-link--remove-underline:hover {
text-decoration: underline;
}
.navds-alert .navds-link,
.navds-confirmation-panel .navds-link {
color: var(--a-text-default);
}
.navds-link:focus-visible {
outline: none;
color: var(--ac-link-focus-text, var(--a-text-on-action));
@@ -45,3 +65,15 @@
color: inherit;
flex-shrink: 0;
}
.navds-link--action {
color: var(--ac-link-action-text, var(--a-text-action));
}
.navds-link--neutral {
color: var(--ac-link-neutral-text, var(--a-text-default));
}
.navds-link--subtle {
color: var(--ac-link-subtle-text, var(--a-text-subtle));
}
+40 -9
View File
@@ -1,14 +1,24 @@
.navds-list ul,
.navds-list ol {
padding: 0;
margin-block: var(--a-spacing-4);
}
.navds-list--nested {
margin: var(--a-spacing-4) 0 var(--a-spacing-8) 0;
.navds-list--nested ul,
.navds-list--nested ol {
margin-block: var(--a-spacing-2);
}
.navds-list li:not(.navds-list__item--noMargin) {
margin-bottom: var(--a-spacing-5);
.navds-list li {
margin-block-end: var(--a-spacing-5);
}
.navds-list--small li {
margin-block-end: var(--a-spacing-2);
}
.navds-list--nested li {
margin-block-end: var(--a-spacing-3);
}
.navds-list:last-child > ul > li:last-child,
@@ -34,15 +44,30 @@
.navds-list .navds-list__item-marker--bullet {
display: flex;
margin-top: 6px;
align-items: center;
height: var(--a-font-line-height-large);
grid-column: 1 / 2;
/* stylelint-disable-next-line csstools/value-no-unknown-custom-properties */
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-medium);
}
.navds-list__item-marker--icon {
/* stylelint-disable-next-line csstools/value-no-unknown-custom-properties */
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-large);
padding-block-end: 1px;
}
.navds-list--small .navds-list__item-marker--icon {
height: var(--a-font-line-height-medium);
}
.navds-list__item-marker--icon svg {
flex-shrink: 0;
}
.navds-list ul .navds-list__item-content {
@@ -51,6 +76,7 @@
.navds-list ol {
padding-left: var(--a-spacing-2);
list-style: decimal;
}
.navds-list ol li {
@@ -60,7 +86,12 @@
.navds-list ol li::marker {
font-weight: var(--a-font-weight-bold);
font-size: var(--a-font-size-medium);
/* stylelint-disable-next-line csstools/value-no-unknown-custom-properties */
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-large);
}
.navds-list--small ol li::marker {
font-size: var(--a-font-size-small);
line-height: var(--a-font-line-height-medium);
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-css",
"version": "4.6.1",
"version": "4.12.0",
"description": "CSS for NAV Designsystem",
"author": "Aksel | NAV designsystem team",
"keywords": [
@@ -27,11 +27,11 @@
"css:get-version": "node config/get-version.js"
},
"devDependencies": {
"@navikt/ds-tokens": "^4.6.1",
"@navikt/ds-tokens": "^4.12.0",
"cssnano": "6.0.0",
"fast-glob": "3.2.11",
"lodash": "4.17.21",
"nodemon": "^2.0.12",
"nodemon": "^3.0.1",
"normalize.css": "^8.0.1",
"postcss": "^8.4.0",
"postcss-cli": "^9.0.0",
+6 -6
View File
@@ -8,12 +8,12 @@
gap: var(--a-spacing-05);
color: var(--ac-read-more-text, var(--a-text-action));
border-radius: var(--a-border-radius-small);
padding: var(--a-spacing-1) var(--a-spacing-1) var(--a-spacing-1) 2px;
padding: var(--a-spacing-1) var(--a-spacing-1) var(--a-spacing-1) var(--a-spacing-05);
text-align: start;
}
.navds-read-more--small .navds-read-more__button {
padding: 2px var(--a-spacing-1) 2px 2px;
padding: var(--a-spacing-05) var(--a-spacing-1) var(--a-spacing-05) var(--a-spacing-05);
}
.navds-read-more__button:hover {
@@ -39,14 +39,14 @@
.navds-read-more__content {
margin-top: var(--a-spacing-1);
border-left: 2px solid var(--ac-read-more-line, var(--a-border-divider));
margin-left: 13px;
padding-left: 13px;
margin-left: 0.8125rem;
padding-left: 0.8125rem;
color: var(--a-text-default);
}
.navds-read-more--small .navds-read-more__content {
margin-left: 11px;
padding-left: 11px;
margin-left: 0.6875rem;
padding-left: 0.6875rem;
}
.navds-read-more__content--closed {
+59
View File
@@ -0,0 +1,59 @@
.navds-stack {
--__ac-stack-align: initial;
--__ac-stack-justify: initial;
--__ac-stack-direction: initial;
--__ac-stack-wrap: initial;
--__ac-stack-gap-xs: initial;
--__ac-stack-gap-sm: initial;
--__ac-stack-gap-md: initial;
--__ac-stack-gap-lg: initial;
--__ac-stack-gap-xl: initial;
--__ac-stack-gap: var(--__ac-stack-gap-xs);
gap: var(--__ac-stack-gap);
display: flex;
align-items: var(--__ac-stack-align);
justify-content: var(--__ac-stack-justify);
flex-flow: var(--__ac-stack-direction) var(--__ac-stack-wrap);
}
.navds-stack__spacer {
flex: 1;
justify-self: stretch;
align-self: stretch;
}
.navds-stack > .navds-stack__spacer {
margin-block-start: calc(var(--__ac-stack-gap) * -1);
}
.navds-hstack > .navds-stack__spacer {
margin-inline-start: calc(var(--__ac-stack-gap) * -1);
}
@media (min-width: 480px) {
.navds-stack {
--__ac-stack-gap: var(--__ac-stack-gap-sm, var(--__ac-stack-gap-xs));
}
}
@media (min-width: 768px) {
.navds-stack {
--__ac-stack-gap: var(--__ac-stack-gap-md, var(--__ac-stack-gap-sm, var(--__ac-stack-gap-xs)));
}
}
@media (min-width: 1024px) {
.navds-stack {
--__ac-stack-gap: var(--__ac-stack-gap-lg, var(--__ac-stack-gap-md, var(--__ac-stack-gap-sm, var(--__ac-stack-gap-xs))));
}
}
@media (min-width: 1280px) {
.navds-stack {
--__ac-stack-gap: var(
--__ac-stack-gap-xl,
var(--__ac-stack-gap-lg, var(--__ac-stack-gap-md, var(--__ac-stack-gap-sm, var(--__ac-stack-gap-xs))))
);
}
}
+11 -1
View File
@@ -40,6 +40,10 @@
background-color: var(--ac-stepper-line-completed, var(--a-border-selected));
}
.navds-stepper__item--behind.navds-stepper__item--completed + .navds-stepper__item > .navds-stepper__line--1 {
background-color: var(--ac-stepper-line-completed, var(--a-border-selected));
}
.navds-stepper__line--1 {
grid-row: line-1;
display: none;
@@ -109,7 +113,7 @@ button.navds-stepper__step {
.navds-stepper__circle--success {
border: none;
background: none;
font-size: 28px;
font-size: 1.75rem;
color: var(--a-text-on-action);
background-color: var(--ac-stepper-text, var(--a-surface-action));
}
@@ -213,6 +217,12 @@ button.navds-stepper__step {
background-color: var(--ac-stepper-non-interactive-line-completed, var(--a-border-strong));
}
.navds-stepper__item--non-interactive.navds-stepper__item--behind.navds-stepper__item--completed
+ .navds-stepper__item
> .navds-stepper__line--1 {
background-color: var(--ac-stepper-non-interactive-line-completed, var(--a-border-strong));
}
:where(.navds-stepper__step--non-interactive.navds-stepper__step--active) .navds-stepper__circle {
background-color: var(--ac-stepper-non-interactive-active-bg, var(--a-surface-inverted));
border-color: var(--ac-stepper-non-interactive-active-border, var(--a-surface-inverted));
+9 -5
View File
@@ -16,7 +16,7 @@
display: table-row;
}
.navds-table__body .navds-table__row--shade-on-hover:hover {
.navds-table__body .navds-table__row--shade-on-hover:not(.navds-table__expandable-row--expansion-disabled):hover {
background-color: var(--ac-table-row-hover, var(--a-bg-subtle));
}
@@ -80,11 +80,11 @@
}
.navds-table .navds-checkbox .navds-checkbox__input {
top: -12px;
top: -0.75rem;
}
.navds-table .navds-checkbox--small .navds-checkbox__input {
top: -6px;
top: -0.375rem;
}
.navds-table .navds-checkbox .navds-checkbox__label {
@@ -154,6 +154,10 @@
transition: border-bottom-color 190ms cubic-bezier(0.6, 0.04, 0.98, 0.335);
}
.navds-table__expandable-row:not(.navds-table__expandable-row--expansion-disabled):hover {
cursor: pointer;
}
.navds-table__expandable-row--open .navds-table__data-cell {
border-bottom-color: transparent;
}
@@ -168,12 +172,12 @@
.navds-table__toggle-expand-cell {
padding: 0;
width: 56px;
width: 3.5rem;
}
.navds-table--small .navds-table__toggle-expand-cell {
padding: 0;
width: 36px;
width: 2.25rem;
}
.navds-table__toggle-expand-button {
+4 -4
View File
@@ -20,7 +20,7 @@
.navds-tabs__scroll-button {
padding: var(--a-spacing-3) var(--a-spacing-4);
width: 44px;
width: 2.75rem;
display: flex;
justify-content: center;
align-items: center;
@@ -38,11 +38,11 @@
.navds-tabs--small .navds-tabs__scroll-button {
padding: 0.375rem var(--a-spacing-4);
width: 32px;
width: 2rem;
}
.navds-tabs__tab {
min-height: 48px;
min-height: 3rem;
padding: var(--a-spacing-3) var(--a-spacing-4);
display: inline-flex;
justify-content: center;
@@ -92,7 +92,7 @@
}
.navds-tabs__tab--small {
min-height: 32px;
min-height: 2rem;
padding: 0.375rem var(--a-spacing-4);
}
+3 -3
View File
@@ -5,17 +5,17 @@
align-items: center;
justify-content: center;
padding: var(--a-spacing-05) var(--a-spacing-2);
min-height: 32px;
min-height: 2rem;
line-height: 1;
}
.navds-tag--small {
min-height: 24px;
min-height: 1.5rem;
padding: 0 0.375rem;
}
.navds-tag--xsmall {
min-height: 20px;
min-height: 1.25rem;
padding: 0 var(--a-spacing-1);
}
+3 -2
View File
@@ -12,6 +12,7 @@
height: 1rem;
box-sizing: content-box;
grid-column: 2;
margin-bottom: var(--a-spacing-1);
}
.navds-timeline__row-label {
@@ -181,7 +182,7 @@
0 0 0 4px var(--ac-timeline-pin-bg, var(--a-surface-danger)), 0 0 0 5px var(--a-surface-default);
z-index: 2;
border-radius: var(--a-border-radius-full);
padding: 3.5px;
padding: var(--a-spacing-1);
position: relative;
}
@@ -232,7 +233,7 @@
.navds-timeline__pin-wrapper::before {
content: "";
top: var(--navdsc-timeline-pin-size);
height: calc(94% - var(--navdsc-timeline-pin-size));
height: calc(88% - var(--navdsc-timeline-pin-size));
width: 1px;
margin: 0 auto;
background: var(--a-surface-inverted);
+5 -6
View File
@@ -8,7 +8,7 @@
border-radius: var(--a-border-radius-medium);
background-color: var(--ac-toggle-group-bg, var(--a-surface-transparent));
box-shadow: inset 0 0 0 1px var(--ac-toggle-group-border, var(--a-border-default));
padding: calc(var(--a-spacing-1) + 1px);
padding: calc(var(--a-spacing-1) + 0.0625rem);
gap: var(--a-spacing-1);
display: inline-grid;
grid-auto-flow: column;
@@ -16,7 +16,7 @@
}
.navds-toggle-group--small {
padding: calc(var(--a-spacing-05) + 1px);
padding: calc(var(--a-spacing-05) + 0.0625rem);
}
.navds-toggle-group--neutral {
@@ -29,12 +29,12 @@
align-items: center;
justify-content: center;
padding: var(--a-spacing-1) var(--a-spacing-3);
min-height: 38px;
min-height: 2.375rem;
border: none;
cursor: pointer;
background-color: var(--ac-toggle-group-button-bg, var(--a-surface-transparent));
color: var(--ac-toggle-group-button-text, var(--a-text-default));
border-radius: 2px;
border-radius: var(--a-border-radius-small);
min-width: fit-content;
font-weight: var(--a-font-weight-regular);
}
@@ -113,7 +113,6 @@
.navds-toggle-group__button[aria-checked="true"] {
background-color: var(--ac-toggle-group-selected-bg, var(--a-surface-action-selected));
color: var(--ac-toggle-group-selected-text, var(--a-text-on-action));
font-weight: var(--a-font-weight-bold);
}
.navds-toggle-group--neutral > .navds-toggle-group__button[aria-pressed="true"],
@@ -124,7 +123,7 @@
.navds-toggle-group--small > .navds-toggle-group__button {
padding: var(--a-spacing-05) var(--a-spacing-3);
min-height: 26px;
min-height: 1.625rem;
min-width: fit-content;
}
+19 -1
View File
@@ -175,7 +175,10 @@
"--ac-link-focus-border": "--a-border-focus",
"--ac-link-active-text": "--a-text-on-action",
"--ac-link-active-bg": "--a-border-focus",
"--ac-link-active-border": "--a-border-focus"
"--ac-link-active-border": "--a-border-focus",
"--ac-link-action-text": "--a-text-action",
"--ac-link-neutral-text": "--a-text-default",
"--ac-link-subtle-text": "--a-text-subtle"
},
"list": {
"--ac-list-marker-ul-color": "--ac-list-marker-color",
@@ -372,6 +375,21 @@
"--ac-search-button-focus-active-border": "--a-surface-default",
"--ac-search-error-border": "--a-border-danger"
},
"combobox": {
"--ac-combobox-clear-icon": "--a-text-subtle",
"--ac-combobox-clear-icon-hover": "--a-text-action-selected",
"--ac-combobox-clear-icon-active": "--a-text-action",
"--ac-combobox-list-bg": "--a-surface-default",
"--ac-combobox-list-text": "--a-text-default",
"--ac-combobox-list-border-color": "--a-border-divider",
"--ac-combobox-list-item-bg": "--a-surface-default",
"--ac-combobox-list-item-hover-bg": "--a-surface-hover",
"--ac-combobox-list-item-selected-bg": "--a-surface-default",
"--ac-combobox-list-item-selected-hover-bg": "--a-surface-action-subtle-hover",
"--ac-combobox-list-item-loading-bg": "--a-surface-default",
"--ac-combobox-list-item-hover-border-left": "--a-border-strong",
"--ac-combobox-list-item-selected-hover-border-left": "--a-border-focus"
},
"select": {
"--ac-select-bg": "--a-surface-default",
"--ac-select-text": "--a-text-default",
+4 -4
View File
@@ -25,8 +25,8 @@
}
.navds-tooltip__arrow {
height: 7px;
width: 7px;
height: 0.4375rem;
width: 0.4375rem;
transform: rotate(45deg);
z-index: -1;
background-color: var(--ac-tooltip-bg, var(--a-surface-inverted));
@@ -45,8 +45,8 @@
color: var(--ac-tooltip-key-text, var(--a-text-on-inverted));
border-radius: var(--a-border-radius-small);
padding: 0 var(--a-spacing-1);
min-width: 18px;
height: 18px;
min-width: 1.125rem;
height: 1.125rem;
display: inline-flex;
align-items: center;
justify-content: center;
+163
View File
@@ -1,5 +1,168 @@
# @navikt/ds-react
## 4.12.0
### Minor Changes
- Combobox: La til støtte for feilmeldinger i Combobox ([#2182](https://github.com/navikt/aksel/pull/2182))
- CopyButton: Har nå prop 'iconPosition' for å høyre/venstre aligne ikon ([#2173](https://github.com/navikt/aksel/pull/2173))
### Patch Changes
- List: Fikset sentrering, margins ([#2168](https://github.com/navikt/aksel/pull/2168))
- Combobox: Kjører nå 'onChange' + 'onClear' når input blir reset programmatisk ([#2183](https://github.com/navikt/aksel/pull/2183))
- Combobox: Fikset custom-options i singleselect ([#2180](https://github.com/navikt/aksel/pull/2180))
- Combobox: Fjernet unødvendige 'onClear'-calls når man velger verdier ([#2170](https://github.com/navikt/aksel/pull/2170))
- Checkbox: Checkmark er nå SVG-ikon og ikke Base64 ([#2171](https://github.com/navikt/aksel/pull/2171))
- Combobox: Lukker nå nedtrekksmeny hvis man legger til ny option i singleselect ([#2177](https://github.com/navikt/aksel/pull/2177))
- Updated dependencies []:
- @navikt/aksel-icons@4.12.0
## 4.11.2
### Patch Changes
- Updated dependencies []:
- @navikt/aksel-icons@4.11.2
## 4.11.1
### Patch Changes
- Updated dependencies []:
- @navikt/aksel-icons@4.11.1
## 4.11.0
### Minor Changes
- HGrid: Ny komponent 'HGrid' er ute i Beta: [Dokumentasjon](https://aksel.nav.no/komponenter/core/hgrid) ([#1838](https://github.com/navikt/aksel/pull/1838))
### Patch Changes
- Fixes bug where combobox list could not be closed after clicking a chip ([#2155](https://github.com/navikt/aksel/pull/2155))
- Grid: Markert som deprecated. Bruk nye 'HGrid' ([#2153](https://github.com/navikt/aksel/pull/2153))
- Updated dependencies []:
- @navikt/aksel-icons@4.11.0
## 4.10.2
### Patch Changes
- Updated dependencies []:
- @navikt/aksel-icons@4.10.2
## 4.10.1
### Patch Changes
- Updated dependencies []:
- @navikt/aksel-icons@4.10.1
## 4.10.0
### Minor Changes
- Link: La til 'variant', 'underline' og 'inlineText'-prop ([#2093](https://github.com/navikt/aksel/pull/2093))
### Patch Changes
- Updated dependencies []:
- @navikt/aksel-icons@4.10.0
## 4.9.1
### Patch Changes
- Button: Fikset aria-live bug der knapp alltid ble lest opp av skjermleser ved render ([#2143](https://github.com/navikt/aksel/pull/2143))
- Tweaks to combobox - updated example, small bugfixes, better affordance for selected options and added flag for "isAddedByUser" to onToggleSelected ([#2144](https://github.com/navikt/aksel/pull/2144))
- Updated dependencies []:
- @navikt/aksel-icons@4.9.1
## 4.9.0
### Minor Changes
- Table: ExpandableRow kan nå åpnes med 'expandOnRowClick'-prop ([#2127](https://github.com/navikt/aksel/pull/2127))
### Patch Changes
- Updated dependencies []:
- @navikt/aksel-icons@4.9.0
## 4.8.0
### Minor Changes
- Nye komponenter `VStack`, `HStack` og `Spacer` for å enklere kunne lage layout med flexbox og spacing-variabler. ([#2040](https://github.com/navikt/aksel/pull/2040))
### Patch Changes
- Updated dependencies []:
- @navikt/aksel-icons@4.8.0
## 4.7.4
### Patch Changes
- Textfield: La til type 'time' som tilgjengelig option ([#2137](https://github.com/navikt/aksel/pull/2137))
- Updated dependencies []:
- @navikt/aksel-icons@4.7.4
## 4.7.3
### Patch Changes
- Chips: Removable Chips submitter ikke forms ved klikk lengre ([#2124](https://github.com/navikt/aksel/pull/2124))
- Updated dependencies []:
- @navikt/aksel-icons@4.7.3
## 4.7.2
### Patch Changes
- Stepper: Fikset `hotizontal`-bug når step var `completed` ([#2116](https://github.com/navikt/aksel/pull/2116))
- Updated dependencies []:
- @navikt/aksel-icons@4.7.2
## 4.7.1
### Patch Changes
- Datepicker: Fikset bug ved bruk dynamisk oppdatering av minDate. Vist `month` vil nå alltid være oppdatert når datepicker åpnes ([#2117](https://github.com/navikt/aksel/pull/2117))
- Updated dependencies []:
- @navikt/aksel-icons@4.7.1
## 4.7.0
### Minor Changes
- Ny komponent Combobox! ([#1868](https://github.com/navikt/aksel/pull/1868))
### Patch Changes
- Timeline: Har nå egen `axisLabelTemplates`-prop for axixlabel formatering ([#2109](https://github.com/navikt/aksel/pull/2109))
- Combobox post-release tweaks ([#2112](https://github.com/navikt/aksel/pull/2112))
- Updated dependencies []:
- @navikt/aksel-icons@4.7.0
## 4.6.1
### Patch Changes
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@navikt/ds-react",
"version": "4.6.1",
"version": "4.12.0",
"description": "Aksel react-components for NAV designsystem",
"author": "Aksel | NAV designsystem team",
"license": "MIT",
@@ -38,7 +38,7 @@
},
"dependencies": {
"@floating-ui/react": "0.24.1",
"@navikt/aksel-icons": "^4.6.1",
"@navikt/aksel-icons": "^4.12.0",
"@radix-ui/react-tabs": "1.0.0",
"@radix-ui/react-toggle-group": "1.0.0",
"clsx": "^1.2.1",
+1
View File
@@ -102,6 +102,7 @@ export const Alert = forwardRef<HTMLDivElement, AlertProps>(
{
"navds-alert--full-width": fullWidth,
"navds-alert--inline": inline,
"navds-alert--close-button": closeButton,
}
)}
>
+11 -1
View File
@@ -185,6 +185,16 @@ export const WithCloseButton: Story = {
</BodyLong>
<Link href="#">Id elit esse enim reprehenderit</Link>
</AlertWithCloseButton>
<AlertWithCloseButton>
<DsHeading spacing size="small" level="3">
Aliquip duis est in commodo pariatur
</DsHeading>
<BodyLong>
Ullamco ullamco laborum et commodo sint culpa cupidatat culpa qui
laboris ex. Labore ex occaecat proident qui qui fugiat magna. Fugiat
sint commodo consequat eu aute.
</BodyLong>
</AlertWithCloseButton>
</div>
);
},
@@ -197,6 +207,6 @@ export const WithCloseButton: Story = {
});
const buttonsAfter = canvas.getAllByTitle("Lukk Alert");
expect(buttonsAfter.length).toBe(1);
expect(buttonsAfter.length).toBe(2);
},
};
+1 -5
View File
@@ -126,11 +126,7 @@ export const Button: OverridableComponent<ButtonProps, HTMLButtonElement> =
<span className="navds-button__icon">{icon}</span>
)}
{children && (
<Label
as="span"
size={size === "medium" ? "medium" : "small"}
aria-live="polite"
>
<Label as="span" size={size === "medium" ? "medium" : "small"}>
{children}
</Label>
)}
@@ -148,8 +148,9 @@ export const Loading = {
</div>
),
args: { chromatic: { disableSnapshot: true } },
chromatic: { disableSnapshot: true },
parameters: {
chromatic: { disableSnapshot: true },
},
};
export const Icon = () => (
@@ -275,6 +276,7 @@ export const LoadingWithAs = {
</div>
),
args: { chromatic: { disableSnapshot: true } },
chromatic: { disableSnapshot: true },
parameters: {
chromatic: { disableSnapshot: true },
},
};
+1 -1
View File
@@ -70,7 +70,7 @@ export const Chips: ChipsComponent = forwardRef<HTMLUListElement, ChipsProps>(
})}
>
{React.Children.map(children, (chip, index) => {
return <li key={index + (chip?.toString() ?? "")}>{chip}</li>;
return <li key={chip?.toString() || index}>{chip}</li>;
})}
</ul>
);
+8 -8
View File
@@ -1,7 +1,6 @@
import { XMarkIcon } from "@navikt/aksel-icons";
import cl from "clsx";
import React, { forwardRef } from "react";
import { OverridableComponent } from "../util/OverridableComponent";
export interface RemovableChipsProps
extends React.ButtonHTMLAttributes<HTMLButtonElement> {
@@ -22,26 +21,27 @@ export interface RemovableChipsProps
onDelete?: () => void;
}
export const RemovableChips: OverridableComponent<
RemovableChipsProps,
HTMLButtonElement
> = forwardRef(
export const RemovableChips = forwardRef<
HTMLButtonElement,
RemovableChipsProps
>(
(
{
className,
children,
variant = "action",
as: Component = "button",
removeLabel = "slett",
onDelete,
type = "button",
...rest
},
ref
) => {
return (
<Component
<button
{...rest}
ref={ref}
type={type}
className={cl(
"navds-chips__chip navds-chips__removable navds-chips--icon-right",
className,
@@ -57,7 +57,7 @@ export const RemovableChips: OverridableComponent<
<span className="navds-chips__removable-icon">
<XMarkIcon aria-hidden />
</span>
</Component>
</button>
);
}
);
@@ -63,6 +63,11 @@ export interface CopyButtonProps
* @default 'Kopiert'
*/
activeTitle?: string;
/**
* Icon position in Button
* @default "left"
*/
iconPosition?: "left" | "right";
}
/**
@@ -91,6 +96,7 @@ export const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
activeDuration = 2000,
title = "Kopier",
activeTitle = "Kopiert",
iconPosition = "left",
...rest
},
ref
@@ -119,6 +125,25 @@ export const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
}, activeDuration);
};
const CopyIcon = () => {
return active ? (
<span className="navds-copybutton__icon">
{activeIcon ?? (
<CheckmarkIcon
aria-hidden={!!text}
title={text ? undefined : activeTitle}
/>
)}
</span>
) : (
<span className="navds-copybutton__icon">
{icon ?? (
<FilesIcon aria-hidden={!!text} title={text ? undefined : title} />
)}
</span>
);
};
return (
<button
ref={ref}
@@ -138,25 +163,7 @@ export const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
onClick={handleClick}
>
<span className="navds-copybutton__content">
{active ? (
<span className="navds-copybutton__icon">
{activeIcon ?? (
<CheckmarkIcon
aria-hidden={!!text}
title={text ? undefined : activeTitle}
/>
)}
</span>
) : (
<span className="navds-copybutton__icon">
{icon ?? (
<FilesIcon
aria-hidden={!!text}
title={text ? undefined : title}
/>
)}
</span>
)}
{iconPosition === "left" && <CopyIcon />}
{text &&
(active ? (
@@ -176,6 +183,7 @@ export const CopyButton = forwardRef<HTMLButtonElement, CopyButtonProps>(
{text}
</Label>
))}
{iconPosition === "right" && <CopyIcon />}
</span>
</button>
);
@@ -60,6 +60,16 @@ export const Variants = {
),
};
export const IconPosition = {
render: () => (
<div className="colgap">
<CopyButton copyText="3.14" iconPosition="left" text="Kopier" />
<CopyButton copyText="3.14" iconPosition="right" text="Kopier" />
</div>
),
};
export const Sizes = {
render: () => (
<div className="colgap">
@@ -166,8 +166,7 @@ export const useDatepicker = (
const handleOpen = useCallback(
(open: boolean) => {
setOpen(open);
!open &&
setMonth(selectedDay ?? defaultSelected ?? defaultMonth ?? today);
open && setMonth(selectedDay ?? defaultSelected ?? defaultMonth ?? today);
},
[defaultMonth, defaultSelected, selectedDay, today]
);
@@ -163,8 +163,7 @@ export const useMonthpicker = (
const handleOpen = useCallback(
(open: boolean) => {
setOpen(open);
!open &&
setYear(selectedMonth ?? defaultSelected ?? defaultYear ?? today);
open && setYear(selectedMonth ?? defaultSelected ?? defaultYear ?? today);
},
[defaultSelected, defaultYear, selectedMonth, today]
);
@@ -1,8 +1,8 @@
import cl from "clsx";
import React, { forwardRef } from "react";
import { OverridableComponent } from "../../../util/OverridableComponent";
import GroupedHeading, { GroupedHeadingProps } from "./Heading";
import GroupedItem, { GroupedItemProps } from "./Item";
import GroupedHeading, { GroupedHeadingProps } from "./GroupedHeading";
import GroupedItem, { GroupedItemProps } from "./GroupedItem";
export interface GroupedListProps
extends React.HTMLAttributes<HTMLDListElement> {
+1 -1
View File
@@ -31,7 +31,7 @@ export interface TextFieldProps
* Type of form control. Picking the correct type helps user fill inn their required information
* @default "text"
*/
type?: "email" | "number" | "password" | "tel" | "text" | "url";
type?: "email" | "number" | "password" | "tel" | "text" | "url" | "time";
}
/**
@@ -86,6 +86,23 @@ export const Checkbox = forwardRef<HTMLInputElement, CheckboxProps>(
}}
/>
<label htmlFor={inputProps.id} className="navds-checkbox__label">
<span className="navds-checkbox__icon">
<svg
xmlns="http://www.w3.org/2000/svg"
width="13"
height="10"
viewBox="0 0 13 10"
fill="none"
focusable={false}
role="img"
aria-hidden
>
<path
d="M4.03524 6.41478L10.4752 0.404669C11.0792 -0.160351 12.029 -0.130672 12.5955 0.47478C13.162 1.08027 13.1296 2.03007 12.5245 2.59621L5.02111 9.59934C4.74099 9.85904 4.37559 10 4.00025 10C3.60651 10 3.22717 9.84621 2.93914 9.56111L0.439143 7.06111C-0.146381 6.47558 -0.146381 5.52542 0.439143 4.93989C1.02467 4.35437 1.97483 4.35437 2.56036 4.93989L4.03524 6.41478Z"
fill="currentColor"
/>
</svg>
</span>
<span
className={cl("navds-checkbox__content", {
"navds-sr-only": props.hideLabel,
@@ -0,0 +1,29 @@
import React from "react";
import { XMarkIcon } from "@navikt/aksel-icons";
interface ClearButtonProps extends React.HTMLAttributes<HTMLButtonElement> {
handleClear: (event: any) => void;
clearButtonLabel?: string;
}
export const ClearButton: React.FC<ClearButtonProps> = ({
handleClear,
clearButtonLabel,
...rest
}) => {
return (
<button
type="button"
onClick={handleClear}
className="navds-combobox__button-clear"
{...rest}
>
<span className="navds-sr-only">
{clearButtonLabel ? clearButtonLabel : "Tøm"}
</span>
<XMarkIcon aria-hidden />
</button>
);
};
export default ClearButton;
@@ -0,0 +1,147 @@
import cl from "clsx";
import React, { forwardRef, useMemo, useRef } from "react";
import { BodyShort, ErrorMessage, Label, mergeRefs } from "../..";
import ClearButton from "./ClearButton";
import FilteredOptions from "./FilteredOptions/FilteredOptions";
import { useFilteredOptionsContext } from "./FilteredOptions/filteredOptionsContext";
import SelectedOptions from "./SelectedOptions/SelectedOptions";
import ToggleListButton from "./ToggleListButton";
import { ComboboxProps } from "./types";
import { useSelectedOptionsContext } from "./SelectedOptions/selectedOptionsContext";
import ComboboxWrapper from "./ComboboxWrapper";
import { useInputContext } from "./Input/inputContext";
import Input from "./Input/Input";
export const Combobox = forwardRef<
HTMLInputElement,
Omit<ComboboxProps, "onChange" | "options" | "size">
>((props, ref) => {
const {
value: externalValue,
onClear,
className,
hideLabel = false,
description,
label,
clearButton = true,
clearButtonLabel,
toggleListButton = true,
toggleListButtonLabel,
inputClassName,
shouldShowSelectedOptions = true,
...rest
} = props;
const toggleListButtonRef = useRef<HTMLButtonElement>(null);
const { currentOption, toggleIsListOpen } = useFilteredOptionsContext();
const { selectedOptions } = useSelectedOptionsContext();
const {
clearInput,
error,
errorId,
focusInput,
hasError,
inputDescriptionId,
inputProps,
inputRef,
value,
showErrorMsg,
size = "medium",
} = useInputContext();
const mergedInputRef = useMemo(
() => mergeRefs([inputRef, ref]),
[inputRef, ref]
);
return (
<ComboboxWrapper
className={className}
hasError={hasError}
inputProps={inputProps}
inputSize={size}
toggleIsListOpen={toggleIsListOpen}
toggleListButtonRef={toggleListButtonRef}
>
<Label
htmlFor={inputProps.id}
size={size}
className={cl("navds-form-field__label", {
"navds-sr-only": hideLabel,
})}
>
{label}
</Label>
{!!description && (
<BodyShort
as="div"
className={cl("navds-form-field__description", {
"navds-sr-only": hideLabel,
})}
id={inputDescriptionId}
size={size}
>
{description}
</BodyShort>
)}
<div className="navds-combobox__wrapper">
<div
className={cl(
"navds-combobox__wrapper-inner navds-text-field__input",
{
"navds-combobox__wrapper-inner--virtually-unfocused":
currentOption !== null,
}
)}
onClick={focusInput}
>
{!shouldShowSelectedOptions ? (
<Input
id={inputProps.id}
ref={mergedInputRef}
inputClassName={inputClassName}
{...rest}
/>
) : (
<SelectedOptions selectedOptions={selectedOptions} size={size}>
<Input
id={inputProps.id}
ref={mergedInputRef}
inputClassName={inputClassName}
{...rest}
/>
</SelectedOptions>
)}
<div>
{value && clearButton && (
<ClearButton
handleClear={clearInput}
clearButtonLabel={clearButtonLabel}
tabIndex={-1}
/>
)}
{toggleListButton && (
<ToggleListButton
toggleListButtonLabel={toggleListButtonLabel}
ref={toggleListButtonRef}
/>
)}
</div>
</div>
<FilteredOptions />
</div>
<div
className="navds-form-field__error"
id={errorId}
aria-relevant="additions removals"
aria-live="polite"
>
{showErrorMsg && <ErrorMessage size={size}>{error}</ErrorMessage>}
</div>
</ComboboxWrapper>
);
});
export default Combobox;
@@ -0,0 +1,99 @@
import React, { forwardRef } from "react";
import Combobox from "./Combobox";
import { FilteredOptionsProvider } from "./FilteredOptions/filteredOptionsContext";
import { CustomOptionsProvider } from "./customOptionsContext";
import { SelectedOptionsProvider } from "./SelectedOptions/selectedOptionsContext";
import { InputContextProvider } from "./Input/inputContext";
import { ComboboxProps } from "./types";
/**
* A component that allows the user to search in a list of options
*
* Has options for allowing only one or multiple options to be selected,
* or adding new, user-submitted values.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/combobox)
*
* @example
* ```jsx
* const options = ["apple", "banana", "orange"];
*
* return (
* <Combobox
* label="Velg en verdi"
* options={options}
* id="my-combobox"
* shouldAutoComplete
* />
* )
* ```
*/
const ComboboxProvider = forwardRef<HTMLInputElement, ComboboxProps>(
(props, ref) => {
const {
allowNewValues = false,
children,
defaultValue,
error,
errorId,
filteredOptions,
id,
isListOpen,
isLoading = false,
isMultiSelect,
onToggleSelected,
selectedOptions,
options,
value,
onChange,
onClear,
shouldAutocomplete,
size,
...rest
} = props;
return (
<InputContextProvider
value={{
defaultValue,
error,
errorId,
id,
value,
onChange,
onClear,
shouldAutocomplete,
size,
}}
>
<CustomOptionsProvider>
<SelectedOptionsProvider
value={{
allowNewValues,
isMultiSelect,
selectedOptions,
onToggleSelected,
options,
}}
>
<FilteredOptionsProvider
value={{
allowNewValues,
filteredOptions,
isListOpen,
isLoading,
isMultiSelect,
options,
}}
>
<Combobox ref={ref} {...rest}>
{children}
</Combobox>
</FilteredOptionsProvider>
</SelectedOptionsProvider>
</CustomOptionsProvider>
</InputContextProvider>
);
}
);
export default ComboboxProvider;
@@ -0,0 +1,68 @@
import cl from "clsx";
import React, { useRef, useState } from "react";
type ComboboxWrapperProps = {
children: any;
className?: string;
hasError: boolean;
inputProps: {
disabled?: boolean;
};
inputSize: string;
toggleIsListOpen: (isListOpen: boolean) => void;
toggleListButtonRef: React.RefObject<HTMLButtonElement>;
};
const ComboboxWrapper = ({
children,
className,
hasError,
inputProps,
inputSize,
toggleIsListOpen,
toggleListButtonRef,
}: ComboboxWrapperProps) => {
const wrapperRef = useRef<HTMLDivElement | null>(null);
const [hasFocusWithin, setHasFocusWithin] = useState(false);
function onFocusInsideWrapper(e) {
if (
!wrapperRef.current?.contains(e.relatedTarget) &&
toggleListButtonRef?.current !== e.target
) {
toggleIsListOpen(true);
setHasFocusWithin(true);
}
}
function onBlurWrapper(e) {
if (!wrapperRef.current?.contains(e.relatedTarget)) {
toggleIsListOpen(false);
setHasFocusWithin(false);
}
}
return (
<div
ref={wrapperRef}
className={cl(
className,
"navds-form-field",
`navds-form-field--${inputSize}`,
"navds-search",
{
"navds-search--error": hasError,
"navds-search--disabled": !!inputProps.disabled,
"navds-combobox--focused": hasFocusWithin,
}
)}
onFocus={onFocusInsideWrapper}
onBlur={onBlurWrapper}
tabIndex={-1}
>
{children}
</div>
);
};
export default ComboboxWrapper;
@@ -0,0 +1,23 @@
import React from "react";
const CheckIcon = () => {
return (
<svg
width="16"
height="13"
viewBox="0 0 16 13"
fill="none"
xmlns="http://www.w3.org/2000/svg"
aria-hidden="true"
>
<path
fillRule="evenodd"
clipRule="evenodd"
fill="#005B82"
d="M14.2014 0L16 1.89047L4.77943 13L0 8.39552L1.79361 6.5L4.77418 9.3019L14.2014 0Z"
/>
</svg>
);
};
export default CheckIcon;
@@ -0,0 +1,109 @@
import React from "react";
import cl from "clsx";
import { BodyShort, Label, Loader } from "../../..";
import { CheckmarkIcon, PlusIcon } from "@navikt/aksel-icons";
import { useFilteredOptionsContext } from "./filteredOptionsContext";
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
import { useInputContext } from "../Input/inputContext";
const FilteredOptions = () => {
const {
inputProps: { id },
size,
value,
} = useInputContext();
const {
allowNewValues,
isLoading,
isListOpen,
filteredOptions,
filteredOptionsIndex,
filteredOptionsRef,
isValueNew,
toggleIsListOpen,
} = useFilteredOptionsContext();
const { isMultiSelect, selectedOptions, toggleOption } =
useSelectedOptionsContext();
return (
<ul
ref={filteredOptionsRef}
className={cl("navds-combobox__list", {
"navds-combobox__list--closed": !isListOpen,
})}
id={`${id}-filtered-options`}
role="listbox"
tabIndex={-1}
>
{isLoading && (
<li
className="navds-combobox__list-item--loading"
role="option"
aria-selected={false}
id={`${id}-is-loading`}
>
<Loader aria-label="Søker..." />
</li>
)}
{isValueNew && allowNewValues && (
<li
tabIndex={-1}
onPointerUp={(event) => {
toggleOption(value, event);
if (!isMultiSelect && !selectedOptions.includes(value))
toggleIsListOpen(false);
}}
id={`${id}-combobox-new-option`}
className={cl("navds-combobox__list-item__new-option", {
"navds-combobox__list-item__new-option--focus":
filteredOptionsIndex === -1,
})}
role="option"
aria-selected={false}
>
<PlusIcon aria-hidden />
<BodyShort size={size}>
Legg til{" "}
<Label as="span" size={size}>
&#8220;{value}&#8221;
</Label>
</BodyShort>
</li>
)}
{!isLoading && filteredOptions.length === 0 && (
<li
className="navds-combobox__list-item__no-options"
role="option"
aria-selected={false}
id={`${id}-no-hits`}
>
Ingen søketreff
</li>
)}
{filteredOptions.map((option, index) => (
<li
className={cl("navds-combobox__list-item", {
"navds-combobox__list-item--focus": index === filteredOptionsIndex,
"navds-combobox__list-item--selected":
selectedOptions.includes(option),
})}
id={`${id}-option-${option.replace(" ", "-")}`}
key={option}
tabIndex={-1}
onPointerUp={(event) => {
toggleOption(option, event);
if (!isMultiSelect && !selectedOptions.includes(option))
toggleIsListOpen(false);
}}
role="option"
aria-selected={selectedOptions.includes(option)}
>
<BodyShort size={size}>{option}</BodyShort>
{selectedOptions.includes(option) && <CheckmarkIcon />}
</li>
))}
</ul>
);
};
export default FilteredOptions;
@@ -0,0 +1,275 @@
import React, {
useState,
useEffect,
useMemo,
createContext,
useContext,
useCallback,
useRef,
useLayoutEffect,
} from "react";
import cl from "clsx";
import { useCustomOptionsContext } from "../customOptionsContext";
import { useInputContext } from "../Input/inputContext";
import usePrevious from "../../../util/usePrevious";
const normalizeText = (text: string): string =>
typeof text === "string" ? `${text}`.toLowerCase().trim() : "";
const isPartOfText = (value, text) =>
normalizeText(text).startsWith(normalizeText(value ?? ""));
const isValueInList = (value, list) =>
list?.find((listItem) => normalizeText(value) === normalizeText(listItem));
const getMatchingValuesFromList = (value, list) =>
list?.filter((listItem) => isPartOfText(value, listItem));
type FilteredOptionsContextType = {
activeDecendantId?: string;
allowNewValues?: boolean;
ariaDescribedBy?: string;
filteredOptionsRef: React.RefObject<HTMLUListElement>;
filteredOptionsIndex: number | null;
setFilteredOptionsIndex: (index: number) => void;
isListOpen: boolean;
isLoading?: boolean;
filteredOptions: string[];
isValueNew: boolean;
toggleIsListOpen: (newState?: boolean) => void;
currentOption: string | null;
resetFilteredOptionsIndex: () => void;
moveFocusUp: () => void;
moveFocusDown: () => void;
moveFocusToInput: () => void;
moveFocusToEnd: () => void;
shouldAutocomplete?: boolean;
};
const FilteredOptionsContext = createContext<FilteredOptionsContextType>(
{} as FilteredOptionsContextType
);
export const FilteredOptionsProvider = ({ children, value: props }) => {
const {
allowNewValues,
filteredOptions: externalFilteredOptions,
isListOpen: isExternalListOpen,
isLoading,
options,
} = props;
const filteredOptionsRef = useRef<HTMLUListElement | null>(null);
const {
inputProps: { "aria-describedby": partialAriaDescribedBy, id },
value,
searchTerm,
setValue,
setSearchTerm,
shouldAutocomplete,
} = useInputContext();
const [filteredOptionsIndex, setFilteredOptionsIndex] = useState<
number | null
>(null);
const [isInternalListOpen, setInternalListOpen] = useState(false);
const { customOptions } = useCustomOptionsContext();
const filteredOptions = useMemo(() => {
if (externalFilteredOptions) {
return externalFilteredOptions;
}
const opts = [...customOptions, ...options];
setFilteredOptionsIndex(null);
return getMatchingValuesFromList(searchTerm, opts);
}, [customOptions, externalFilteredOptions, options, searchTerm]);
const previousSearchTerm = usePrevious(searchTerm);
useLayoutEffect(() => {
if (
shouldAutocomplete &&
normalizeText(searchTerm) !== "" &&
(previousSearchTerm?.length || 0) < searchTerm.length &&
filteredOptions.length > 0 &&
!isValueInList(searchTerm, filteredOptions)
) {
setValue(
`${searchTerm}${filteredOptions[0].substring(searchTerm.length)}`
);
setSearchTerm(searchTerm);
}
}, [
filteredOptions,
previousSearchTerm,
searchTerm,
setSearchTerm,
setValue,
shouldAutocomplete,
]);
const isListOpen = useMemo(() => {
return isExternalListOpen ?? isInternalListOpen;
}, [isExternalListOpen, isInternalListOpen]);
const toggleIsListOpen = useCallback((newState?: boolean) => {
setFilteredOptionsIndex(null);
setInternalListOpen((oldState) => newState ?? !oldState);
}, []);
const isValueNew = useMemo(
() => Boolean(value) && !isValueInList(value, filteredOptions),
[value, filteredOptions]
);
const getMinimumIndex = useCallback(() => {
return isValueNew && allowNewValues ? -1 : 0;
}, [allowNewValues, isValueNew]);
const ariaDescribedBy = useMemo(() => {
let activeOption;
if (!isLoading && filteredOptions.length === 0) {
activeOption = `${id}-no-hits`;
} else if ((value && value !== "") || isLoading) {
if (shouldAutocomplete && filteredOptions[0]) {
activeOption = `${id}-option-${filteredOptions[0].replace(" ", "-")}`;
} else if (isListOpen && isLoading) {
activeOption = `${id}-is-loading`;
}
}
return cl(activeOption, partialAriaDescribedBy) || undefined;
}, [
isListOpen,
isLoading,
value,
partialAriaDescribedBy,
shouldAutocomplete,
filteredOptions,
id,
]);
const currentOption = useMemo(() => {
if (filteredOptionsIndex == null) {
return null;
}
if (filteredOptionsIndex === -1) {
return value;
}
return filteredOptions[filteredOptionsIndex];
}, [filteredOptionsIndex, filteredOptions, value]);
const resetFilteredOptionsIndex = () => {
setFilteredOptionsIndex(getMinimumIndex());
};
const scrollToOption = useCallback((newIndex: number) => {
if (
filteredOptionsRef.current &&
filteredOptionsRef.current.children[newIndex]
) {
const child = filteredOptionsRef.current.children[newIndex];
const { top, bottom } = child.getBoundingClientRect();
const parentRect = filteredOptionsRef.current.getBoundingClientRect();
if (top < parentRect.top || bottom > parentRect.bottom) {
child.scrollIntoView({ block: "nearest" });
}
}
}, []);
useEffect(() => {
if (filteredOptionsIndex !== null && isListOpen) {
scrollToOption(filteredOptionsIndex);
}
}, [filteredOptionsIndex, isListOpen, scrollToOption]);
const moveFocusToInput = useCallback(() => {
setFilteredOptionsIndex(null);
toggleIsListOpen(false);
}, [toggleIsListOpen]);
const moveFocusToEnd = useCallback(() => {
const lastIndex = filteredOptions.length - 1;
toggleIsListOpen(true);
setFilteredOptionsIndex(lastIndex);
}, [filteredOptions.length, toggleIsListOpen]);
const moveFocusUp = useCallback(() => {
if (filteredOptionsIndex === null) {
return;
}
if (filteredOptionsIndex === getMinimumIndex()) {
toggleIsListOpen(false);
setFilteredOptionsIndex(null);
} else {
const newIndex = Math.max(getMinimumIndex(), filteredOptionsIndex - 1);
setFilteredOptionsIndex(newIndex);
}
}, [filteredOptionsIndex, getMinimumIndex, toggleIsListOpen]);
const moveFocusDown = useCallback(() => {
if (filteredOptionsIndex === null || !isListOpen) {
toggleIsListOpen(true);
if (allowNewValues || filteredOptions.length >= 1) {
setFilteredOptionsIndex(getMinimumIndex());
}
return;
}
const newIndex = Math.min(
filteredOptionsIndex + 1,
Math.max(getMinimumIndex(), filteredOptions.length - 1)
);
setFilteredOptionsIndex(newIndex);
}, [
allowNewValues,
filteredOptions.length,
filteredOptionsIndex,
getMinimumIndex,
isListOpen,
toggleIsListOpen,
]);
const activeDecendantId = useMemo(() => {
if (filteredOptionsIndex === null) {
return undefined;
} else if (filteredOptionsIndex === -1) {
return `${id}-combobox-new-option`;
} else {
return `${id}-option-${currentOption?.replace(" ", "-")}`;
}
}, [filteredOptionsIndex, currentOption, id]);
const filteredOptionsState = {
activeDecendantId,
allowNewValues,
filteredOptionsRef,
filteredOptionsIndex,
setFilteredOptionsIndex,
shouldAutocomplete,
isListOpen,
isLoading,
filteredOptions,
isValueNew,
toggleIsListOpen,
currentOption,
resetFilteredOptionsIndex,
moveFocusUp,
moveFocusDown,
moveFocusToInput,
moveFocusToEnd,
ariaDescribedBy,
};
return (
<FilteredOptionsContext.Provider value={filteredOptionsState}>
{children}
</FilteredOptionsContext.Provider>
);
};
export const useFilteredOptionsContext = () => {
const context = useContext(FilteredOptionsContext);
if (!context) {
throw new Error(
"useFilteredOptionsContext must be used within a FilteredOptionsProvider"
);
}
return context;
};
@@ -0,0 +1,185 @@
import { omit } from "../../..";
import React, {
useCallback,
forwardRef,
InputHTMLAttributes,
ChangeEvent,
} from "react";
import cl from "clsx";
import { useSelectedOptionsContext } from "../SelectedOptions/selectedOptionsContext";
import { useFilteredOptionsContext } from "../FilteredOptions/filteredOptionsContext";
import { useInputContext } from "./inputContext";
interface InputProps
extends Omit<InputHTMLAttributes<HTMLInputElement>, "value"> {
ref: React.Ref<HTMLInputElement>;
inputClassName?: string;
errorId?: string;
value?: string;
error?: React.ReactNode;
}
const Input = forwardRef<HTMLInputElement, InputProps>(
({ inputClassName, error, errorId, ...rest }, ref) => {
const { clearInput, inputProps, onChange, size, value } = useInputContext();
const {
selectedOptions,
removeSelectedOption,
toggleOption,
isMultiSelect,
} = useSelectedOptionsContext();
const {
activeDecendantId,
allowNewValues,
currentOption,
filteredOptions,
toggleIsListOpen,
isListOpen,
filteredOptionsIndex,
moveFocusUp,
moveFocusDown,
ariaDescribedBy,
moveFocusToInput,
moveFocusToEnd,
setFilteredOptionsIndex,
shouldAutocomplete,
} = useFilteredOptionsContext();
const onEnter = useCallback(
(event: React.KeyboardEvent) => {
if (currentOption) {
event.preventDefault();
// Selecting a value from the dropdown / FilteredOptions
toggleOption(currentOption, event);
if (!isMultiSelect && !selectedOptions.includes(currentOption))
toggleIsListOpen(false);
} else if (shouldAutocomplete && selectedOptions.includes(value)) {
event.preventDefault();
// Trying to set the same value that is already set, so just clearing the input
clearInput(event);
} else if ((allowNewValues || shouldAutocomplete) && value !== "") {
event.preventDefault();
// Autocompleting or adding a new value
toggleOption(value, event);
if (!isMultiSelect && !selectedOptions.includes(value))
toggleIsListOpen(false);
}
},
[
allowNewValues,
clearInput,
currentOption,
isMultiSelect,
selectedOptions,
shouldAutocomplete,
toggleIsListOpen,
toggleOption,
value,
]
);
const handleKeyUp = (e: React.KeyboardEvent<HTMLInputElement>) => {
e.preventDefault();
switch (e.key) {
case "Escape":
clearInput(e);
toggleIsListOpen(false);
break;
case "Enter":
case "Accept":
onEnter(e);
break;
case "Home":
moveFocusToInput();
break;
case "End":
moveFocusToEnd();
break;
default:
break;
}
};
const handleKeyDown = useCallback(
(e) => {
if (e.key === "Backspace") {
if (value === "") {
const lastSelectedOption =
selectedOptions[selectedOptions.length - 1];
removeSelectedOption(lastSelectedOption);
}
} else if (e.key === "ArrowDown") {
// Check that cursor position is at the end of the input field,
// so we don't interfere with text editing
if (e.target.selectionStart === value?.length) {
e.preventDefault();
moveFocusDown();
}
} else if (e.key === "ArrowUp") {
// Check that the FilteredOptions list is open and has virtual focus.
// Otherwise ignore keystrokes, so it doesn't interfere with text editing
if (isListOpen && filteredOptionsIndex !== null) {
e.preventDefault();
moveFocusUp();
}
}
},
[
value,
selectedOptions,
removeSelectedOption,
moveFocusDown,
isListOpen,
filteredOptionsIndex,
moveFocusUp,
]
);
const onChangeHandler = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const newValue = event.target.value;
if (newValue && newValue !== "") {
toggleIsListOpen(true);
} else if (filteredOptions.length === 0) {
toggleIsListOpen(false);
}
onChange(event);
},
[filteredOptions.length, onChange, toggleIsListOpen]
);
const onBlur = () => {
setFilteredOptionsIndex(-1);
};
return (
<input
{...rest}
{...omit(inputProps, ["aria-invalid"])}
ref={ref}
value={value}
onChange={onChangeHandler}
type="text"
role="combobox"
onBlur={onBlur}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
aria-controls={`${inputProps.id}-filtered-options`}
aria-expanded={!!isListOpen}
autoComplete="off"
aria-autocomplete={shouldAutocomplete ? "both" : "list"}
aria-activedescendant={activeDecendantId}
aria-describedby={ariaDescribedBy}
aria-invalid={inputProps["aria-invalid"]}
className={cl(
inputClassName,
"navds-combobox__input",
"navds-body-short",
`navds-body-${size}`
)}
/>
);
}
);
export default Input;
@@ -0,0 +1,130 @@
import React, {
ChangeEvent,
ChangeEventHandler,
createContext,
useCallback,
useContext,
useLayoutEffect,
useMemo,
useRef,
useState,
} from "react";
import { useFormField, FormFieldType } from "../../useFormField";
interface InputContextType extends FormFieldType {
clearInput: (event: React.PointerEvent | React.KeyboardEvent) => void;
error?: string;
focusInput: () => void;
inputRef: React.RefObject<HTMLInputElement>;
value: string;
setValue: (text: string) => void;
onChange: ChangeEventHandler<HTMLInputElement>;
searchTerm: string;
setSearchTerm: React.Dispatch<React.SetStateAction<string>>;
shouldAutocomplete?: boolean;
}
const InputContext = createContext<InputContextType>({} as InputContextType);
export const InputContextProvider = ({ children, value: props }) => {
const {
defaultValue = "",
description,
disabled,
error,
errorId,
id: externalId,
value: externalValue,
onChange: externalOnChange,
onClear,
shouldAutocomplete,
size,
} = props;
const formFieldProps = useFormField(
{
description,
disabled,
error,
errorId,
id: externalId,
size,
},
"comboboxfield"
);
const inputRef = useRef<HTMLInputElement | null>(null);
const [internalValue, setInternalValue] = useState<string>(defaultValue);
const value = useMemo(
() => String(externalValue ?? internalValue),
[externalValue, internalValue]
);
const [searchTerm, setSearchTerm] = useState(value);
const onChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
const value = event.currentTarget.value;
externalValue ?? setInternalValue(value);
externalOnChange?.(event);
setSearchTerm(value);
},
[externalValue, externalOnChange]
);
const setValue = useCallback(
(text) => {
setInternalValue(text);
},
[setInternalValue]
);
const clearInput = useCallback(
(event: React.PointerEvent | React.KeyboardEvent) => {
onClear?.(event);
externalOnChange?.(null, "");
setValue("");
setSearchTerm("");
},
[externalOnChange, onClear, setValue]
);
const focusInput = useCallback(() => {
inputRef.current?.focus?.();
}, []);
useLayoutEffect(() => {
if (shouldAutocomplete && inputRef && value !== searchTerm) {
inputRef.current?.setSelectionRange?.(searchTerm.length, value.length);
}
}, [value, searchTerm, shouldAutocomplete]);
return (
<InputContext.Provider
value={{
...formFieldProps,
clearInput,
error,
focusInput,
inputRef,
value,
setValue,
onChange,
searchTerm,
setSearchTerm,
shouldAutocomplete,
}}
>
{children}
</InputContext.Provider>
);
};
export const useInputContext = () => {
const context = useContext(InputContext);
if (!context) {
throw new Error(
"useInputContext must be used within an InputContextProvider"
);
}
return context;
};
@@ -0,0 +1,48 @@
import React from "react";
import { Chips } from "../../..";
import { useSelectedOptionsContext } from "./selectedOptionsContext";
import { useInputContext } from "../Input/inputContext";
interface SelectedOptionsProps {
selectedOptions?: string[];
size?: "medium" | "small";
children: React.ReactNode;
}
const Option = ({ option }: { option: string }) => {
const { isMultiSelect, removeSelectedOption } = useSelectedOptionsContext();
const { focusInput } = useInputContext();
const onClick = (e) => {
e.stopPropagation();
removeSelectedOption(option);
focusInput();
};
if (!isMultiSelect) {
return (
<div className="navds-combobox__selected-options--no-bg">{option}</div>
);
}
return <Chips.Removable onClick={onClick}>{option}</Chips.Removable>;
};
const SelectedOptions: React.FC<SelectedOptionsProps> = ({
selectedOptions = [],
size,
children,
}) => {
return (
<Chips className="navds-combobox__selected-options" size={size}>
{selectedOptions.length
? selectedOptions.map((option, i) => (
<Option key={option + i} option={option} />
))
: []}
{children}
</Chips>
);
};
export default SelectedOptions;
@@ -0,0 +1,157 @@
import React, {
createContext,
useCallback,
useContext,
useMemo,
useState,
} from "react";
import usePrevious from "../../../util/usePrevious";
import { useInputContext } from "../Input/inputContext";
import { ComboboxProps } from "../types";
import { useCustomOptionsContext } from "../customOptionsContext";
type SelectedOptionsContextType = {
addSelectedOption: (option: string) => void;
isMultiSelect?: boolean;
removeSelectedOption: (option: string) => void;
prevSelectedOptions?: string[];
selectedOptions: string[];
setSelectedOptions: (any) => void;
toggleOption: (
option: string,
event: React.KeyboardEvent | React.PointerEvent
) => void;
};
const SelectedOptionsContext = createContext<SelectedOptionsContextType>(
{} as SelectedOptionsContextType
);
export const SelectedOptionsProvider = ({
children,
value,
}: {
children: any;
value: Pick<
ComboboxProps,
| "allowNewValues"
| "isMultiSelect"
| "options"
| "selectedOptions"
| "onToggleSelected"
>;
}) => {
const { clearInput, focusInput } = useInputContext();
const {
customOptions,
removeCustomOption,
addCustomOption,
setCustomOptions,
} = useCustomOptionsContext();
const {
allowNewValues,
isMultiSelect,
selectedOptions: externalSelectedOptions,
onToggleSelected,
options,
} = value;
const [internalSelectedOptions, setSelectedOptions] = useState<string[]>([]);
const selectedOptions = useMemo(
() =>
externalSelectedOptions ?? [...customOptions, ...internalSelectedOptions],
[customOptions, externalSelectedOptions, internalSelectedOptions]
);
const addSelectedOption = useCallback(
(option: string) => {
const isCustomOption = !options
.map((opt) => opt.toLowerCase())
.includes(option?.toLowerCase?.());
if (isCustomOption) {
allowNewValues && addCustomOption(option);
!isMultiSelect && setSelectedOptions([]);
} else if (isMultiSelect) {
setSelectedOptions((prevSelectedOptions) => [
...prevSelectedOptions,
option,
]);
} else {
setSelectedOptions([option]);
setCustomOptions([]);
}
onToggleSelected?.(option, true, isCustomOption);
},
[
addCustomOption,
allowNewValues,
isMultiSelect,
onToggleSelected,
options,
setCustomOptions,
]
);
const removeSelectedOption = useCallback(
(option: string) => {
const isCustomOption = customOptions.includes(option);
if (isCustomOption) {
removeCustomOption(option);
} else {
setSelectedOptions((prevSelectedOptions) =>
prevSelectedOptions.filter(
(selectedOption) => selectedOption !== option
)
);
}
onToggleSelected?.(option, false, isCustomOption);
},
[customOptions, onToggleSelected, removeCustomOption]
);
const toggleOption = useCallback(
(option: string, event: React.KeyboardEvent | React.PointerEvent) => {
if (selectedOptions.includes(option)) {
removeSelectedOption(option);
} else {
addSelectedOption(option);
}
clearInput(event);
focusInput();
},
[
addSelectedOption,
clearInput,
focusInput,
removeSelectedOption,
selectedOptions,
]
);
const prevSelectedOptions = usePrevious<string[]>(selectedOptions);
const selectedOptionsState = {
addSelectedOption,
isMultiSelect,
removeSelectedOption,
prevSelectedOptions,
selectedOptions,
setSelectedOptions,
toggleOption,
};
return (
<SelectedOptionsContext.Provider value={selectedOptionsState}>
{children}
</SelectedOptionsContext.Provider>
);
};
export const useSelectedOptionsContext = () => {
const context = useContext(SelectedOptionsContext);
if (!context) {
throw new Error(
"useSelectedOptionsContext must be used within a SelectedOptionsProvider"
);
}
return context;
};
@@ -0,0 +1,37 @@
import React, { forwardRef } from "react";
import { ChevronUpIcon, ChevronDownIcon } from "@navikt/aksel-icons";
import { useFilteredOptionsContext } from "./FilteredOptions/filteredOptionsContext";
interface ToggleListButtonProps {
toggleListButtonLabel?: string;
}
export const ToggleListButton = forwardRef<
HTMLButtonElement,
ToggleListButtonProps
>(({ toggleListButtonLabel }, ref) => {
const { isListOpen, toggleIsListOpen } = useFilteredOptionsContext();
return (
<button
type="button"
onPointerUp={() => toggleIsListOpen()}
onKeyDown={({ key }) => key === "Enter" && toggleIsListOpen()}
className="navds-combobox__button-toggle-list"
aria-expanded={isListOpen}
tabIndex={-1}
ref={ref}
>
<span className="navds-sr-only">
{toggleListButtonLabel ?? "Alternativer"}
</span>
{isListOpen ? (
<ChevronUpIcon aria-hidden />
) : (
<ChevronDownIcon aria-hidden />
)}
</button>
);
});
export default ToggleListButton;
@@ -0,0 +1,511 @@
/* eslint-disable react-hooks/rules-of-hooks */
import { Meta } from "@storybook/react";
import React, { useState, useId, useMemo } from "react";
import { userEvent, within } from "@storybook/testing-library";
import { Chips, UNSAFE_Combobox, TextField } from "../../index";
import { expect, jest } from "@storybook/jest";
export default {
title: "ds-react/Combobox",
component: UNSAFE_Combobox,
argTypes: {
isListOpen: {
control: {
type: "boolean",
},
},
isLoading: {
control: {
type: "boolean",
},
},
},
} as Meta;
const options = [
"banana",
"apple",
"apple pie",
"tangerine",
"pear",
"grape",
"kiwi",
"mango",
"passion fruit",
"pineapple",
"strawberry",
"watermelon",
"grape fruit",
];
const initialSelectedOptions = ["passion fruit", "grape fruit"];
const DemoContainer = ({
dataTheme,
children,
}: {
children: any;
dataTheme: "dark" | "light";
}) => (
<div data-theme={dataTheme} style={{ width: "300px" }}>
{children}
</div>
);
export const Default = (props) => {
const id = useId();
return (
<DemoContainer dataTheme={props.darkMode}>
<UNSAFE_Combobox
options={props.options}
label="Hva er dine favorittfrukter?"
/* everything under here is optional? */
shouldAutocomplete={props.shouldAutoComplete}
size="medium"
id={id}
/>
</DemoContainer>
);
};
Default.args = {
options,
shouldAutoComplete: true,
};
export function MultiSelect(props) {
const id = useId();
return (
<DemoContainer dataTheme={props.darkMode}>
<UNSAFE_Combobox
id={id}
label="Komboboks - velg flere"
options={props.options}
isMultiSelect={props.isMultiSelect}
size={props.size}
/>
</DemoContainer>
);
}
MultiSelect.args = {
options,
isMultiSelect: true,
size: "medium",
};
export function WithAddNewOptions(props) {
const id = useId();
return (
<DemoContainer dataTheme={props.darkMode}>
<UNSAFE_Combobox
id={id}
label="Komboboks med mulighet for å legge til nye verdier"
options={props.options}
allowNewValues={props.allowNewValues}
shouldAutocomplete={props.shouldAutoComplete}
/>
</DemoContainer>
);
}
WithAddNewOptions.args = {
options,
allowNewValues: true,
shouldAutoComplete: true,
};
export function MultiSelectWithAddNewOptions(props) {
const id = useId();
return (
<DemoContainer dataTheme={props.darkMode}>
<UNSAFE_Combobox
id={id}
isMultiSelect={props.isMultiSelect}
label="Multiselect komboboks med mulighet for å legge til nye verdier"
options={props.options}
allowNewValues={props.allowNewValues}
/>
</DemoContainer>
);
}
MultiSelectWithAddNewOptions.args = {
allowNewValues: true,
isMultiSelect: true,
options,
shouldAutocomplete: false,
};
export const MultiSelectWithExternalChips = (props) => {
const [selectedOptions, setSelectedOptions] = useState<string[]>(
props.selectedOptions
);
const [value, setValue] = useState("");
const id = useId();
const toggleSelected = (option) =>
selectedOptions.includes(option)
? setSelectedOptions(selectedOptions.filter((opt) => opt !== option))
: setSelectedOptions([...selectedOptions, option]);
return (
<DemoContainer dataTheme={props.darkMode}>
{selectedOptions && (
<Chips>
{selectedOptions.map((option) => (
<Chips.Removable
key={option}
onPointerUp={() => toggleSelected(option)}
onKeyUp={(e) => e.key === "Enter" && toggleSelected(option)}
>
{option}
</Chips.Removable>
))}
</Chips>
)}
<UNSAFE_Combobox
options={options}
selectedOptions={selectedOptions}
onToggleSelected={(option: string) => toggleSelected(option)}
isListOpen={props.isListOpen}
isMultiSelect
value={props.controlled ? value : undefined}
onChange={(event) =>
props.controlled ? setValue(event.currentTarget.value) : undefined
}
label="Komboboks"
size="medium"
error={props.error && "error here"}
id={id}
shouldShowSelectedOptions={false}
/>
</DemoContainer>
);
};
MultiSelectWithExternalChips.args = {
controlled: false,
options,
selectedOptions: [],
};
export function Loading(props) {
const id = useId();
return (
<DemoContainer dataTheme={props.darkMode}>
<UNSAFE_Combobox
id={id}
label="Komboboks (laster)"
options={[]}
selectedOptions={[]}
isListOpen={props.isListOpen}
isLoading={props.isLoading}
/>
</DemoContainer>
);
}
Loading.args = {
isLoading: true,
isListOpen: true,
};
export function ComboboxWithNoHits(props) {
const id = useId();
const [value, setValue] = useState(props.value);
return (
<DemoContainer dataTheme={props.darkMode}>
<UNSAFE_Combobox
id={id}
label="Komboboks (uten søketreff)"
options={props.options}
value={value}
onChange={(event) => setValue(event.currentTarget.value)}
isListOpen={true}
/>
</DemoContainer>
);
}
ComboboxWithNoHits.args = {
options,
value: "Orange",
};
export const Controlled = (props) => {
const id = useId();
const [value, setValue] = useState(props.value);
const [selectedOptions, setSelectedOptions] = useState(props.selectedOptions);
const filteredOptions = useMemo(
() => props.options.filter((option) => option.includes(value)),
[props.options, value]
);
const onToggleSelected = (option, isSelected) => {
if (isSelected) {
setSelectedOptions([...selectedOptions, option]);
} else {
setSelectedOptions(selectedOptions.filter((o) => o !== option));
}
};
return (
<DemoContainer dataTheme={props.darkMode}>
<TextField
label="Overstyr value"
onChange={(event) => setValue(event.target.value)}
value={value}
/>
<br />
<UNSAFE_Combobox
label="Hva er dine favorittfrukter?"
id={id}
filteredOptions={filteredOptions}
isMultiSelect
options={props.options}
onChange={(event) => setValue(event.target.value)}
onToggleSelected={onToggleSelected}
selectedOptions={selectedOptions}
value={value}
/>
</DemoContainer>
);
};
Controlled.args = {
value: "apple",
options,
selectedOptions: initialSelectedOptions,
};
export const ComboboxSizes = (props) => (
<DemoContainer dataTheme={props.darkMode}>
<UNSAFE_Combobox
label="Hva er dine favorittfrukter?"
description="Medium single-select"
options={options}
/>
<br />
<UNSAFE_Combobox
label="Hva er dine favorittfrukter?"
description="Small single-select"
options={options}
size="small"
/>
<br />
<UNSAFE_Combobox
label="Hva er dine favorittfrukter?"
description="Medium multiselect"
options={options}
isMultiSelect
allowNewValues
/>
<br />
<UNSAFE_Combobox
label="Hva er dine favorittfrukter?"
description="Small multiselect"
options={options}
isMultiSelect
size="small"
allowNewValues
/>
</DemoContainer>
);
ComboboxSizes.args = {
options,
};
export const WithError = {
args: {
error: "Du må velge en favorittfrukt.",
isLoading: true,
},
render: (props) => {
const [hasSelectedValue, setHasSelectedValue] = useState(false);
const [isLoading, setIsLoading] = useState(false);
return (
<DemoContainer dataTheme={props.darkMode}>
<UNSAFE_Combobox
filteredOptions={isLoading ? [] : undefined}
options={options}
label="Hva er dine favorittfrukter?"
error={!hasSelectedValue && props.error}
isLoading={isLoading}
onChange={() => {
setIsLoading(true);
setTimeout(() => setIsLoading(false), 2000);
}}
onToggleSelected={(_, isSelected) => setHasSelectedValue(isSelected)}
/>
</DemoContainer>
);
},
};
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
export const CancelInputTest = {
render: (props) => {
return (
<DemoContainer dataTheme={props.darkMode}>
<UNSAFE_Combobox
options={options}
label="Hva er dine favorittfrukter?"
/>
</DemoContainer>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByLabelText("Hva er dine favorittfrukter?");
userEvent.click(input);
await userEvent.type(input, "apple", { delay: 200 });
await sleep(1000);
userEvent.keyboard("{ArrowDown}");
await sleep(1000);
userEvent.keyboard("{Escape}");
await sleep(1000);
userEvent.keyboard("{ArrowDown}");
await sleep(500);
const banana = canvas.getByText("banana");
userEvent.click(banana);
},
};
export const RemoveSelectedMultiSelectTest = {
render: (props) => {
return (
<DemoContainer dataTheme={props.darkMode}>
<UNSAFE_Combobox
options={options}
label="Hva er dine favorittfrukter?"
isMultiSelect
/>
</DemoContainer>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByLabelText("Hva er dine favorittfrukter?");
userEvent.click(input);
await userEvent.type(input, "apple", { delay: 200 });
await sleep(250);
userEvent.keyboard("{ArrowDown}");
await sleep(250);
userEvent.keyboard("{Enter}");
await sleep(250);
userEvent.keyboard("{Escape}");
await sleep(250);
userEvent.click(input);
await userEvent.type(input, "banana", { delay: 200 });
await sleep(250);
userEvent.keyboard("{ArrowDown}");
await sleep(250);
userEvent.keyboard("{Enter}");
await sleep(250);
const appleSlett = canvas.getByLabelText("apple slett");
appleSlett.focus();
await sleep(250);
userEvent.click(appleSlett);
await sleep(250);
const appleOption = canvas.getByRole("option", {
name: "apple",
selected: false,
});
expect(appleOption).toBeVisible();
userEvent.keyboard("{Escape}");
await sleep(250);
expect(appleOption).not.toBeVisible();
const bananaSlett = canvas.getByLabelText("banana slett");
expect(bananaSlett).toBeInTheDocument();
const appleSlettAgain = canvas.queryByLabelText("apple slett");
expect(appleSlettAgain).not.toBeInTheDocument();
},
};
export const AddWhenAddNewDisabledTest = {
render: (props) => {
return (
<DemoContainer dataTheme={props.darkMode}>
<UNSAFE_Combobox
options={options}
label="Hva er dine favorittfrukter?"
isMultiSelect
/>
</DemoContainer>
);
},
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByLabelText("Hva er dine favorittfrukter?");
userEvent.click(input);
await userEvent.type(input, "aaa", { delay: 200 });
await sleep(250);
userEvent.keyboard("{ArrowDown}");
await sleep(250);
userEvent.keyboard("{ArrowDown}");
await sleep(250);
userEvent.keyboard("{Enter}");
await sleep(250);
userEvent.keyboard("{Escape}");
await sleep(250);
const invalidSelect = canvas.queryByLabelText("aaa slett");
expect(invalidSelect).not.toBeInTheDocument();
},
};
export const TestThatCallbacksOnlyFireWhenExpected = {
args: {
onChange: jest.fn(),
onClear: jest.fn(),
onToggleSelected: jest.fn(),
},
render: (props) => {
return (
<DemoContainer dataTheme={props.darkMode}>
<UNSAFE_Combobox
options={options}
label="Hva er dine favorittfrukter?"
{...props}
/>
</DemoContainer>
);
},
play: async ({ canvasElement, args }) => {
args.onToggleSelected.mockClear();
args.onClear.mockClear();
args.onChange.mockClear();
const canvas = within(canvasElement);
const input = canvas.getByLabelText("Hva er dine favorittfrukter?");
const searchWord = "tangerine";
userEvent.click(input);
await userEvent.type(input, searchWord, { delay: 200 });
await sleep(250);
userEvent.keyboard("{ArrowDown}");
await sleep(250);
userEvent.keyboard("{Enter}");
await sleep(250);
expect(args.onClear.mock.calls).toHaveLength(1);
expect(args.onToggleSelected.mock.calls).toHaveLength(1);
expect(args.onChange.mock.calls).toHaveLength(searchWord.length + 1);
},
};
@@ -0,0 +1,123 @@
/* eslint-disable react/jsx-pascal-case */
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import React, { useId } from "react";
import { UNSAFE_Combobox } from "..";
import { act } from "react-dom/test-utils";
const options = [
"banana",
"apple",
"tangerine",
"pear",
"grape",
"kiwi",
"mango",
"passion fruit",
"pineapple",
"strawberry",
"watermelon",
"grape fruit",
];
const App = (props) => {
const id = useId();
return (
<div data-theme="light">
<UNSAFE_Combobox
label="Hva er dine favorittfrukter?"
size="medium"
variant="simple"
id={id}
{...props}
/>
</div>
);
};
describe("Render combobox", () => {
describe("with multi select", () => {
it("Should be able to search, select and remove selections", async () => {
const utils = render(<App isMultiSelect options={options} />);
await act(async () => {
await userEvent.click(
utils.getByRole("combobox", { name: "Hva er dine favorittfrukter?" })
);
});
await act(async () => {
await userEvent.type(
utils.getByRole("combobox", { name: "Hva er dine favorittfrukter?" }),
"apple"
);
});
await act(async () => {
await userEvent.click(
await utils.findByRole("option", { name: "apple" })
);
});
expect(
await utils.findByRole("option", { name: "apple", selected: true })
).toBeInTheDocument();
await act(async () => {
await userEvent.click(
await utils.findByRole("button", { name: "apple slett" })
);
});
});
});
it("Should show loading icon when loading (used for async search)", async () => {
const utils = render(<App options={[]} isListOpen isLoading />);
expect(await utils.findByRole("option", { name: "venter..." }));
});
});
describe("Combobox state-handling", () => {
it("Should not select previous focused element when closes", async () => {
const utils = render(<App options={options} />);
await act(async () => {
await userEvent.click(
utils.getByRole("combobox", { name: "Hva er dine favorittfrukter?" })
);
});
await act(async () => {
await userEvent.type(
utils.getByRole("combobox", { name: "Hva er dine favorittfrukter?" }),
"ban"
);
await userEvent.keyboard("{ArrowDown}");
await userEvent.keyboard("{ArrowUp}");
await userEvent.keyboard("{Enter}");
});
expect(
await utils.queryByRole("button", { name: "banana slett" })
).toBeNull();
});
it("Should reset list when resetting input (ESC)", async () => {
const utils = render(<App options={options} />);
await act(async () => {
await userEvent.click(
utils.getByRole("combobox", { name: "Hva er dine favorittfrukter?" })
);
});
await act(async () => {
await userEvent.type(
utils.getByRole("combobox", { name: "Hva er dine favorittfrukter?" }),
"apple"
);
await userEvent.keyboard("{ArrowDown}");
await userEvent.keyboard("{Escape}");
await userEvent.keyboard("{ArrowDown}");
});
expect(
await utils.findByRole("option", { name: "banana" })
).toBeInTheDocument();
});
});
@@ -0,0 +1,65 @@
import React, { useState, useCallback, createContext, useContext } from "react";
import { useInputContext } from "./Input/inputContext";
import { useSelectedOptionsContext } from "./SelectedOptions/selectedOptionsContext";
type CustomOptionsContextType = {
customOptions: string[];
removeCustomOption: (option: string) => void;
addCustomOption: (option: string) => void;
setCustomOptions: React.Dispatch<React.SetStateAction<string[]>>;
};
const CustomOptionsContext = createContext<CustomOptionsContextType>(
{} as CustomOptionsContextType
);
export const CustomOptionsProvider = ({ children }) => {
const [customOptions, setCustomOptions] = useState<string[]>([]);
const { focusInput } = useInputContext();
const { isMultiSelect } = useSelectedOptionsContext();
const removeCustomOption = useCallback(
(option) => {
setCustomOptions((prevCustomOptions) =>
prevCustomOptions.filter((o) => o !== option)
);
focusInput();
},
[focusInput, setCustomOptions]
);
const addCustomOption = useCallback(
(option) => {
if (isMultiSelect) {
setCustomOptions((prevOptions) => [...prevOptions, option]);
} else {
setCustomOptions([option]);
}
focusInput();
},
[focusInput, isMultiSelect, setCustomOptions]
);
const customOptionsState = {
customOptions,
removeCustomOption,
addCustomOption,
setCustomOptions,
};
return (
<CustomOptionsContext.Provider value={customOptionsState}>
{children}
</CustomOptionsContext.Provider>
);
};
export const useCustomOptionsContext = () => {
const context = useContext(CustomOptionsContext);
if (!context) {
throw new Error(
"useCustomOptionsContext must be used within a CustomOptionsProvider"
);
}
return context;
};
@@ -0,0 +1,2 @@
export { default as Combobox } from "./ComboboxProvider";
export { type ComboboxProps } from "./types";
@@ -0,0 +1,130 @@
import React, { ChangeEvent, InputHTMLAttributes } from "react";
import { FormFieldProps } from "../useFormField";
export interface ComboboxProps
extends FormFieldProps,
Omit<InputHTMLAttributes<HTMLInputElement>, "size" | "onChange" | "value"> {
/**
* Combobox label
*/
label: React.ReactNode;
/**
* List of options to use for autocompletion
*/
options: string[];
/**
* If enabled, adds an option to add the value of the input as an option whenever there are no options matching the value.
*/
allowNewValues?: boolean;
/**
* If "true" adds a button to clear the value in the input field
*/
clearButton?: boolean;
/**
* Custom name for the clear button. Requires "clearButton" to be "true".
*
* @default "Tøm"
*/
clearButtonLabel?: string;
/**
* A list of options to display in the dropdown list.
* If provided, this overrides the internal search logic in the component.
* Useful for e.g. searching on a server or when overriding the search algorithm to search for synonyms or similar.
*/
filteredOptions?: string[];
/**
* Optionally hide the label visually.
* Not recommended, but can be considered for e.g. search fields in the top menu.
*/
hideLabel?: boolean;
/**
* Custom class name for the input field.
*
* If used for styling, please consider using tokens instead.
*/
inputClassName?: string | undefined;
/**
* Controlled open/closed state for the dropdown list
*/
isListOpen?: boolean;
/**
* Set to "true" when doing an async search and waiting for new filteredOptions.
*
* Will show a spinner in the dropdown and announce to screen readers that it is loading.
*/
isLoading?: boolean;
/**
* Set to "true" to allow multiple selections
*
* This will display selected values as a list of Chips in front of the input field, instead of a selection replacing the value of the input.
*
*/
isMultiSelect?: boolean;
/**
* Callback function triggered whenever the value of the input field is triggered.
*
* @param event
* @returns
*/
onChange?: (
event: ChangeEvent<HTMLInputElement> | null,
value?: string
) => void;
/**
* Callback function triggered whenever the input field is cleared
*
* @param event
* @returns
*/
onClear?: (event: React.PointerEvent | React.KeyboardEvent) => void;
/**
* Callback function triggered whenever an option is selected or de-selected
*
* @param option
* @param isSelected - Whether the option has been selected or unselected
* @param isCustomOption - Whether the option comes from user input, instead of from the list
* @returns
*/
onToggleSelected?: (
option: string,
isSelected: boolean,
isCustomOption: boolean
) => void;
/**
* List of selected options.
*
* Use this prop when controlling the selected state outside for the component,
* e.g. for a filter, where options can be toggled elsewhere/programmatically.
*/
selectedOptions?: string[];
/**
* Set to "true" to enable inline autocomplete.
*
* @default false
*/
shouldAutocomplete?: boolean;
/**
* When set to "true" displays selected options as Chips before the input field
*
* @default true
*/
shouldShowSelectedOptions?: boolean;
/**
* When set to "true" displays the toggle button for opening/closing the dropdown list
*
* @default true
*/
toggleListButton?: boolean;
/**
* Custom name for the toggle list-button. Requires "toggleListButton" to be "true".
*
* @default "Alternativer"
*/
toggleListButtonLabel?: string;
/**
* Set this to override the value of the input field.
*
* This converts the input to a controlled input, so you have to use onChange to update the value.
*/
value?: string;
}
+1
View File
@@ -17,6 +17,7 @@ export {
type RadioProps,
} from "./radio";
export { Search, type SearchClearEvent, type SearchProps } from "./search";
export { Combobox as UNSAFE_Combobox, type ComboboxProps } from "./combobox";
export { default as Select, type SelectProps } from "./Select";
export { default as Switch, type SwitchProps } from "./Switch";
export { Counter, default as Textarea, type TextareaProps } from "./Textarea";
+19 -1
View File
@@ -35,10 +35,28 @@ export interface FormFieldProps {
readOnly?: boolean;
}
export interface FormFieldType {
showErrorMsg: boolean;
hasError: boolean;
errorId: string;
inputDescriptionId: string;
size: "small" | "medium";
inputProps: {
id: string;
"aria-invalid"?: boolean;
"aria-describedby"?: string;
disabled?: boolean;
};
readOnly?: boolean;
}
/**
* Handles props and their state for various form-fields in context with Fieldset
*/
export const useFormField = (props: FormFieldProps, prefix: string) => {
export const useFormField = (
props: FormFieldProps,
prefix: string
): FormFieldType => {
const { size, error, errorId: propErrorId } = props;
const fieldset = useContext(FieldsetContext);
+4
View File
@@ -25,6 +25,10 @@ export interface CellProps extends HTMLAttributes<HTMLDivElement> {
lg?: Column;
}
/**
* @deprecated Use <HGrid /> instead. <Grid /> will be removed in the future.
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/hgrid)
*/
export const Cell = forwardRef<HTMLDivElement, CellProps>(
({ children, xs, sm, md, lg, className, ...rest }, ref) => {
return (
+4
View File
@@ -8,6 +8,10 @@ export interface GridProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
}
/**
* @deprecated Use <HGrid /> instead. <Grid /> will be removed in the future.
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/hgrid)
*/
export const Grid = forwardRef<HTMLDivElement, GridProps>(
({ children, className, ...rest }, ref) => (
<div ref={ref} className={cl("navds-grid", className)} {...rest}>
-4
View File
@@ -1,6 +1,2 @@
export { default as Cell, type CellProps } from "./Cell";
export { default as Grid, type GridProps } from "./Grid";
export {
default as ContentContainer,
type ContentContainerProps,
} from "./ContentContainer";
+3
View File
@@ -32,3 +32,6 @@ export * from "./toggle-group";
export * from "./tooltip";
export * from "./typography";
export * from "./util";
export * from "./layout/stack";
export * from "./layout/grid";
export * from "./layout/content-container";
@@ -0,0 +1,4 @@
export {
default as ContentContainer,
type ContentContainerProps,
} from "./ContentContainer";
@@ -0,0 +1,101 @@
import React, { forwardRef, HTMLAttributes } from "react";
import cl from "clsx";
import {
getResponsiveProps,
getResponsiveValue,
ResponsiveProp,
SpacingScale,
} from "../utilities/css";
export interface HGridProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
/**
* Number of columns to display. Can be a number, a string with a unit or tokens for spesific breakpoints.
* Sets `grid-template-columns`, so `fr`, `minmax` etc. works.
* @example
* columns={{ sm: 1, md: 1, lg: "1fr auto", xl: "1fr auto"}}
* @example
* columns={3}
* @example
* columns="repeat(3, minmax(0, 1fr))"
*/
columns?: ResponsiveProp<number | string>;
/** Spacing between columns. Based on spacing-tokens.
* @example
* gap="6"
* gap={{ sm: "2", md: "2", lg: "6", xl: "6"}}
*/
gap?: ResponsiveProp<SpacingScale>;
}
/**
* Horizontal Grid Primitive with dynamic columns and gap based on breakpoints.
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/hgrid)
* @see 🏷️ {@link HGridProps}
*
* @example
* <HGrid gap="6" columns={3}>
* <div />
* <div />
* <div />
* </HGrid>
* @example
* <HGrid gap={{xs: "2", md: "6"}} columns={3}>
* <div />
* <div />
* <div />
* </HGrid>
* @example
* <HGrid gap="6" columns={{ sm: 1, md: 1, lg: "1fr auto", xl: "1fr auto"}}>
* <div />
* <div />
* <div />
* </HGrid>
*/
export const HGrid = forwardRef<HTMLDivElement, HGridProps>(
({ className, columns, gap, style, ...rest }, ref) => {
const styles: React.CSSProperties = {
...style,
...getResponsiveProps(`hgrid`, "gap", "spacing", gap),
...getResponsiveValue(`hgrid`, "columns", formatGrid(columns)),
};
return (
<div
{...rest}
ref={ref}
className={cl("navds-hgrid", className)}
style={styles}
/>
);
}
);
function formatGrid(
props?: ResponsiveProp<number | string>
): ResponsiveProp<number | string> {
if (!props) {
return {};
}
if (typeof props === "string" || typeof props === "number") {
return getColumnValue(props);
}
return Object.fromEntries(
Object.entries(props).map(([breakpointToken, columnValue]) => [
breakpointToken,
getColumnValue(columnValue),
])
);
}
const getColumnValue = (prop: number | string) => {
if (typeof prop === "number") {
return `repeat(${prop}, minmax(0, 1fr))`;
}
return prop;
};
export default HGrid;
@@ -0,0 +1,102 @@
import React from "react";
import { HGrid } from ".";
const columnsVariants = {
Number: "columnNumber",
String: "columnString",
Object: "columnObject",
};
export default {
title: "ds-react/HGrid",
component: HGrid,
parameters: {
layout: "fullscreen",
},
argTypes: {
columnsType: {
defaultValue: Object.keys(columnsVariants)[0],
options: Object.keys(columnsVariants),
control: { type: "radio" },
},
},
};
/* const getColumnsProp = () */
export const Default = {
render: (props) => (
<HGrid
gap={props?.gap ?? 4}
columns={props[columnsVariants[props.columnsType]]}
>
<Placeholder text="1" />
<Placeholder text="2" />
<Placeholder text="3" />
<Placeholder text="4" />
</HGrid>
),
args: {
columnNumber: 4,
columnObject: { xs: 1, md: 4 },
columnString: "repeat(3, minmax(0, 1fr))",
gap: "4",
},
};
export const Gap = {
render: () => (
<HGrid gap="6">
<Placeholder text="1" />
<Placeholder text="2" />
<Placeholder text="3" />
<Placeholder text="4" />
</HGrid>
),
};
export const DynamicGap = {
render: () => (
<HGrid gap={{ xs: "2", md: "8" }}>
<Placeholder text="1" />
<Placeholder text="2" />
<Placeholder text="3" />
<Placeholder text="4" />
</HGrid>
),
};
export const Columns = {
render: () => (
<HGrid gap="4" columns={2}>
<Placeholder text="1" />
<Placeholder text="2" />
<Placeholder text="3" />
<Placeholder text="4" />
</HGrid>
),
};
export const DynamicColumns = {
render: () => (
<HGrid gap="4" columns={{ sm: "1fr 5fr", md: "2fr 2fr" }}>
<Placeholder text="1" />
<Placeholder text="2" />
</HGrid>
),
};
function Placeholder({ text }) {
return (
<div
style={{
background: "var(--a-deepblue-900)",
height: "5rem",
width: "auto",
color: "white",
}}
>
{text}
</div>
);
}
@@ -0,0 +1 @@
export { default as HGrid, type HGridProps } from "./HGrid";
@@ -0,0 +1,30 @@
import React, { forwardRef } from "react";
import { OverridableComponent } from "../../util/OverridableComponent";
import { StackProps, Stack } from "./Stack";
export type HStackProps = Omit<StackProps, "direction">;
/**
* Layout-primitive for horizontal flexbox
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/Stack)
* @see 🏷️ {@link HStackProps}
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
*
* @example
* <HStack gap="8">
* <MyComponent />
* <MyComponent />
* </HStack>
*
* @example
* // Responsive gap
* <HStack gap={{xs: "2", md: "6"}}>
* <MyComponent />
* <MyComponent />
* </HStack>
*/
export const HStack: OverridableComponent<HStackProps, HTMLDivElement> =
forwardRef((props, ref) => {
return <Stack {...props} ref={ref} direction="row" />;
});
@@ -0,0 +1,15 @@
import React from "react";
/**
* Layout-primitive for auto-spacing between elements
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/Stack)
*
* @example
* <HStack gap="8">
* <MyComponent />
* <Spacer />
* <MyComponent />
* </HStack>
*/
export const Spacer = () => <div className="navds-stack__spacer" />;
@@ -0,0 +1,76 @@
import cl from "clsx";
import React, { forwardRef, HTMLAttributes } from "react";
import { OverridableComponent } from "../../util/OverridableComponent";
import {
getResponsiveProps,
ResponsiveProp,
SpacingScale,
} from "../utilities/css";
export interface StackProps extends HTMLAttributes<HTMLDivElement> {
children: React.ReactNode;
/**
* Justify-content
*/
justify?:
| "start"
| "center"
| "end"
| "space-around"
| "space-between"
| "space-evenly";
/**
* Align-items
*/
align?: "start" | "center" | "end" | "baseline" | "stretch";
/**
* flex-wrap
*/
wrap?: boolean;
/**
* @example
* gap='4'
* gap={{xs: '2', sm: '3', md: '4', lg: '5', xl: '6'}}
*/
gap?: ResponsiveProp<SpacingScale>;
direction: "row" | "column";
}
export const Stack: OverridableComponent<StackProps, HTMLDivElement> =
forwardRef(
(
{
as: Component = "div",
className,
align,
justify,
wrap = true,
gap,
style: _style,
direction,
...rest
},
ref
) => {
const style = {
..._style,
"--__ac-stack-direction": direction,
"--__ac-stack-align": align,
"--__ac-stack-justify": justify,
"--__ac-stack-wrap": wrap ? "wrap" : "nowrap",
...getResponsiveProps(`stack`, "gap", "spacing", gap),
} as React.CSSProperties;
return (
<Component
{...rest}
ref={ref}
style={style}
className={cl("navds-stack", className, {
"navds-vstack": direction === "column",
"navds-hstack": direction === "row",
})}
/>
);
}
);
@@ -0,0 +1,30 @@
import React, { forwardRef } from "react";
import { OverridableComponent } from "../../util/OverridableComponent";
import { StackProps, Stack } from "./Stack";
export type VStackProps = Omit<StackProps, "direction" | "wrap">;
/**
* Layout-primitive for vetical flexbox
*
* @see [📝 Documentation](https://aksel.nav.no/komponenter/core/Stack)
* @see 🏷️ {@link VStackProps}
* @see [🤖 OverridableComponent](https://aksel.nav.no/grunnleggende/kode/overridablecomponent) support
*
* @example
* <VStack gap="8">
* <MyComponent />
* <MyComponent />
* </VStack>
*
* @example
* // Responsive gap
* <VStack gap={{xs: "2", md: "6"}}>
* <MyComponent />
* <MyComponent />
* </VStack>
*/
export const VStack: OverridableComponent<VStackProps, HTMLDivElement> =
forwardRef((props, ref) => {
return <Stack {...props} ref={ref} direction="column" wrap={false} />;
});
@@ -0,0 +1,3 @@
export { HStack, type HStackProps } from "./HStack";
export { VStack, type VStackProps } from "./VStack";
export { Spacer } from "./Spacer";
@@ -0,0 +1,161 @@
import React from "react";
import type { Meta } from "@storybook/react";
import { HStack, VStack, Spacer } from ".";
export default {
title: "ds-react/Stack",
component: HStack,
} satisfies Meta<typeof HStack>;
export const Horizontal = {
render: () => (
<HStack gap="4">
<Placeholders count={4} />
</HStack>
),
};
export const Spacing = {
render: () => (
<div style={{ height: "80vh", display: "flex" }}>
<VStack gap="8">
<Spacer />
<HStack gap="4">
<Placeholders count={1} />
<Spacer />
<Placeholders count={1} />
</HStack>
<HStack gap="4">
<Placeholders count={1} />
<Placeholders count={1} />
</HStack>
<HStack gap="4">
<Placeholders count={2} />
</HStack>
</VStack>
</div>
),
parameters: {
layout: "fullscreen",
},
};
export const Vertical = {
render: () => (
<VStack gap="4">
<Placeholders count={4} />
</VStack>
),
};
export const VerticalDemo = {
render: () => (
<VStack gap="2">
<VStack>
<Placeholders count={4} />
</VStack>
<Placeholders count={4} />
<VStack>
<Placeholders count={4} />
</VStack>
</VStack>
),
};
export const VerticalAlign = {
render: () => (
<VStack gap="4">
<VStack align="start">
<Placeholders count={2} />
</VStack>
<VStack align="center">
<Placeholders count={2} />
</VStack>
<VStack align="end">
<Placeholders count={2} />
</VStack>
</VStack>
),
parameters: {
layout: "fullscreen",
},
};
export const OverrideComponent = {
render: () => (
<VStack gap="4" as="form" onSubmit={(e) => e.preventDefault()}>
<Placeholders count={4} />
</VStack>
),
};
export const Responsive = {
render: () => (
<VStack gap={{ xs: "1", sm: "3", md: "6", lg: "10", xl: "16" }}>
<Placeholders count={4} />
</VStack>
),
};
export const Nested = {
render: () => (
<VStack gap="16">
<Placeholders count={2}>
<VStack gap="4">
<Placeholders count={2} color="gray" />
</VStack>
</Placeholders>
</VStack>
),
};
export const DividerDemo = {
render: () => (
<div style={{ height: "80vh", width: "40rem" }}>
<VStack gap={{ xs: "2", md: "6", lg: "12" }}>
<HStack gap={{ xs: "2", md: "6", lg: "12" }}>
<Placeholders count={1} />
<Spacer />
<Placeholders count={1} />
</HStack>
<hr
style={{
border: "none",
borderBottom: "1px solid var(--a-border-divider)",
margin: 0,
}}
/>
<HStack gap={{ xs: "2", md: "6", lg: "12" }}>
<Placeholders count={2} />
</HStack>
</VStack>
</div>
),
};
function Placeholders({
count,
children,
color,
}: {
count: number;
children?: React.ReactNode;
color?: string;
}) {
return (
<>
{Array.from({ length: count }, (_, i) => (
<div
key={i}
style={{
backgroundColor: color ?? "var(--a-purple-200)",
height: children ? "" : "3rem",
width: children ? "" : "3rem",
}}
>
{children}
</div>
))}
</>
);
}
@@ -0,0 +1,78 @@
export type BreakpointsAlias = "xs" | "sm" | "md" | "lg" | "xl";
export type SpacingScale =
| "0"
| "05"
| "1"
| "2"
| "3"
| "4"
| "5"
| "6"
| "7"
| "8"
| "9"
| "10"
| "11"
| "12"
| "14"
| "16"
| "18"
| "20"
| "24"
| "32";
type ResponsivePropConfig<T = string> = {
// eslint-disable-next-line no-unused-vars
[Breakpoint in BreakpointsAlias]?: T;
};
export type ResponsiveProp<T> = T | ResponsivePropConfig<T>;
export type ResponsiveValue<T = string> = undefined | ResponsiveProp<T>;
export function getResponsiveProps<T = string>(
componentName: string,
componentProp: string,
tokenSubgroup: string,
responsiveProp?: ResponsiveProp<T>
) {
if (!responsiveProp) {
return {};
}
if (typeof responsiveProp === "string") {
return {
[`--__ac-${componentName}-${componentProp}-xs`]: `var(--a-${tokenSubgroup}-${responsiveProp})`,
};
}
return Object.fromEntries(
Object.entries(responsiveProp).map(([breakpointAlias, aliasOrScale]) => [
`--__ac-${componentName}-${componentProp}-${breakpointAlias}`,
`var(--a-${tokenSubgroup}-${aliasOrScale})`,
])
);
}
export function getResponsiveValue<T = string>(
componentName: string,
componentProp: string,
responsiveProp?: ResponsiveValue<T>
) {
if (!responsiveProp) {
return {};
}
if (typeof responsiveProp === "string") {
return {
[`--__ac-${componentName}-${componentProp}-xs`]: responsiveProp,
};
}
return Object.fromEntries(
Object.entries(responsiveProp).map(([breakpointAlias, responsiveValue]) => [
`--__ac-${componentName}-${componentProp}-${breakpointAlias}`,
responsiveValue,
])
);
}
+47 -3
View File
@@ -4,6 +4,23 @@ import { OverridableComponent } from "../util/OverridableComponent";
export interface LinkProps
extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
/**
* Inverts when the underline appears. If this is false,
* the underline does not appear by default, but does appear when the link is hovered.
* This makes it more suitable for use when inlined in text.
* @default true
*/
underline?: boolean;
/**
* Renders link with `display: inline` for better wrapping in text.
* @default false
*/
inlineText?: boolean;
/**
* Variant of the component to use.
* @default "action"
*/
variant?: "action" | "neutral" | "subtle";
/**
* Link text
*/
@@ -22,10 +39,37 @@ export interface LinkProps
* <Link href="#">lenke til ny side</Link>
* <Link as={ReactRouter.Link} to="#">lenke til ny side</Link>
* ```
* @example
* ```jsx
* <BodyLong>
* ...
* <Link href="#" inlineText>lenke til ny side</Link>
* ...
* </BodyLong>
* ```
*/
export const Link: OverridableComponent<LinkProps, HTMLAnchorElement> =
forwardRef(({ as: Component = "a", className, ...rest }, ref) => (
<Component {...rest} ref={ref} className={cl("navds-link", className)} />
));
forwardRef(
(
{
as: Component = "a",
className,
underline = true,
variant = "action",
inlineText = false,
...rest
},
ref
) => (
<Component
{...rest}
ref={ref}
className={cl("navds-link", className, `navds-link--${variant}`, {
"navds-link--remove-underline": !underline,
"navds-link--inline-text": inlineText,
})}
/>
)
);
export default Link;
+143 -13
View File
@@ -6,29 +6,76 @@ import {
ConfirmationPanel as DsConfirmationPanel,
Link,
} from "..";
import { RandomIcon } from "../util/RandomIcon";
export default {
title: "ds-react/Link",
component: Link,
};
const LinkWrapper = ({
children = "Ex aliqua incididunt",
iconRight = false,
iconLeft = false,
variant = "action",
underline = false,
inlineText = true,
}) => (
<>
{" "}
<Link
href="#"
underline={underline}
variant={variant as "action" | "neutral" | "subtle"}
inlineText={inlineText}
>
{iconLeft && (
<>
<RandomIcon />{" "}
</>
)}
{children}
{iconRight && (
<>
{" "}
<RandomIcon />
</>
)}
</Link>{" "}
</>
);
export const Default = {
render: (props) => {
render: ({ icon, inline }) => {
const LinkD = () => (
<Link href="#">
{props.icon && <PlusCircleFillIcon />}Ex aliqua incididunt{" "}
{props.icon && <PlusCircleFillIcon />}
</Link>
<>
{" "}
<Link href="#" underline={!inline} inlineText={inline}>
{icon && <PlusCircleFillIcon />}Ex aliqua incididunt
{icon && <PlusCircleFillIcon />}
</Link>{" "}
</>
);
if (props.inline) {
if (inline) {
return (
<BodyLong>
Incididunt laborum nisi nisi Lorem <LinkD /> in. Laborum aute fugiat
officia adipisicing non veniam dolor nulla non ex consectetur fugiat
eiusmod aute. Culpa sit aute est duis minim in in voluptate velit
fugiat. Laboris est id deserunt ut ea Lorem eu. Esse elit laboris aute
commodo sint laborum fugiat aliqua.
</BodyLong>
<div
className="colgap"
style={{
width: "800px",
border: "1px solid black",
borderRadius: "8px",
}}
>
<BodyLong>
Incididunt laborum nisi nisi Lorem
<LinkD />
in. Laborum aute fugiat officia adipisicing non veniam dolor nulla
non ex consectetur fugiat eiusmod aute. Culpa sit aute est duis
minim in in voluptate velit fugiat. Laboris est id deserunt ut ea
Lorem eu. Esse elit laboris aute commodo sint laborum fugiat aliqua.
</BodyLong>
</div>
);
}
return <LinkD />;
@@ -40,6 +87,49 @@ export const Default = {
},
};
export const InlineInsideBodyLong = {
render: ({ iconLeft, iconRight }) => {
return (
<div
className="colgap"
style={{
width: "800px",
border: "1px solid black",
borderRadius: "8px",
}}
>
<style>{`.storybook-custom-spacing { white-space: pre;}`}</style>
<BodyLong>
<LinkWrapper underline iconLeft={iconLeft} iconRight={iconRight} />
Eiusmod aute.
<LinkWrapper underline iconLeft={iconLeft} iconRight={iconRight} />
Culpa sit aute est duis minim in in voluptate velit Incididunt laborum
nisi nisi Lorem officia adipisicing non veniam
<LinkWrapper underline iconLeft={iconLeft} iconRight={iconRight}>
blah blah blah blah blah blah blah blah blah blah blah blah blah
blah blah blah blah blah blah blah blah
</LinkWrapper>
dolor eu. Esse elit laboris aute commodo sint laborum fugiat aliqua.
<LinkWrapper underline iconLeft={iconLeft} iconRight={iconRight}>
Link
</LinkWrapper>
</BodyLong>
<BodyLong className="storybook-custom-spacing">
Custom{" "}
<LinkWrapper underline iconLeft={iconLeft} iconRight={iconRight}>
link
</LinkWrapper>
{" "}spacing.
</BodyLong>
</div>
);
},
args: {
iconLeft: false,
iconRight: false,
},
};
const DemoLink = () => (
<Link href="#">
<PlusCircleFillIcon aria-hidden /> Ex aliqua incididunt{" "}
@@ -83,3 +173,43 @@ export const ConfirmationPanel = () => {
</div>
);
};
export const Variants = {
render: ({ iconLeft, iconRight }) => {
return (
<div className="colgap">
{["action", "neutral", "subtle"].map((variant) => (
<>
<div>
<LinkWrapper
iconLeft={iconLeft}
iconRight={iconRight}
variant={variant}
/>
</div>
</>
))}
</div>
);
},
args: {
iconLeft: false,
iconRight: false,
},
};
export const InlineLink = {
render: () => (
<BodyLong>
Officia incididunt Culpa sit aute est duis minim in in voluptate velit
Incididunt laborum nisi nisi Lorem officia adipisicing non veniam{" "}
<Link href="#" inlineText={true}>
lenke til ny side
<PlusCircleFillIcon aria-hidden />
</Link>{" "}
Culpa sit aute est duis minim in in voluptate velit Incididunt laborum
nisi nisi Lorem officia adipisicing non veniam occaecat commodo id ad
aliquip.
</BodyLong>
),
};
+21 -5
View File
@@ -24,6 +24,11 @@ export interface ListProps extends React.HTMLAttributes<HTMLDivElement> {
* @default "h3"
*/
headingTag?: React.ElementType<any>;
/**
* Changes padding, height and font-size
* @default medium
*/
size?: "medium" | "small";
}
export interface ListComponent
@@ -37,13 +42,15 @@ export interface ListComponent
}
interface ListContextProps {
listType: "ul" | "ol";
listType: ListProps["as"];
isNested: boolean | null;
size: ListProps["size"];
}
export const ListContext = createContext<ListContextProps>({
listType: "ul",
isNested: null,
size: "medium",
});
export const List = forwardRef<HTMLDivElement, ListProps>(
@@ -55,35 +62,44 @@ export const List = forwardRef<HTMLDivElement, ListProps>(
title,
description,
headingTag = "h3",
size,
...rest
},
ref
) => {
const ariaId = useId();
const { isNested } = useContext(ListContext);
const { isNested, size: _size } = useContext(ListContext);
const listSize = size ?? _size ?? "medium";
return (
<ListContext.Provider
value={{
listType: ListTag,
isNested: isNested === null ? false : true,
size: listSize,
}}
>
<div
{...rest}
ref={ref}
className={cl("navds-list", className, {
className={cl("navds-list", `navds-list--${listSize}`, className, {
"navds-list--nested": isNested === null ? false : true,
})}
>
{title && (
<Heading id={`tittel-${ariaId}`} size="small" as={headingTag}>
<Heading
id={`tittel-${ariaId}`}
size={listSize === "medium" ? "small" : "xsmall"}
as={headingTag}
>
{title}
</Heading>
)}
{description && (
<BodyShort id={`description-${ariaId}`}>{description}</BodyShort>
<BodyShort size={listSize} id={`description-${ariaId}`}>
{description}
</BodyShort>
)}
<ListTag
aria-labelledby={title && `tittel-${ariaId}`}
+4 -10
View File
@@ -20,7 +20,7 @@ export interface ListItemProps extends React.HTMLAttributes<HTMLLIElement> {
export const ListItem = forwardRef<HTMLLIElement, ListItemProps>(
({ className, children, title, icon, ...rest }, ref) => {
const { listType, isNested } = useContext(ListContext);
const { listType, size } = useContext(ListContext);
if (listType === "ol" && icon) {
console.warn(
@@ -29,13 +29,7 @@ export const ListItem = forwardRef<HTMLLIElement, ListItemProps>(
}
return (
<li
{...rest}
ref={ref}
className={cl("navds-list__item", className, {
"navds-list__item--noMargin": isNested,
})}
>
<li {...rest} ref={ref} className={cl("navds-list__item", className)}>
{listType === "ul" && (
<div
className={cl({
@@ -62,9 +56,9 @@ export const ListItem = forwardRef<HTMLLIElement, ListItemProps>(
</div>
)}
<BodyShort as="div" size="small" className="navds-list__item-content">
<BodyShort as="div" size={size} className="navds-list__item-content">
{title && (
<Label as="p" size="small">
<Label as="p" size={size}>
{title}
</Label>
)}
Loaded 100 of 233 files, more files were not shown because too many files have changed in this diff. Show more