@ks-digital/designsystem-angular 0.0.1-alpha.25 → 0.0.1-alpha.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/__internals/index.d.ts +54 -0
- package/alert/index.d.ts +17 -0
- package/button/index.d.ts +29 -0
- package/card/index.d.ts +22 -0
- package/details/index.d.ts +26 -0
- package/fesm2022/ks-digital-designsystem-angular-__internals.mjs +50 -0
- package/fesm2022/ks-digital-designsystem-angular-__internals.mjs.map +1 -0
- package/fesm2022/ks-digital-designsystem-angular-alert.mjs +35 -0
- package/fesm2022/ks-digital-designsystem-angular-alert.mjs.map +1 -0
- package/fesm2022/ks-digital-designsystem-angular-button.mjs +63 -0
- package/fesm2022/ks-digital-designsystem-angular-button.mjs.map +1 -0
- package/fesm2022/ks-digital-designsystem-angular-card.mjs +70 -0
- package/fesm2022/ks-digital-designsystem-angular-card.mjs.map +1 -0
- package/fesm2022/ks-digital-designsystem-angular-details.mjs +100 -0
- package/fesm2022/ks-digital-designsystem-angular-details.mjs.map +1 -0
- package/fesm2022/ks-digital-designsystem-angular-forms.mjs +408 -0
- package/fesm2022/ks-digital-designsystem-angular-forms.mjs.map +1 -0
- package/fesm2022/ks-digital-designsystem-angular-paragraph.mjs +24 -0
- package/fesm2022/ks-digital-designsystem-angular-paragraph.mjs.map +1 -0
- package/fesm2022/ks-digital-designsystem-angular-popover.mjs +165 -0
- package/fesm2022/ks-digital-designsystem-angular-popover.mjs.map +1 -0
- package/fesm2022/ks-digital-designsystem-angular-search.mjs +181 -0
- package/fesm2022/ks-digital-designsystem-angular-search.mjs.map +1 -0
- package/fesm2022/ks-digital-designsystem-angular-spinner.mjs +89 -0
- package/fesm2022/ks-digital-designsystem-angular-spinner.mjs.map +1 -0
- package/fesm2022/ks-digital-designsystem-angular-validation-message.mjs +25 -0
- package/fesm2022/ks-digital-designsystem-angular-validation-message.mjs.map +1 -0
- package/fesm2022/ks-digital-designsystem-angular.mjs +7 -0
- package/fesm2022/ks-digital-designsystem-angular.mjs.map +1 -0
- package/forms/index.d.ts +100 -0
- package/index.d.ts +2 -0
- package/package.json +59 -20
- package/paragraph/index.d.ts +8 -0
- package/popover/index.d.ts +25 -0
- package/search/index.d.ts +84 -0
- package/spinner/index.d.ts +25 -0
- package/validation-message/index.d.ts +8 -0
- package/.storybook/customTheme.ts +0 -15
- package/.storybook/default-args.ts +0 -18
- package/.storybook/main.ts +0 -27
- package/.storybook/manager.ts +0 -10
- package/.storybook/preview-head.html +0 -16
- package/.storybook/preview.ts +0 -70
- package/.storybook/themes.ts +0 -9
- package/.storybook/tsconfig.json +0 -16
- package/.storybook/vite.config.mts +0 -5
- package/eslint.config.mjs +0 -28
- package/ng-package.json +0 -9
- package/project.json +0 -81
- package/src/components/alert/alert.mdx +0 -46
- package/src/components/alert/alert.spec.ts +0 -33
- package/src/components/alert/alert.stories.ts +0 -138
- package/src/components/alert/alert.ts +0 -46
- package/src/components/alert/index.ts +0 -1
- package/src/components/button/button.mdx +0 -40
- package/src/components/button/button.spec.ts +0 -86
- package/src/components/button/button.stories.ts +0 -123
- package/src/components/button/button.ts +0 -60
- package/src/components/button/index.ts +0 -1
- package/src/components/card/card-block.ts +0 -10
- package/src/components/card/card.mdx +0 -100
- package/src/components/card/card.spec.ts +0 -70
- package/src/components/card/card.stories.ts +0 -101
- package/src/components/card/card.ts +0 -44
- package/src/components/card/index.ts +0 -2
- package/src/components/checkbox/README.md +0 -13
- package/src/components/checkbox/checkbox.mdx +0 -50
- package/src/components/checkbox/checkbox.spec.ts +0 -21
- package/src/components/checkbox/checkbox.stories.ts +0 -182
- package/src/components/checkbox/index.ts +0 -0
- package/src/components/colors.ts +0 -36
- package/src/components/common-inputs.ts +0 -30
- package/src/components/details/controlled-details.ts +0 -63
- package/src/components/details/details-content.ts +0 -7
- package/src/components/details/details-summary.ts +0 -7
- package/src/components/details/details.mdx +0 -89
- package/src/components/details/details.spec.ts +0 -56
- package/src/components/details/details.stories.ts +0 -129
- package/src/components/details/details.ts +0 -69
- package/src/components/details/index.ts +0 -3
- package/src/components/field/field-counter.ts +0 -56
- package/src/components/field/field-description.ts +0 -10
- package/src/components/field/field-error.ts +0 -13
- package/src/components/field/field-observer.ts +0 -121
- package/src/components/field/field-state.ts +0 -21
- package/src/components/field/field.mdx +0 -40
- package/src/components/field/field.spec.ts +0 -131
- package/src/components/field/field.stories.ts +0 -98
- package/src/components/field/field.ts +0 -70
- package/src/components/field/index.ts +0 -3
- package/src/components/fieldset/fieldset-description.ts +0 -8
- package/src/components/fieldset/fieldset-legend.ts +0 -11
- package/src/components/fieldset/fieldset.spec.ts +0 -80
- package/src/components/fieldset/fieldset.ts +0 -11
- package/src/components/fieldset/index.ts +0 -3
- package/src/components/input/index.ts +0 -1
- package/src/components/input/input.mdx +0 -11
- package/src/components/input/input.spec.ts +0 -25
- package/src/components/input/input.stories.ts +0 -72
- package/src/components/input/input.ts +0 -67
- package/src/components/label/index.ts +0 -1
- package/src/components/label/label.ts +0 -17
- package/src/components/paragraph/index.ts +0 -1
- package/src/components/paragraph/paragraph.ts +0 -10
- package/src/components/popover/controlled-popover.ts +0 -62
- package/src/components/popover/index.ts +0 -1
- package/src/components/popover/popover.mdx +0 -81
- package/src/components/popover/popover.spec.ts +0 -143
- package/src/components/popover/popover.stories.ts +0 -63
- package/src/components/popover/popover.ts +0 -186
- package/src/components/radio/radio.mdx +0 -117
- package/src/components/radio/radio.stories.ts +0 -226
- package/src/components/search/index.ts +0 -4
- package/src/components/search/search-button.ts +0 -35
- package/src/components/search/search-clear.ts +0 -57
- package/src/components/search/search-input.ts +0 -18
- package/src/components/search/search.mdx +0 -56
- package/src/components/search/search.spec.ts +0 -48
- package/src/components/search/search.stories.ts +0 -205
- package/src/components/search/search.ts +0 -50
- package/src/components/spinner/index.ts +0 -1
- package/src/components/spinner/spinner.mdx +0 -24
- package/src/components/spinner/spinner.spec.ts +0 -13
- package/src/components/spinner/spinner.stories.ts +0 -54
- package/src/components/spinner/spinner.ts +0 -62
- package/src/components/switch/switch.mdx +0 -82
- package/src/components/switch/switch.stories.ts +0 -94
- package/src/components/textarea/textarea.mdx +0 -14
- package/src/components/textarea/textarea.stories.ts +0 -52
- package/src/components/validation-message/index.ts +0 -1
- package/src/components/validation-message/validation-message.ts +0 -11
- package/src/index.ts +0 -14
- package/src/test-setup.ts +0 -12
- package/src/utils/log-if-devmode.ts +0 -13
- package/src/utils/random-id.ts +0 -3
- package/tsconfig.json +0 -34
- package/tsconfig.lib.json +0 -28
- package/tsconfig.lib.prod.json +0 -9
- package/tsconfig.spec.json +0 -30
- package/vite.config.mts +0 -35
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
import { Component, computed, signal } from '@angular/core'
|
|
2
|
-
import { Button } from '../button'
|
|
3
|
-
import { Details } from './details'
|
|
4
|
-
import { DetailsContent } from './details-content'
|
|
5
|
-
import { DetailsSummary } from './details-summary'
|
|
6
|
-
|
|
7
|
-
@Component({
|
|
8
|
-
selector: 'fiks-controlled-details',
|
|
9
|
-
imports: [Details, DetailsContent, DetailsSummary, Button],
|
|
10
|
-
template: `
|
|
11
|
-
<button
|
|
12
|
-
ksd-button
|
|
13
|
-
variant="secondary"
|
|
14
|
-
style="margin-bottom: .5em;"
|
|
15
|
-
(click)="toggleOpen()"
|
|
16
|
-
>
|
|
17
|
-
{{ toggleOpenText() }}
|
|
18
|
-
</button>
|
|
19
|
-
<ksd-details [open]="open1()" (toggled)="open1.set(!open1())">
|
|
20
|
-
<ksd-details-summary>Enkeltpersonforetak</ksd-details-summary>
|
|
21
|
-
<ksd-details-content>
|
|
22
|
-
Skal du starte for deg selv? Enkeltpersonforetak er ofte den enkleste
|
|
23
|
-
måten å etablere bedrift på. Denne organisasjonsformen har både fordeler
|
|
24
|
-
og ulemper. Det gir deg stor grad av frihet, men kan også gi betydelig
|
|
25
|
-
risiko fordi du har personlig ansvar for økonomien.
|
|
26
|
-
</ksd-details-content>
|
|
27
|
-
</ksd-details>
|
|
28
|
-
<ksd-details [open]="open2()" (toggled)="open2.set(!open2())">
|
|
29
|
-
<ksd-details-summary>Aksjeselskap (AS)</ksd-details-summary>
|
|
30
|
-
<ksd-details-content>
|
|
31
|
-
Planlegger du å starte næringsvirksomhet alene eller sammen med andre?
|
|
32
|
-
Innebærer næringsvirksomheten en økonomisk risiko? Vil du ha rettigheter
|
|
33
|
-
som arbeidstaker og muligheten til at andre kan investere i selskapet
|
|
34
|
-
ditt? Da kan aksjeselskap være en hensiktsmessig organisasjonsform.
|
|
35
|
-
</ksd-details-content>
|
|
36
|
-
</ksd-details>
|
|
37
|
-
<ksd-details [open]="open3()" (toggled)="open3.set(!open3())">
|
|
38
|
-
<ksd-details-summary>Ansvarlig selskap (ANS/DA)</ksd-details-summary>
|
|
39
|
-
<ksd-details-content>
|
|
40
|
-
Skal dere starte opp egen virksomhet sammen? Samarbeider dere godt?
|
|
41
|
-
Krever virksomheten små investeringer og innebærer liten økonomisk
|
|
42
|
-
risiko? Da kan ansvarlig selskap være aktuelt.
|
|
43
|
-
</ksd-details-content>
|
|
44
|
-
</ksd-details>
|
|
45
|
-
`,
|
|
46
|
-
})
|
|
47
|
-
export class ControlledDetails {
|
|
48
|
-
open1 = signal(false)
|
|
49
|
-
open2 = signal(false)
|
|
50
|
-
open3 = signal(false)
|
|
51
|
-
|
|
52
|
-
isOpen = computed(() =>
|
|
53
|
-
[this.open1(), this.open2(), this.open3()].every(Boolean),
|
|
54
|
-
)
|
|
55
|
-
toggleOpenText = computed(() => (this.isOpen() ? 'Lukk alle' : 'Åpne alle'))
|
|
56
|
-
|
|
57
|
-
toggleOpen = () => {
|
|
58
|
-
const isOpen = this.isOpen()
|
|
59
|
-
this.open1.set(!isOpen)
|
|
60
|
-
this.open2.set(!isOpen)
|
|
61
|
-
this.open3.set(!isOpen)
|
|
62
|
-
}
|
|
63
|
-
}
|
|
@@ -1,89 +0,0 @@
|
|
|
1
|
-
import { Meta, Canvas, Controls, Primary } from '@storybook/addon-docs/blocks'
|
|
2
|
-
import * as DetailsStories from './details.stories'
|
|
3
|
-
|
|
4
|
-
<Meta of={DetailsStories} />
|
|
5
|
-
|
|
6
|
-
# Details
|
|
7
|
-
|
|
8
|
-
Med `Details` kan du presentera mykje innhald på liten plass i ei eller fleire rader. Heile rada er klikkbar og let brukarar opna eller lukka visninga av innhaldet under.
|
|
9
|
-
|
|
10
|
-
<Primary />
|
|
11
|
-
<Controls />
|
|
12
|
-
|
|
13
|
-
## Bruk
|
|
14
|
-
|
|
15
|
-
```html
|
|
16
|
-
/* Utan ramme */
|
|
17
|
-
<ksd-details>
|
|
18
|
-
<ksd-details-summary>Vedlegg</ksd-details-summary>
|
|
19
|
-
<ksd-details-content>Vedlegg 1, vedlegg 2, vedlegg 3</ksd-details-content>
|
|
20
|
-
</ksd-details>
|
|
21
|
-
|
|
22
|
-
/* Med ramme */
|
|
23
|
-
|
|
24
|
-
<article ksd-card>
|
|
25
|
-
<ksd-details>
|
|
26
|
-
<ksd-details-summary>Vedlegg</ksd-details-summary>
|
|
27
|
-
<ksd-details-content>Vedlegg 1, vedlegg 2, vedlegg 3</ksd-details-content>
|
|
28
|
-
</ksd-details>
|
|
29
|
-
</article>
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
## Kodeeksempler
|
|
33
|
-
|
|
34
|
-
### Med ramme
|
|
35
|
-
|
|
36
|
-
`Details` kan visast med ramme ved å leggje den i eit `Card`.
|
|
37
|
-
Dette kan passa i tilfelle der `Details` ikkje fyller heile sida, eller når det berre er ei rad.
|
|
38
|
-
|
|
39
|
-
<Canvas of={DetailsStories.InCard} />
|
|
40
|
-
|
|
41
|
-
### Med fargar
|
|
42
|
-
|
|
43
|
-
`Details` kan visast i fargar frå temaet ditt.
|
|
44
|
-
|
|
45
|
-
<Canvas of={DetailsStories.InCardWithColor} />
|
|
46
|
-
|
|
47
|
-
#### Kontrollert
|
|
48
|
-
|
|
49
|
-
`Details` held sjølv styr på om den er open eller lukka, men dette kan òg kontrollerast utanfrå.
|
|
50
|
-
|
|
51
|
-
<Canvas of={DetailsStories.Controlled} />
|
|
52
|
-
|
|
53
|
-
## Retningslinjer
|
|
54
|
-
|
|
55
|
-
Ikkje bruk `Details` til å skjula innhald for å gjera sida "ryddigare". Når vi skjuler innhold er det fare for at brukerne ikke se innholdet i det hele tatt. Finn ut om du faktisk må skjula innhald og ver klar over kvifor du gjer det.
|
|
56
|
-
|
|
57
|
-
**Passar til å**
|
|
58
|
-
|
|
59
|
-
- samla innhald
|
|
60
|
-
- gjera det frivillig å sjå innhald som er mindre viktig
|
|
61
|
-
- visa tilleggsinformasjon som kan vera til hjelp for brukarane
|
|
62
|
-
|
|
63
|
-
**Passar ikkje til å**
|
|
64
|
-
|
|
65
|
-
- visa viktig innhald som alle bør sjå når dei kjem til sida
|
|
66
|
-
- oppsummera feilmeldingar - bruk heller [`ErrorSummary`](/docs/komponenter-errorsummary--docs)
|
|
67
|
-
|
|
68
|
-
### Unngå nøsta lister
|
|
69
|
-
|
|
70
|
-
Ikkje legg ein `Details` inni ein annan, det vi kallar nøsta lister. Det kan bli forvirrande for brukaren å forstå kva som er opna og lukka, spesielt når fleire nivå kan opnast samtidig.
|
|
71
|
-
|
|
72
|
-
## Tekst
|
|
73
|
-
|
|
74
|
-
Sørg for at overskrifta gjev ei god skildring av kva innhaldet i `Details` er. Overskriftene kan ha stor tyding for om brukarane finn det dei treng, om innhaldet blir lese og om det kan reknast som tilgjengeleg for alle brukarar. «Vis meir» eller «Les meir her» er ikkje gode nok titlar. Har du ein `Details` med mange nedtrekk, kan du ha ei hovudoverskrift eller temaoverskrift over heile lista.
|
|
75
|
-
|
|
76
|
-
Hold innholdet i `Details` kort for å sikre at det lett kan relateres til overskriften. Om innhaldet er for langt bør du fordela innhaldet i fleire `Details`. Ved veldig mykje innhald kan det vere betre å vurdere å lage eigne sider.
|
|
77
|
-
|
|
78
|
-
## Tilgjengelegheit
|
|
79
|
-
|
|
80
|
-
[`Chevron`](https://aksel.nav.no/ikoner/ChevronDown)-ikonet er plassert til venstre for teksten, av omsyn til brukarar med nedsett synsfelt. Dei ser berre ein liten del av skjermen om gongen, og skannar gjerne vertikalt nedover på venstre side. Difor bør funksjonelle element stå til venstre, der dei lettare blir oppdaga. Plasseringa av chevronen bør òg vere konsekvent for å unngå forveksling med andre element.
|
|
81
|
-
|
|
82
|
-
Ikkje plasser andre interaktive element inn i `Details.Summary`, då heile rada skal vera klikkbar. Ikonet og teksten skal _ikkje_ lenka til ulike handlingar (til dømes at teksten går vidare til ei side, medan ikonet opnar lista). Brukarane ventar ikkje at ikon og tekst skal gje ulikt resultat når dei vel dei.
|
|
83
|
-
|
|
84
|
-
<kbd>Space</kbd> eller <kbd>Enter</kbd> åpnar eller lukkar `Details`.
|
|
85
|
-
|
|
86
|
-
## Kjende manglar
|
|
87
|
-
|
|
88
|
-
I Firefox og Safari blir det ikkje spelt av animasjon når `Details` blir opna og lukka.
|
|
89
|
-
Sjå [interpolate-size](https://developer.mozilla.org/en-US/docs/Web/CSS/interpolate-size).
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { Component } from '@angular/core'
|
|
2
|
-
import { render, screen } from '@testing-library/angular'
|
|
3
|
-
import userEvent from '@testing-library/user-event'
|
|
4
|
-
import { Details } from './details'
|
|
5
|
-
import { DetailsContent } from './details-content'
|
|
6
|
-
import { DetailsSummary } from './details-summary'
|
|
7
|
-
|
|
8
|
-
describe('Details', () => {
|
|
9
|
-
it('should have summary, content and be open when clicked', async () => {
|
|
10
|
-
@Component({
|
|
11
|
-
template: `
|
|
12
|
-
<ksd-details>
|
|
13
|
-
<ksd-details-summary>Details Summary Text</ksd-details-summary>
|
|
14
|
-
<ksd-details-content
|
|
15
|
-
>The fantastic details content text</ksd-details-content
|
|
16
|
-
>
|
|
17
|
-
</ksd-details>
|
|
18
|
-
`,
|
|
19
|
-
imports: [Details, DetailsContent, DetailsSummary],
|
|
20
|
-
})
|
|
21
|
-
class TestDetails {}
|
|
22
|
-
|
|
23
|
-
await render(TestDetails)
|
|
24
|
-
|
|
25
|
-
const user = userEvent.setup()
|
|
26
|
-
const detailsExpandButton = screen.getByRole('button')
|
|
27
|
-
|
|
28
|
-
await user.click(detailsExpandButton)
|
|
29
|
-
|
|
30
|
-
expect(screen.getByText('Details Summary Text'))
|
|
31
|
-
expect(screen.getByText('The fantastic details content text'))
|
|
32
|
-
expect(detailsExpandButton).toHaveAttribute('aria-expanded', 'true')
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
it('should render details with open state as controlled', async () => {
|
|
36
|
-
@Component({
|
|
37
|
-
template: `
|
|
38
|
-
<ksd-details [open]="true" (toggled)="noop()">
|
|
39
|
-
<ksd-details-summary>Details Summary Text</ksd-details-summary>
|
|
40
|
-
<ksd-details-content
|
|
41
|
-
>The fantastic details content text</ksd-details-content
|
|
42
|
-
>
|
|
43
|
-
</ksd-details>
|
|
44
|
-
`,
|
|
45
|
-
imports: [Details, DetailsContent, DetailsSummary],
|
|
46
|
-
})
|
|
47
|
-
class TestDetails {
|
|
48
|
-
noop = () => undefined
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
await render(TestDetails)
|
|
52
|
-
|
|
53
|
-
const detailsExpandButton = screen.getByRole('button')
|
|
54
|
-
expect(detailsExpandButton).toHaveAttribute('aria-expanded', 'true')
|
|
55
|
-
})
|
|
56
|
-
})
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { Meta, moduleMetadata, StoryObj } from '@storybook/angular'
|
|
2
|
-
import { Card } from '../card/card'
|
|
3
|
-
import { ControlledDetails } from './controlled-details'
|
|
4
|
-
import { Details } from './details'
|
|
5
|
-
import { DetailsContent } from './details-content'
|
|
6
|
-
import { DetailsSummary } from './details-summary'
|
|
7
|
-
|
|
8
|
-
const meta: Meta<Details> = {
|
|
9
|
-
component: Details,
|
|
10
|
-
title: 'Komponenter/Details',
|
|
11
|
-
parameters: {
|
|
12
|
-
layout: 'padded',
|
|
13
|
-
},
|
|
14
|
-
decorators: [
|
|
15
|
-
moduleMetadata({
|
|
16
|
-
imports: [
|
|
17
|
-
Card,
|
|
18
|
-
Details,
|
|
19
|
-
DetailsContent,
|
|
20
|
-
DetailsSummary,
|
|
21
|
-
ControlledDetails,
|
|
22
|
-
],
|
|
23
|
-
}),
|
|
24
|
-
],
|
|
25
|
-
}
|
|
26
|
-
export default meta
|
|
27
|
-
type Story = StoryObj<Details>
|
|
28
|
-
|
|
29
|
-
export const Preview: Story = {
|
|
30
|
-
args: {},
|
|
31
|
-
parameters: {
|
|
32
|
-
summaryText: 'Vedlegg',
|
|
33
|
-
contentText: 'Vedlegg 1, vedlegg 2, vedlegg 3',
|
|
34
|
-
},
|
|
35
|
-
render: (args, context) => ({
|
|
36
|
-
props: {
|
|
37
|
-
...args,
|
|
38
|
-
summary: context.parameters['summaryText'],
|
|
39
|
-
content: context.parameters['contentText'],
|
|
40
|
-
},
|
|
41
|
-
template: `
|
|
42
|
-
<ksd-details data-testid="details">
|
|
43
|
-
<ksd-details-summary>Vedlegg</ksd-details-summary>
|
|
44
|
-
<ksd-details-content>Vedlegg 1, vedlegg 2, vedlegg 3</ksd-details-content>
|
|
45
|
-
</ksd-details>
|
|
46
|
-
`,
|
|
47
|
-
}),
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
export const WithoutCard: Story = {
|
|
51
|
-
render: () => ({
|
|
52
|
-
template: `
|
|
53
|
-
<ksd-details>
|
|
54
|
-
<ksd-details-summary>Vedlegg</ksd-details-summary>
|
|
55
|
-
<ksd-details-content>Vedlegg 1, vedlegg 2, vedlegg 3</ksd-details-content>
|
|
56
|
-
</ksd-details>
|
|
57
|
-
`,
|
|
58
|
-
}),
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export const InCard: Story = {
|
|
62
|
-
render: () => ({
|
|
63
|
-
template: `
|
|
64
|
-
<article ksd-card>
|
|
65
|
-
<ksd-details>
|
|
66
|
-
<ksd-details-summary>Vedlegg</ksd-details-summary>
|
|
67
|
-
<ksd-details-content>Vedlegg 1, vedlegg 2, vedlegg 3</ksd-details-content>
|
|
68
|
-
</ksd-details>
|
|
69
|
-
</article>
|
|
70
|
-
`,
|
|
71
|
-
}),
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export const InCardWithColor: Story = {
|
|
75
|
-
render: () => ({
|
|
76
|
-
template: `
|
|
77
|
-
<div style="display: flex; flex-direction: column; gap: 1rem">
|
|
78
|
-
<article ksd-card>
|
|
79
|
-
<ksd-details data-color="accent">
|
|
80
|
-
<ksd-details-summary>Vedlegg</ksd-details-summary>
|
|
81
|
-
<ksd-details-content>Vedlegg 1, vedlegg 2, vedlegg 3</ksd-details-content>
|
|
82
|
-
</ksd-details>
|
|
83
|
-
</article>
|
|
84
|
-
<article ksd-card>
|
|
85
|
-
<ksd-details data-color="accent" variant="tinted">
|
|
86
|
-
<ksd-details-summary>Vedlegg</ksd-details-summary>
|
|
87
|
-
<ksd-details-content>Vedlegg 1, vedlegg 2, vedlegg 3</ksd-details-content>
|
|
88
|
-
</ksd-details>
|
|
89
|
-
</article>
|
|
90
|
-
</div>
|
|
91
|
-
`,
|
|
92
|
-
}),
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export const WithDifferentSizes: Story = {
|
|
96
|
-
render: () => ({
|
|
97
|
-
template: `
|
|
98
|
-
<ksd-details data-size="sm">
|
|
99
|
-
<ksd-details-summary>Vedlegg</ksd-details-summary>
|
|
100
|
-
<ksd-details-content>Vedlegg 1, vedlegg 2, vedlegg 3</ksd-details-content>
|
|
101
|
-
</ksd-details>
|
|
102
|
-
<ksd-details data-size="md">
|
|
103
|
-
<ksd-details-summary>Vedlegg</ksd-details-summary>
|
|
104
|
-
<ksd-details-content>Vedlegg 1, vedlegg 2, vedlegg 3</ksd-details-content>
|
|
105
|
-
</ksd-details>
|
|
106
|
-
<ksd-details data-size="lg">
|
|
107
|
-
<ksd-details-summary>Vedlegg</ksd-details-summary>
|
|
108
|
-
<ksd-details-content>Vedlegg 1, vedlegg 2, vedlegg 3</ksd-details-content>
|
|
109
|
-
</ksd-details>
|
|
110
|
-
`,
|
|
111
|
-
}),
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
export const DefaultOpen: Story = {
|
|
115
|
-
render: () => ({
|
|
116
|
-
template: `
|
|
117
|
-
<ksd-details [defaultOpen]="true">
|
|
118
|
-
<ksd-details-summary>Vedlegg</ksd-details-summary>
|
|
119
|
-
<ksd-details-content>Vedlegg 1, vedlegg 2, vedlegg 3</ksd-details-content>
|
|
120
|
-
</ksd-details>
|
|
121
|
-
`,
|
|
122
|
-
}),
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
export const Controlled: StoryObj<ControlledDetails> = {
|
|
126
|
-
render: () => ({
|
|
127
|
-
template: `<fiks-controlled-details />`,
|
|
128
|
-
}),
|
|
129
|
-
}
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Component,
|
|
3
|
-
CUSTOM_ELEMENTS_SCHEMA,
|
|
4
|
-
ElementRef,
|
|
5
|
-
input,
|
|
6
|
-
output,
|
|
7
|
-
viewChild,
|
|
8
|
-
} from '@angular/core'
|
|
9
|
-
import '@u-elements/u-details'
|
|
10
|
-
|
|
11
|
-
@Component({
|
|
12
|
-
selector: 'ksd-details',
|
|
13
|
-
schemas: [CUSTOM_ELEMENTS_SCHEMA],
|
|
14
|
-
template: `
|
|
15
|
-
<u-details
|
|
16
|
-
#detailsRef
|
|
17
|
-
class="ds-details"
|
|
18
|
-
[attr.data-variant]="variant()"
|
|
19
|
-
[attr.open]="(open() ?? defaultOpen()) || undefined"
|
|
20
|
-
[attr.data-color]="dataColor()"
|
|
21
|
-
[attr.data-size]="dataSize()"
|
|
22
|
-
(toggle)="onToggle($event)"
|
|
23
|
-
>
|
|
24
|
-
<u-summary>
|
|
25
|
-
<ng-content select="ksd-details-summary" />
|
|
26
|
-
</u-summary>
|
|
27
|
-
<div>
|
|
28
|
-
<ng-content select="ksd-details-content" />
|
|
29
|
-
</div>
|
|
30
|
-
</u-details>
|
|
31
|
-
`,
|
|
32
|
-
styles: `
|
|
33
|
-
/* Styles needed since Designsystemet styles doesnt expect an element wrapping .ds-details, which we have */
|
|
34
|
-
.ds-card > :host(:last-of-type) > .ds-details {
|
|
35
|
-
border-bottom: 0;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
.ds-card > :host(:first-of-type) > .ds-details {
|
|
39
|
-
border-top: 0;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
:host(:not(:first-of-type)) > .ds-details {
|
|
43
|
-
border-top: 0;
|
|
44
|
-
margin-top: 0;
|
|
45
|
-
}
|
|
46
|
-
`,
|
|
47
|
-
})
|
|
48
|
-
export class Details {
|
|
49
|
-
readonly dataSize = input<'sm' | 'md' | 'lg' | undefined>(undefined, {
|
|
50
|
-
// eslint-disable-next-line @angular-eslint/no-input-rename
|
|
51
|
-
alias: 'data-size',
|
|
52
|
-
})
|
|
53
|
-
readonly dataColor = input<string | undefined>(undefined, {
|
|
54
|
-
// eslint-disable-next-line @angular-eslint/no-input-rename
|
|
55
|
-
alias: 'data-color',
|
|
56
|
-
})
|
|
57
|
-
readonly variant = input<'tinted' | 'default'>('default')
|
|
58
|
-
readonly defaultOpen = input<boolean>(false)
|
|
59
|
-
readonly open = input<boolean | undefined>(undefined)
|
|
60
|
-
readonly toggled = output<Event>()
|
|
61
|
-
private detailsRef = viewChild<ElementRef<HTMLDetailsElement>>('detailsRef')
|
|
62
|
-
|
|
63
|
-
onToggle(event: Event) {
|
|
64
|
-
const details = this.detailsRef()?.nativeElement
|
|
65
|
-
if (details && details.open !== this.open()) {
|
|
66
|
-
this.toggled.emit(event)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { Component, computed, effect, inject, input } from '@angular/core'
|
|
2
|
-
import { ValidationMessage } from '../validation-message'
|
|
3
|
-
import { FieldState } from './field-state'
|
|
4
|
-
|
|
5
|
-
@Component({
|
|
6
|
-
selector: 'ksd-field-counter',
|
|
7
|
-
imports: [ValidationMessage],
|
|
8
|
-
template: `
|
|
9
|
-
<div data-field="description" class="ds-sr-only" aria-live="polite">
|
|
10
|
-
@if (hasExceededLimit()) {
|
|
11
|
-
{{ excessCount() }} tegn for mye
|
|
12
|
-
}
|
|
13
|
-
</div>
|
|
14
|
-
@if (hasExceededLimit()) {
|
|
15
|
-
<p ksd-validation-message>{{ excessCount() }} tegn for mye</p>
|
|
16
|
-
} @else {
|
|
17
|
-
<p data-field="validation">{{ remainder() }} tegn igjen</p>
|
|
18
|
-
}
|
|
19
|
-
`,
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Apply custom styles here to get correct spacing because
|
|
23
|
-
* the rendered host element from Angular is getting in the way
|
|
24
|
-
*/
|
|
25
|
-
styles: `
|
|
26
|
-
:host > * {
|
|
27
|
-
margin-top: var(--dsc-field-content-spacing);
|
|
28
|
-
}
|
|
29
|
-
`,
|
|
30
|
-
})
|
|
31
|
-
export class FieldCounter {
|
|
32
|
-
/**
|
|
33
|
-
* The maximum allowed characters.
|
|
34
|
-
*
|
|
35
|
-
**/
|
|
36
|
-
readonly limit = input.required<number>()
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* How many characters have been typed.
|
|
40
|
-
*
|
|
41
|
-
**/
|
|
42
|
-
readonly count = input.required<number>()
|
|
43
|
-
protected readonly remainder = computed(() => this.limit() - this.count())
|
|
44
|
-
protected readonly excessCount = computed(() => Math.abs(this.remainder()))
|
|
45
|
-
protected readonly hasExceededLimit = computed(
|
|
46
|
-
() => this.count() > this.limit(),
|
|
47
|
-
)
|
|
48
|
-
|
|
49
|
-
private fieldState = inject(FieldState)
|
|
50
|
-
|
|
51
|
-
constructor() {
|
|
52
|
-
effect(() => {
|
|
53
|
-
this.fieldState.hasExceededCounter.set(this.hasExceededLimit())
|
|
54
|
-
})
|
|
55
|
-
}
|
|
56
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Component } from '@angular/core'
|
|
2
|
-
import { ValidationMessage } from '../validation-message'
|
|
3
|
-
|
|
4
|
-
@Component({
|
|
5
|
-
selector: '[ksd-error]',
|
|
6
|
-
template: `<ng-content />`,
|
|
7
|
-
hostDirectives: [
|
|
8
|
-
{
|
|
9
|
-
directive: ValidationMessage,
|
|
10
|
-
},
|
|
11
|
-
],
|
|
12
|
-
})
|
|
13
|
-
export class FieldError {}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Lifted from Designsystemet core repo.
|
|
3
|
-
* Takes care of binding ids, labels and aria-describedby attributes
|
|
4
|
-
*
|
|
5
|
-
* @param fieldElement - The field element to observe
|
|
6
|
-
* @returns A function to disconnect the observer
|
|
7
|
-
* */
|
|
8
|
-
export function fieldObserver(fieldElement: HTMLElement | null) {
|
|
9
|
-
if (!fieldElement) return
|
|
10
|
-
|
|
11
|
-
const elements = new Map<Element, string | null>()
|
|
12
|
-
const typeCounter = new Map<string, number>() // Track count for each data-field type
|
|
13
|
-
const uuid = `:${Date.now().toString(36)}${Math.random().toString(36).slice(2, 5)}`
|
|
14
|
-
let input: Element | null = null
|
|
15
|
-
let describedby = ''
|
|
16
|
-
|
|
17
|
-
const process = (mutations: Partial<MutationRecord>[]) => {
|
|
18
|
-
const changed: Node[] = []
|
|
19
|
-
const removed: Node[] = []
|
|
20
|
-
|
|
21
|
-
// Merge MutationRecords
|
|
22
|
-
for (const mutation of mutations) {
|
|
23
|
-
if (mutation.attributeName) changed.push(mutation.target ?? fieldElement)
|
|
24
|
-
// @ts-expect-error - addedNodes is not typed
|
|
25
|
-
changed.push(...(mutation.addedNodes || []))
|
|
26
|
-
removed.push(...(mutation.removedNodes || []))
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Register elements
|
|
30
|
-
for (const el of changed) {
|
|
31
|
-
if (!isElement(el)) continue
|
|
32
|
-
|
|
33
|
-
if (isLabel(el)) elements.set(el, el.htmlFor)
|
|
34
|
-
else if (el.hasAttribute('data-field')) elements.set(el, el.id)
|
|
35
|
-
else if (isInputLike(el)) {
|
|
36
|
-
input = el
|
|
37
|
-
describedby = el.getAttribute('aria-describedby') || ''
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Reset removed elements
|
|
42
|
-
for (const el of removed) {
|
|
43
|
-
if (!isElement(el)) continue
|
|
44
|
-
|
|
45
|
-
if (input === el) input = null
|
|
46
|
-
if (elements.has(el)) {
|
|
47
|
-
setAttr(el, isLabel(el) ? 'for' : 'id', elements.get(el))
|
|
48
|
-
elements.delete(el)
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Connect elements
|
|
53
|
-
const describedbyIds = [describedby] // Keep original aria-describedby
|
|
54
|
-
const inputId = input?.id || uuid
|
|
55
|
-
|
|
56
|
-
// Reset type counters since we reprocess all elements
|
|
57
|
-
typeCounter.clear()
|
|
58
|
-
|
|
59
|
-
for (const [el, value] of elements) {
|
|
60
|
-
const descriptionType = el.getAttribute('data-field')
|
|
61
|
-
let id: string
|
|
62
|
-
|
|
63
|
-
if (descriptionType) {
|
|
64
|
-
// Increment type counter for this type
|
|
65
|
-
const count = (typeCounter.get(descriptionType) || 0) + 1
|
|
66
|
-
typeCounter.set(descriptionType, count)
|
|
67
|
-
id = `${inputId}:${descriptionType}:${count}`
|
|
68
|
-
} else {
|
|
69
|
-
id = inputId
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (!value) setAttr(el, isLabel(el) ? 'for' : 'id', id) // Ensure we have a value
|
|
73
|
-
if (descriptionType === 'validation')
|
|
74
|
-
describedbyIds.unshift(el.id) // Validations to the front
|
|
75
|
-
else if (descriptionType) describedbyIds.push(el.id) // Other descriptions to the back
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
setAttr(input, 'id', inputId)
|
|
79
|
-
setAttr(input, 'aria-describedby', describedbyIds.join(' ').trim())
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const observer = createOptimizedMutationObserver(process)
|
|
83
|
-
observer.observe(fieldElement, {
|
|
84
|
-
attributeFilter: ['id', 'for', 'aria-describedby'],
|
|
85
|
-
attributes: true,
|
|
86
|
-
childList: true,
|
|
87
|
-
subtree: true,
|
|
88
|
-
})
|
|
89
|
-
|
|
90
|
-
process([{ addedNodes: fieldElement.querySelectorAll('*') }]) // Initial setup
|
|
91
|
-
observer.takeRecords() // Clear initial setup queue
|
|
92
|
-
return () => observer.disconnect()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Utilities
|
|
96
|
-
export const isElement = (node: Node) => node instanceof Element
|
|
97
|
-
export const isLabel = (node: Node) => node instanceof HTMLLabelElement
|
|
98
|
-
export const isInputLike = (node: unknown): node is HTMLInputElement =>
|
|
99
|
-
node instanceof HTMLElement &&
|
|
100
|
-
'validity' in node &&
|
|
101
|
-
!(node instanceof HTMLButtonElement) // Matches input, textarea, select and form accosiated custom elements
|
|
102
|
-
|
|
103
|
-
const setAttr = (el: Element | null, name: string, value?: string | null) =>
|
|
104
|
-
value ? el?.setAttribute(name, value) : el?.removeAttribute(name)
|
|
105
|
-
|
|
106
|
-
// Speed up MutationObserver by debouncing, clearing internal queue after changes and only running when page is visible
|
|
107
|
-
function createOptimizedMutationObserver(callback: MutationCallback) {
|
|
108
|
-
const queue: MutationRecord[] = []
|
|
109
|
-
const observer = new MutationObserver((mutations) => {
|
|
110
|
-
if (!queue.length) requestAnimationFrame(process)
|
|
111
|
-
queue.push(...mutations)
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
const process = () => {
|
|
115
|
-
callback(queue, observer)
|
|
116
|
-
queue.length = 0 // Reset queue
|
|
117
|
-
observer.takeRecords() // Clear queue due to DOM changes in callback
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
return observer
|
|
121
|
-
}
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { computed, Injectable, signal } from '@angular/core'
|
|
2
|
-
|
|
3
|
-
@Injectable()
|
|
4
|
-
export class FieldState {
|
|
5
|
-
/**
|
|
6
|
-
* Whether the field counter has exceeded its limit
|
|
7
|
-
*/
|
|
8
|
-
hasExceededCounter = signal(false)
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Whether the field has errors projected from the outside
|
|
12
|
-
*/
|
|
13
|
-
hasProjectedErrors = signal(false)
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Whether the field has any errors associated with it
|
|
17
|
-
*/
|
|
18
|
-
hasError = computed(
|
|
19
|
-
() => this.hasExceededCounter() || this.hasProjectedErrors(),
|
|
20
|
-
)
|
|
21
|
-
}
|