@ks-digital/designsystem-angular 0.0.1-alpha.23 → 0.0.1-alpha.25

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.
Files changed (109) hide show
  1. package/.storybook/customTheme.ts +15 -0
  2. package/.storybook/default-args.ts +18 -0
  3. package/.storybook/main.ts +27 -0
  4. package/.storybook/manager.ts +10 -0
  5. package/.storybook/preview-head.html +16 -0
  6. package/.storybook/preview.ts +70 -0
  7. package/.storybook/themes.ts +9 -0
  8. package/.storybook/tsconfig.json +16 -0
  9. package/.storybook/vite.config.mts +5 -0
  10. package/README.md +3 -3
  11. package/eslint.config.mjs +28 -0
  12. package/ng-package.json +9 -0
  13. package/package.json +18 -27
  14. package/project.json +81 -0
  15. package/src/components/alert/alert.mdx +46 -0
  16. package/src/components/alert/alert.spec.ts +33 -0
  17. package/src/components/alert/alert.stories.ts +138 -0
  18. package/src/components/alert/alert.ts +46 -0
  19. package/src/components/alert/index.ts +1 -0
  20. package/src/components/button/button.mdx +40 -0
  21. package/src/components/button/button.spec.ts +86 -0
  22. package/src/components/button/button.stories.ts +123 -0
  23. package/src/components/button/button.ts +60 -0
  24. package/src/components/button/index.ts +1 -0
  25. package/src/components/card/card-block.ts +10 -0
  26. package/src/components/card/card.mdx +100 -0
  27. package/src/components/card/card.spec.ts +70 -0
  28. package/src/components/card/card.stories.ts +101 -0
  29. package/src/components/card/card.ts +44 -0
  30. package/src/components/card/index.ts +2 -0
  31. package/src/components/checkbox/README.md +13 -0
  32. package/src/components/checkbox/checkbox.mdx +50 -0
  33. package/src/components/checkbox/checkbox.spec.ts +21 -0
  34. package/src/components/checkbox/checkbox.stories.ts +182 -0
  35. package/src/components/checkbox/index.ts +0 -0
  36. package/src/components/colors.ts +36 -0
  37. package/src/components/common-inputs.ts +30 -0
  38. package/src/components/details/controlled-details.ts +63 -0
  39. package/src/components/details/details-content.ts +7 -0
  40. package/src/components/details/details-summary.ts +7 -0
  41. package/src/components/details/details.mdx +89 -0
  42. package/src/components/details/details.spec.ts +56 -0
  43. package/src/components/details/details.stories.ts +129 -0
  44. package/src/components/details/details.ts +69 -0
  45. package/src/components/details/index.ts +3 -0
  46. package/src/components/field/field-counter.ts +56 -0
  47. package/src/components/field/field-description.ts +10 -0
  48. package/src/components/field/field-error.ts +13 -0
  49. package/src/components/field/field-observer.ts +121 -0
  50. package/src/components/field/field-state.ts +21 -0
  51. package/src/components/field/field.mdx +40 -0
  52. package/src/components/field/field.spec.ts +131 -0
  53. package/src/components/field/field.stories.ts +98 -0
  54. package/src/components/field/field.ts +70 -0
  55. package/src/components/field/index.ts +3 -0
  56. package/src/components/fieldset/fieldset-description.ts +8 -0
  57. package/src/components/fieldset/fieldset-legend.ts +11 -0
  58. package/src/components/fieldset/fieldset.spec.ts +80 -0
  59. package/src/components/fieldset/fieldset.ts +11 -0
  60. package/src/components/fieldset/index.ts +3 -0
  61. package/src/components/input/index.ts +1 -0
  62. package/src/components/input/input.mdx +11 -0
  63. package/src/components/input/input.spec.ts +25 -0
  64. package/src/components/input/input.stories.ts +72 -0
  65. package/src/components/input/input.ts +67 -0
  66. package/src/components/label/index.ts +1 -0
  67. package/src/components/label/label.ts +17 -0
  68. package/src/components/paragraph/index.ts +1 -0
  69. package/src/components/paragraph/paragraph.ts +10 -0
  70. package/src/components/popover/controlled-popover.ts +62 -0
  71. package/src/components/popover/index.ts +1 -0
  72. package/src/components/popover/popover.mdx +81 -0
  73. package/src/components/popover/popover.spec.ts +143 -0
  74. package/src/components/popover/popover.stories.ts +63 -0
  75. package/src/components/popover/popover.ts +186 -0
  76. package/src/components/radio/radio.mdx +117 -0
  77. package/src/components/radio/radio.stories.ts +226 -0
  78. package/src/components/search/index.ts +4 -0
  79. package/src/components/search/search-button.ts +35 -0
  80. package/src/components/search/search-clear.ts +57 -0
  81. package/src/components/search/search-input.ts +18 -0
  82. package/src/components/search/search.mdx +56 -0
  83. package/src/components/search/search.spec.ts +48 -0
  84. package/src/components/search/search.stories.ts +205 -0
  85. package/src/components/search/search.ts +50 -0
  86. package/src/components/spinner/index.ts +1 -0
  87. package/src/components/spinner/spinner.mdx +24 -0
  88. package/src/components/spinner/spinner.spec.ts +13 -0
  89. package/src/components/spinner/spinner.stories.ts +54 -0
  90. package/src/components/spinner/spinner.ts +62 -0
  91. package/src/components/switch/switch.mdx +82 -0
  92. package/src/components/switch/switch.stories.ts +94 -0
  93. package/src/components/textarea/textarea.mdx +14 -0
  94. package/src/components/textarea/textarea.stories.ts +52 -0
  95. package/src/components/validation-message/index.ts +1 -0
  96. package/src/components/validation-message/validation-message.ts +11 -0
  97. package/src/index.ts +14 -0
  98. package/src/test-setup.ts +12 -0
  99. package/src/utils/log-if-devmode.ts +13 -0
  100. package/src/utils/random-id.ts +3 -0
  101. package/tsconfig.json +34 -0
  102. package/tsconfig.lib.json +28 -0
  103. package/tsconfig.lib.prod.json +9 -0
  104. package/tsconfig.spec.json +30 -0
  105. package/vite.config.mts +35 -0
  106. package/dist/README.md +0 -55
  107. package/dist/fesm2022/ks-digital-designsystem-angular.mjs +0 -1068
  108. package/dist/fesm2022/ks-digital-designsystem-angular.mjs.map +0 -1
  109. package/dist/index.d.ts +0 -315
@@ -0,0 +1,69 @@
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
+ }
@@ -0,0 +1,3 @@
1
+ export { Details } from './details'
2
+ export { DetailsContent } from './details-content'
3
+ export { DetailsSummary } from './details-summary'
@@ -0,0 +1,56 @@
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
+ }
@@ -0,0 +1,10 @@
1
+ import { Component } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: '[ksd-field-description]',
5
+ host: {
6
+ 'data-field': 'description',
7
+ },
8
+ template: `<ng-content />`,
9
+ })
10
+ export class FieldDescription {}
@@ -0,0 +1,13 @@
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 {}
@@ -0,0 +1,121 @@
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
+ }
@@ -0,0 +1,21 @@
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
+ }
@@ -0,0 +1,40 @@
1
+ import {
2
+ Meta,
3
+ Primary,
4
+ Canvas,
5
+ Controls,
6
+ Story,
7
+ } from '@storybook/addon-docs/blocks'
8
+
9
+ import * as FieldStories from './field.stories'
10
+
11
+ <Meta of={FieldStories} />
12
+
13
+ # Field
14
+
15
+ `Field` er et hjelpemiddel for å automatisk koble sammen labels, input og feilmeldinger. Field kan brukes sammen med input, textarea og select.
16
+
17
+ <Primary />
18
+ <Controls />
19
+
20
+ ## Bruk
21
+
22
+ ```tsx
23
+ import {
24
+ Field,
25
+ FieldError,
26
+ Label,
27
+ Input,
28
+ } from '@ks-digital/designsystem-angular'
29
+ ;<ksd-field>
30
+ <ksd-label>Kort beskrivelse</ksd-label>
31
+ <input ksd-input type="text" />
32
+ <ksd-error>Feilmelding</ksd-error>
33
+ </ksd-field>
34
+ ```
35
+
36
+ ## Feilmeldinger
37
+
38
+ Bruk `<ksd-error>` for å sette en feilmelding. Du må selv kontrollere når disse skal vises eller skjules. Det er tillatt å vise flere fielmeldinger av gangen.
39
+
40
+ <Canvas of={FieldStories.Error} />
@@ -0,0 +1,131 @@
1
+ import { render, screen } from '@testing-library/angular'
2
+ import userEvent from '@testing-library/user-event'
3
+ import { Input } from '../input/input'
4
+ import { Label } from '../label/label'
5
+ import { Field } from './field'
6
+ import { FieldDescription } from './field-description'
7
+
8
+ test('should connect checkbox and label', async () => {
9
+ await render(
10
+ `
11
+ <ksd-field>
12
+ <ksd-label> Check me </ksd-label>
13
+ <input ksd-input type="checkbox" value="telefon" />
14
+ </ksd-field>`,
15
+ { imports: [Field, Label, Input] },
16
+ )
17
+
18
+ const label = screen.getByText('Check me')
19
+ const checkbox = screen.getByRole('checkbox')
20
+
21
+ expect(label.getAttribute('for')).toBe(checkbox.getAttribute('id'))
22
+ })
23
+
24
+ describe('should connect checkbox and description', () => {
25
+ test('should connect checkbox and description', async () => {
26
+ await render(
27
+ `
28
+ <ksd-field>
29
+ <ksd-label> Check me </ksd-label>
30
+ <input ksd-input type="checkbox" value="telefon" />
31
+ <p ksd-field-description>Description</p>
32
+ </ksd-field>`,
33
+ { imports: [Field, Label, Input, FieldDescription] },
34
+ )
35
+
36
+ const checkbox = screen.getByRole('checkbox')
37
+ const description = screen.getByText('Description')
38
+
39
+ expect(checkbox.getAttribute('aria-describedby')).toBe(
40
+ description.getAttribute('id'),
41
+ )
42
+ })
43
+
44
+ test('should not connect checkbox and description if description is not provided', async () => {
45
+ await render(
46
+ `
47
+ <ksd-field>
48
+ <ksd-label> Check me </ksd-label>
49
+ <input ksd-input type="checkbox" value="telefon" />
50
+ </ksd-field>`,
51
+ { imports: [Field, Label, Input] },
52
+ )
53
+
54
+ const checkbox = screen.getByRole('checkbox')
55
+ expect(checkbox.getAttribute('aria-describedby')).toBeNull()
56
+ })
57
+
58
+ test('should keep existing aria-describedby', async () => {
59
+ await render(
60
+ `
61
+ <ksd-field>
62
+ <ksd-label> Check me </ksd-label>
63
+ <input ksd-input type="checkbox" value="telefon" aria-describedby="existing-id" />
64
+ </ksd-field>`,
65
+ { imports: [Field, Label, Input] },
66
+ )
67
+
68
+ const checkbox = screen.getByRole('checkbox')
69
+ expect(checkbox.getAttribute('aria-describedby')).toBe('existing-id')
70
+ })
71
+
72
+ test('should pass through a user-supplied id', async () => {
73
+ await render(
74
+ `
75
+ <ksd-field>
76
+ <ksd-label> Check me </ksd-label>
77
+ <input ksd-input type="checkbox" value="telefon" id="test" />
78
+ </ksd-field>`,
79
+ { imports: [Field, Label, Input] },
80
+ )
81
+
82
+ const input = screen.getByRole('checkbox')
83
+
84
+ expect(input).toHaveAttribute('id', 'test')
85
+ })
86
+ })
87
+
88
+ describe('FieldCounter', () => {
89
+ test('should render counter with description and validation message connected to input', async () => {
90
+ await render(
91
+ `
92
+ <ksd-field>
93
+ <ksd-label> Check me </ksd-label>
94
+ <input ksd-input [counter]="5" type="text" />
95
+ </ksd-field>`,
96
+ { imports: [Field, Label, Input] },
97
+ )
98
+
99
+ const counter = screen.getByText('5 tegn igjen')
100
+ const input = screen.getByRole('textbox')
101
+
102
+ expect(input.getAttribute('aria-describedby')).toContain(
103
+ counter.getAttribute('id'),
104
+ )
105
+ })
106
+
107
+ test('should show a validation message if the input is too long', async () => {
108
+ await render(
109
+ `
110
+ <ksd-field>
111
+ <ksd-label> Check me </ksd-label>
112
+ <input ksd-input [counter]="5" type="text" />
113
+ </ksd-field>`,
114
+ { imports: [Field, Label, Input] },
115
+ )
116
+
117
+ const input = screen.getByRole('textbox')
118
+ const user = userEvent.setup()
119
+
120
+ await user.type(input, '123456')
121
+
122
+ const visibleMessage = screen.getByText('1 tegn for mye', { selector: 'p' })
123
+ expect(visibleMessage).toBeInTheDocument()
124
+
125
+ const screenReaderMessage = screen.getByText('1 tegn for mye', {
126
+ selector: 'div',
127
+ })
128
+ expect(screenReaderMessage).toBeInTheDocument()
129
+ expect(screenReaderMessage).toHaveAttribute('aria-live', 'polite')
130
+ })
131
+ })
@@ -0,0 +1,98 @@
1
+ import {
2
+ argsToTemplate,
3
+ moduleMetadata,
4
+ type Meta,
5
+ type StoryObj,
6
+ } from '@storybook/angular'
7
+ import { CommonArgs } from '../../../.storybook/default-args'
8
+ import { Input } from '../input/input'
9
+ import { Label } from '../label/label'
10
+ import { Field } from './field'
11
+ import { FieldDescription } from './field-description'
12
+ import { FieldError } from './field-error'
13
+
14
+ type FieldArgs = CommonArgs & {
15
+ readonly: boolean
16
+ disabled: boolean
17
+ counter: number
18
+ }
19
+
20
+ const meta: Meta<Field> = {
21
+ component: Field,
22
+ title: 'Komponenter/Field',
23
+ decorators: [
24
+ moduleMetadata({
25
+ imports: [Label, Field, Input, FieldError, FieldDescription],
26
+ }),
27
+ ],
28
+ }
29
+ export default meta
30
+ type Story = StoryObj<FieldArgs>
31
+
32
+ export const Preview: Story = {
33
+ args: {
34
+ readonly: false,
35
+ disabled: false,
36
+ },
37
+
38
+ render: (args) => ({
39
+ props: args,
40
+ template: `
41
+ <ksd-field>
42
+ <ksd-label>Etternavn</ksd-label>
43
+ <div ksd-field-description>Etternavn kan ikke inneholde mellomrom</div>
44
+ <input ksd-input type="text" ${argsToTemplate(args)} />
45
+ </ksd-field>
46
+ `,
47
+ }),
48
+ }
49
+
50
+ export const Rows: Story = {
51
+ args: {
52
+ ...Preview.args,
53
+ },
54
+
55
+ render: (args) => ({
56
+ props: args,
57
+ template: `
58
+ <ksd-field>
59
+ <ksd-label>Label</ksd-label>
60
+ <textarea ksd-input type="text" rows="4" ${argsToTemplate(args)}></textarea>
61
+ </ksd-field>
62
+ `,
63
+ }),
64
+ }
65
+
66
+ export const Counter: Story = {
67
+ args: {
68
+ ...Preview.args,
69
+ counter: 5,
70
+ },
71
+
72
+ render: (args) => ({
73
+ props: args,
74
+ template: `
75
+ <ksd-field>
76
+ <ksd-label>Label</ksd-label>
77
+ <input ksd-input [counter]="5" type="text" ${argsToTemplate(args)} />
78
+ </ksd-field>
79
+ `,
80
+ }),
81
+ }
82
+
83
+ export const Error: Story = {
84
+ args: {
85
+ ...Preview.args,
86
+ },
87
+
88
+ render: (args) => ({
89
+ props: args,
90
+ template: `
91
+ <ksd-field>
92
+ <ksd-label>Navn</ksd-label>
93
+ <input ksd-input type="text" ${argsToTemplate(args)} />
94
+ <p ksd-error>Feltet må fylles ut</p>
95
+ </ksd-field>
96
+ `,
97
+ }),
98
+ }
@@ -0,0 +1,70 @@
1
+ import {
2
+ afterNextRender,
3
+ Component,
4
+ computed,
5
+ contentChild,
6
+ contentChildren,
7
+ effect,
8
+ ElementRef,
9
+ inject,
10
+ input,
11
+ } from '@angular/core'
12
+ import { CommonInputs } from '../common-inputs'
13
+ import { Input } from '../input/input'
14
+ import { Label } from '../label/label'
15
+ import { ValidationMessage } from '../validation-message'
16
+ import { FieldCounter } from './field-counter'
17
+ import { fieldObserver } from './field-observer'
18
+ import { FieldState } from './field-state'
19
+
20
+ /**
21
+ * Use the Field component to connect inputs and labels
22
+ */
23
+ @Component({
24
+ selector: 'ksd-field',
25
+ hostDirectives: [
26
+ {
27
+ directive: CommonInputs,
28
+ inputs: ['data-size', 'data-color'],
29
+ },
30
+ ],
31
+ host: {
32
+ class: 'ds-field',
33
+ '[attr.dataPosition]': 'position()',
34
+ },
35
+ template: `
36
+ <ng-content />
37
+ @if (hasCounter()) {
38
+ <ksd-field-counter [limit]="limit() ?? 0" [count]="count() ?? 0" />
39
+ }
40
+ `,
41
+ imports: [FieldCounter],
42
+ providers: [FieldState],
43
+ })
44
+ export class Field {
45
+ /**
46
+ * Position of toggle inputs (radio, checkbox, switch) in field
47
+ * @default start
48
+ */
49
+ position = input<'start' | 'end'>('start')
50
+
51
+ private readonly fieldState = inject(FieldState)
52
+ private readonly input = contentChild(Input)
53
+ private readonly label = contentChild(Label)
54
+ private readonly projectedErrors = contentChildren(ValidationMessage)
55
+
56
+ private readonly el = inject(ElementRef)
57
+ protected readonly count = computed(() => this.input()?.value().length)
58
+ protected readonly limit = computed(() => this.input()?.counter())
59
+ protected readonly hasCounter = computed(() => this.limit())
60
+
61
+ constructor() {
62
+ afterNextRender(() => {
63
+ fieldObserver(this.el.nativeElement)
64
+ })
65
+
66
+ effect(() => {
67
+ this.fieldState.hasProjectedErrors.set(this.projectedErrors().length > 0)
68
+ })
69
+ }
70
+ }
@@ -0,0 +1,3 @@
1
+ export { Field } from './field'
2
+ export { FieldDescription } from './field-description'
3
+ export { FieldError } from './field-error'
@@ -0,0 +1,8 @@
1
+ import { Component } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: 'p[ksd-fieldset-description]',
5
+ template: `<ng-content />`,
6
+ host: {},
7
+ })
8
+ export class FieldsetDescription {}
@@ -0,0 +1,11 @@
1
+ import { Component } from '@angular/core'
2
+
3
+ @Component({
4
+ selector: 'legend[ksd-fieldset-legend]',
5
+ host: {
6
+ role: 'legend',
7
+ class: 'ds-label',
8
+ },
9
+ template: ` <ng-content /> `,
10
+ })
11
+ export class FieldsetLegend {}