@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.
- package/.storybook/customTheme.ts +15 -0
- package/.storybook/default-args.ts +18 -0
- package/.storybook/main.ts +27 -0
- package/.storybook/manager.ts +10 -0
- package/.storybook/preview-head.html +16 -0
- package/.storybook/preview.ts +70 -0
- package/.storybook/themes.ts +9 -0
- package/.storybook/tsconfig.json +16 -0
- package/.storybook/vite.config.mts +5 -0
- package/README.md +3 -3
- package/eslint.config.mjs +28 -0
- package/ng-package.json +9 -0
- package/package.json +18 -27
- package/project.json +81 -0
- package/src/components/alert/alert.mdx +46 -0
- package/src/components/alert/alert.spec.ts +33 -0
- package/src/components/alert/alert.stories.ts +138 -0
- package/src/components/alert/alert.ts +46 -0
- package/src/components/alert/index.ts +1 -0
- package/src/components/button/button.mdx +40 -0
- package/src/components/button/button.spec.ts +86 -0
- package/src/components/button/button.stories.ts +123 -0
- package/src/components/button/button.ts +60 -0
- package/src/components/button/index.ts +1 -0
- package/src/components/card/card-block.ts +10 -0
- package/src/components/card/card.mdx +100 -0
- package/src/components/card/card.spec.ts +70 -0
- package/src/components/card/card.stories.ts +101 -0
- package/src/components/card/card.ts +44 -0
- package/src/components/card/index.ts +2 -0
- package/src/components/checkbox/README.md +13 -0
- package/src/components/checkbox/checkbox.mdx +50 -0
- package/src/components/checkbox/checkbox.spec.ts +21 -0
- package/src/components/checkbox/checkbox.stories.ts +182 -0
- package/src/components/checkbox/index.ts +0 -0
- package/src/components/colors.ts +36 -0
- package/src/components/common-inputs.ts +30 -0
- package/src/components/details/controlled-details.ts +63 -0
- package/src/components/details/details-content.ts +7 -0
- package/src/components/details/details-summary.ts +7 -0
- package/src/components/details/details.mdx +89 -0
- package/src/components/details/details.spec.ts +56 -0
- package/src/components/details/details.stories.ts +129 -0
- package/src/components/details/details.ts +69 -0
- package/src/components/details/index.ts +3 -0
- package/src/components/field/field-counter.ts +56 -0
- package/src/components/field/field-description.ts +10 -0
- package/src/components/field/field-error.ts +13 -0
- package/src/components/field/field-observer.ts +121 -0
- package/src/components/field/field-state.ts +21 -0
- package/src/components/field/field.mdx +40 -0
- package/src/components/field/field.spec.ts +131 -0
- package/src/components/field/field.stories.ts +98 -0
- package/src/components/field/field.ts +70 -0
- package/src/components/field/index.ts +3 -0
- package/src/components/fieldset/fieldset-description.ts +8 -0
- package/src/components/fieldset/fieldset-legend.ts +11 -0
- package/src/components/fieldset/fieldset.spec.ts +80 -0
- package/src/components/fieldset/fieldset.ts +11 -0
- package/src/components/fieldset/index.ts +3 -0
- package/src/components/input/index.ts +1 -0
- package/src/components/input/input.mdx +11 -0
- package/src/components/input/input.spec.ts +25 -0
- package/src/components/input/input.stories.ts +72 -0
- package/src/components/input/input.ts +67 -0
- package/src/components/label/index.ts +1 -0
- package/src/components/label/label.ts +17 -0
- package/src/components/paragraph/index.ts +1 -0
- package/src/components/paragraph/paragraph.ts +10 -0
- package/src/components/popover/controlled-popover.ts +62 -0
- package/src/components/popover/index.ts +1 -0
- package/src/components/popover/popover.mdx +81 -0
- package/src/components/popover/popover.spec.ts +143 -0
- package/src/components/popover/popover.stories.ts +63 -0
- package/src/components/popover/popover.ts +186 -0
- package/src/components/radio/radio.mdx +117 -0
- package/src/components/radio/radio.stories.ts +226 -0
- package/src/components/search/index.ts +4 -0
- package/src/components/search/search-button.ts +35 -0
- package/src/components/search/search-clear.ts +57 -0
- package/src/components/search/search-input.ts +18 -0
- package/src/components/search/search.mdx +56 -0
- package/src/components/search/search.spec.ts +48 -0
- package/src/components/search/search.stories.ts +205 -0
- package/src/components/search/search.ts +50 -0
- package/src/components/spinner/index.ts +1 -0
- package/src/components/spinner/spinner.mdx +24 -0
- package/src/components/spinner/spinner.spec.ts +13 -0
- package/src/components/spinner/spinner.stories.ts +54 -0
- package/src/components/spinner/spinner.ts +62 -0
- package/src/components/switch/switch.mdx +82 -0
- package/src/components/switch/switch.stories.ts +94 -0
- package/src/components/textarea/textarea.mdx +14 -0
- package/src/components/textarea/textarea.stories.ts +52 -0
- package/src/components/validation-message/index.ts +1 -0
- package/src/components/validation-message/validation-message.ts +11 -0
- package/src/index.ts +14 -0
- package/src/test-setup.ts +12 -0
- package/src/utils/log-if-devmode.ts +13 -0
- package/src/utils/random-id.ts +3 -0
- package/tsconfig.json +34 -0
- package/tsconfig.lib.json +28 -0
- package/tsconfig.lib.prod.json +9 -0
- package/tsconfig.spec.json +30 -0
- package/vite.config.mts +35 -0
- package/dist/README.md +0 -55
- package/dist/fesm2022/ks-digital-designsystem-angular.mjs +0 -1068
- package/dist/fesm2022/ks-digital-designsystem-angular.mjs.map +0 -1
- package/dist/index.d.ts +0 -315
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { Meta, Primary, Controls, Story } from '@storybook/addon-docs/blocks'
|
|
2
|
+
|
|
3
|
+
import * as ButtonStories from './button.stories'
|
|
4
|
+
|
|
5
|
+
<Meta of={ButtonStories} />
|
|
6
|
+
|
|
7
|
+
# Button
|
|
8
|
+
|
|
9
|
+
Knappar lèt brukarane utføre handlingar.
|
|
10
|
+
|
|
11
|
+
<Primary />
|
|
12
|
+
<Controls />
|
|
13
|
+
|
|
14
|
+
## Ikoner
|
|
15
|
+
|
|
16
|
+
- [Unngå å bruke ikonfonter](https://www.irigoyen.dev/blog/2021/02/17/stop-using-icon-fonts/).
|
|
17
|
+
- For Angular kan du bruke [Ng Icons](https://ng-icons.github.io/ng-icons), et bibliotek som inneholder ikoner fra Phosphor Icons og Material Icons – de to ikonsettene vi offisielt støtter i våre løsninger.
|
|
18
|
+
|
|
19
|
+
### Egne ikoner
|
|
20
|
+
|
|
21
|
+
Du kan også bruke SVG-markup direkte, f.eks. hvis du har egne ikoner.
|
|
22
|
+
|
|
23
|
+
```tsx
|
|
24
|
+
<button ksd-button>
|
|
25
|
+
<svg
|
|
26
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
27
|
+
width="1em"
|
|
28
|
+
height="1em"
|
|
29
|
+
viewBox="0 0 24 24"
|
|
30
|
+
fill="none"
|
|
31
|
+
stroke="currentColor"
|
|
32
|
+
aria-hidden="true"
|
|
33
|
+
>
|
|
34
|
+
<path d="M3 11.5L12 4l9 7.5" />
|
|
35
|
+
<path d="M5 10.5v9.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-9.5" />
|
|
36
|
+
<path d="M10 21v-5a2 2 0 0 1 4 0v5" />
|
|
37
|
+
</svg>
|
|
38
|
+
Eget SVG-ikon
|
|
39
|
+
</button>
|
|
40
|
+
```
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { render, screen } from '@testing-library/angular'
|
|
2
|
+
import userEvent from '@testing-library/user-event'
|
|
3
|
+
import { vi } from 'vitest'
|
|
4
|
+
import { Button } from './button'
|
|
5
|
+
|
|
6
|
+
it('should render as aria-disabled when aria-disabled is true regardless of variant', async () => {
|
|
7
|
+
await render(
|
|
8
|
+
`
|
|
9
|
+
<button ksd-button aria-disabled="true">My button</button>
|
|
10
|
+
`,
|
|
11
|
+
{ imports: [Button] },
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const button = screen.getByRole('button')
|
|
15
|
+
expect(button).toHaveAttribute('aria-disabled')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('should render as disabled when disabled is true regardless of variant', async () => {
|
|
19
|
+
await render(
|
|
20
|
+
`
|
|
21
|
+
<button ksd-button disabled>My button</button>
|
|
22
|
+
`,
|
|
23
|
+
{ imports: [Button] },
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
const button = screen.getByRole('button')
|
|
27
|
+
expect(button).toBeDisabled()
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('should be clickable', async () => {
|
|
31
|
+
const handleClick = vi.fn()
|
|
32
|
+
|
|
33
|
+
await render(
|
|
34
|
+
`<button ksd-button (click)="handleClick()">My button</button>`,
|
|
35
|
+
{
|
|
36
|
+
imports: [Button],
|
|
37
|
+
componentProperties: { handleClick },
|
|
38
|
+
},
|
|
39
|
+
)
|
|
40
|
+
const user = userEvent.setup()
|
|
41
|
+
const button = screen.getByRole('button')
|
|
42
|
+
|
|
43
|
+
await user.click(button)
|
|
44
|
+
expect(handleClick).toHaveBeenCalledTimes(1)
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should not be clickable when disabled', async () => {
|
|
48
|
+
const handleClick = vi.fn()
|
|
49
|
+
|
|
50
|
+
await render(
|
|
51
|
+
`<button ksd-button disabled (click)="handleClick()">My button</button>`,
|
|
52
|
+
{
|
|
53
|
+
imports: [Button],
|
|
54
|
+
componentProperties: { handleClick },
|
|
55
|
+
},
|
|
56
|
+
)
|
|
57
|
+
const user = userEvent.setup()
|
|
58
|
+
const button = screen.getByRole('button')
|
|
59
|
+
|
|
60
|
+
await user.click(button)
|
|
61
|
+
expect(handleClick).not.toHaveBeenCalled()
|
|
62
|
+
})
|
|
63
|
+
|
|
64
|
+
it('should render button text', async () => {
|
|
65
|
+
await render(
|
|
66
|
+
`
|
|
67
|
+
<button ksd-button disabled>Different button text</button>
|
|
68
|
+
`,
|
|
69
|
+
{ imports: [Button] },
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
const button = screen.getByRole('button', { name: 'Different button text' })
|
|
73
|
+
expect(button).toBeInTheDocument()
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('should set aria-busy when loading', async () => {
|
|
77
|
+
await render(
|
|
78
|
+
`
|
|
79
|
+
<button ksd-button loading>My button</button>
|
|
80
|
+
`,
|
|
81
|
+
{ imports: [Button] },
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
const button = screen.getByRole('button')
|
|
85
|
+
expect(button).toHaveAttribute('aria-busy', 'true')
|
|
86
|
+
})
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { NgIcon, provideIcons } from '@ng-icons/core'
|
|
2
|
+
import { phosphorPencilLine } from '@ng-icons/phosphor-icons/regular'
|
|
3
|
+
import {
|
|
4
|
+
argsToTemplate,
|
|
5
|
+
componentWrapperDecorator,
|
|
6
|
+
moduleMetadata,
|
|
7
|
+
type Meta,
|
|
8
|
+
type StoryObj,
|
|
9
|
+
} from '@storybook/angular'
|
|
10
|
+
import { CommonArgs } from '../../../.storybook/default-args'
|
|
11
|
+
import { Button } from './button'
|
|
12
|
+
|
|
13
|
+
type ButtonArgs = CommonArgs & {
|
|
14
|
+
loading: boolean
|
|
15
|
+
disabled: boolean
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const meta: Meta<ButtonArgs> = {
|
|
19
|
+
component: Button,
|
|
20
|
+
title: 'Komponenter/Button',
|
|
21
|
+
decorators: [
|
|
22
|
+
moduleMetadata({
|
|
23
|
+
imports: [Button, NgIcon],
|
|
24
|
+
providers: [provideIcons({ phosphorPencilLine })],
|
|
25
|
+
}),
|
|
26
|
+
componentWrapperDecorator(
|
|
27
|
+
(story) =>
|
|
28
|
+
`<div style="display:flex;flex-direction:row;justify-content:center;align-items:center;flex-wrap:wrap;gap:var(--ds-size-4)">${story}</div>`,
|
|
29
|
+
),
|
|
30
|
+
],
|
|
31
|
+
}
|
|
32
|
+
export default meta
|
|
33
|
+
type Story = StoryObj<ButtonArgs>
|
|
34
|
+
|
|
35
|
+
export const Preview: Story = {
|
|
36
|
+
args: {
|
|
37
|
+
loading: false,
|
|
38
|
+
disabled: false,
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
render: (args) => ({
|
|
42
|
+
props: args,
|
|
43
|
+
template: `
|
|
44
|
+
<button ksd-button ${argsToTemplate(args)}>
|
|
45
|
+
Knapp
|
|
46
|
+
</button>
|
|
47
|
+
`,
|
|
48
|
+
}),
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const Variants: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
...Preview.args,
|
|
54
|
+
},
|
|
55
|
+
render: (args) => ({
|
|
56
|
+
props: args,
|
|
57
|
+
template: `
|
|
58
|
+
<div style="display:flex;flex-direction:row;justify-content:center;align-items:center;flex-wrap:wrap;gap:var(--ds-size-4);">
|
|
59
|
+
<button ksd-button variant="primary" ${argsToTemplate(args)}>Primary</button>
|
|
60
|
+
<button ksd-button variant="secondary" ${argsToTemplate(args)}>Secondary</button>
|
|
61
|
+
<button ksd-button variant="tertiary" ${argsToTemplate(args)}>Teritiary</button>
|
|
62
|
+
</div>
|
|
63
|
+
`,
|
|
64
|
+
}),
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const Icons: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
...Preview.args,
|
|
70
|
+
},
|
|
71
|
+
render: (args) => ({
|
|
72
|
+
props: args,
|
|
73
|
+
template: `
|
|
74
|
+
<button ksd-button ${argsToTemplate(args)}>
|
|
75
|
+
<ng-icon name="phosphorPencilLine" />
|
|
76
|
+
Rediger
|
|
77
|
+
</button>
|
|
78
|
+
|
|
79
|
+
<button icon ksd-button ${argsToTemplate(args)} aria-label="Kun ikon">
|
|
80
|
+
<ng-icon name="phosphorPencilLine" />
|
|
81
|
+
</button>
|
|
82
|
+
|
|
83
|
+
<button ksd-button ${argsToTemplate(args)}>
|
|
84
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24" fill="none" stroke="currentColor" aria-hidden="true">
|
|
85
|
+
<path d="M3 11.5L12 4l9 7.5" />
|
|
86
|
+
<path d="M5 10.5v9.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-9.5" />
|
|
87
|
+
<path d="M10 21v-5a2 2 0 0 1 4 0v5" />
|
|
88
|
+
</svg>
|
|
89
|
+
|
|
90
|
+
Eget SVG-ikon
|
|
91
|
+
</button>
|
|
92
|
+
`,
|
|
93
|
+
}),
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export const AsLink: Story = {
|
|
97
|
+
args: {
|
|
98
|
+
...Preview.args,
|
|
99
|
+
},
|
|
100
|
+
render: (args) => ({
|
|
101
|
+
props: args,
|
|
102
|
+
template: `
|
|
103
|
+
<a ksd-button target="_blank" rel="noreferrer" href="https://ksdigital.no" ${argsToTemplate(args)}>Gå til ksdigital.no</a>
|
|
104
|
+
`,
|
|
105
|
+
}),
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const Loading: Story = {
|
|
109
|
+
args: {
|
|
110
|
+
...Preview.args,
|
|
111
|
+
loading: true,
|
|
112
|
+
},
|
|
113
|
+
render: (args) => ({
|
|
114
|
+
props: args,
|
|
115
|
+
template: `
|
|
116
|
+
<div style="display:flex;flex-direction:row;justify-content:center;align-items:center;flex-wrap:wrap;gap:var(--ds-size-4);">
|
|
117
|
+
<button ksd-button variant="primary" ${argsToTemplate(args)}>Primary</button>
|
|
118
|
+
<button ksd-button variant="secondary" ${argsToTemplate(args)}>Secondary</button>
|
|
119
|
+
<button ksd-button variant="tertiary" ${argsToTemplate(args)}>Teritiary</button>
|
|
120
|
+
</div>
|
|
121
|
+
`,
|
|
122
|
+
}),
|
|
123
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { booleanAttribute, Component, input } from '@angular/core'
|
|
2
|
+
import { CommonInputs } from '../common-inputs'
|
|
3
|
+
import { Spinner } from '../spinner/spinner'
|
|
4
|
+
|
|
5
|
+
@Component({
|
|
6
|
+
selector: 'button[ksd-button], a[ksd-button]',
|
|
7
|
+
hostDirectives: [
|
|
8
|
+
{
|
|
9
|
+
directive: CommonInputs,
|
|
10
|
+
inputs: ['data-size', 'data-color'],
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
imports: [Spinner],
|
|
14
|
+
host: {
|
|
15
|
+
class: 'ds-button',
|
|
16
|
+
type: 'button',
|
|
17
|
+
'[attr.data-variant]': 'variant()',
|
|
18
|
+
'[attr.data-icon]': 'icon() || null',
|
|
19
|
+
'[attr.disabled]': 'disabled() ? true : null',
|
|
20
|
+
'[attr.aria-busy]': 'loading() ? true : null',
|
|
21
|
+
},
|
|
22
|
+
styles: `
|
|
23
|
+
/* Ensure transcluded icons are aligned properly */
|
|
24
|
+
:host ::ng-deep > * {
|
|
25
|
+
display: inline-flex;
|
|
26
|
+
}
|
|
27
|
+
`,
|
|
28
|
+
|
|
29
|
+
template: `
|
|
30
|
+
@if (loading()) {
|
|
31
|
+
<ksd-spinner aria-hidden="true" />
|
|
32
|
+
}
|
|
33
|
+
<ng-content />
|
|
34
|
+
`,
|
|
35
|
+
})
|
|
36
|
+
export class Button {
|
|
37
|
+
/**
|
|
38
|
+
* Specify which variant to use
|
|
39
|
+
* @default 'primary'
|
|
40
|
+
*/
|
|
41
|
+
readonly variant = input<'primary' | 'secondary' | 'tertiary'>('primary')
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Toggle loading state.
|
|
45
|
+
* Pass an element if you want to display a custom loader.
|
|
46
|
+
*
|
|
47
|
+
* @default false
|
|
48
|
+
*/
|
|
49
|
+
readonly loading = input(false, { transform: booleanAttribute })
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Disables element
|
|
53
|
+
*/
|
|
54
|
+
readonly disabled = input(false, { transform: booleanAttribute })
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* If this is a button with only an icon
|
|
58
|
+
*/
|
|
59
|
+
readonly icon = input(false, { transform: booleanAttribute })
|
|
60
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Button } from './button'
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Meta,
|
|
3
|
+
Primary,
|
|
4
|
+
Canvas,
|
|
5
|
+
Controls,
|
|
6
|
+
Story,
|
|
7
|
+
} from '@storybook/addon-docs/blocks'
|
|
8
|
+
|
|
9
|
+
import * as CardStories from './card.stories'
|
|
10
|
+
|
|
11
|
+
<Meta of={CardStories} />
|
|
12
|
+
|
|
13
|
+
# Card
|
|
14
|
+
|
|
15
|
+
Med Card kan vi fremheve informasjon eller oppgaver som hører sammen.
|
|
16
|
+
|
|
17
|
+
<Primary />
|
|
18
|
+
<Controls />
|
|
19
|
+
|
|
20
|
+
## Bruk
|
|
21
|
+
|
|
22
|
+
`ksd-card` brukes som direktiv sammen med riktig HTML-tag i din kontekst, f.eks `article`. Du kan bruke `ksd-card-block` for å dele inn kortet i flere seksjoner.
|
|
23
|
+
|
|
24
|
+
```html
|
|
25
|
+
<article ksd-card>
|
|
26
|
+
<h2 ksd-card-block>Tittel</h2>
|
|
27
|
+
<p ksd-card-block>Innhold</p>
|
|
28
|
+
<p ksd-card-block>Valgfri fotnote</p>
|
|
29
|
+
</article>
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Kodeeksempler
|
|
33
|
+
|
|
34
|
+
### Liste
|
|
35
|
+
|
|
36
|
+
En liste med cards i en grid-layout.
|
|
37
|
+
|
|
38
|
+
```html
|
|
39
|
+
<ul
|
|
40
|
+
style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem;"
|
|
41
|
+
>
|
|
42
|
+
<li ksd-card>
|
|
43
|
+
<h2>Item 1</h2>
|
|
44
|
+
<p>Description</p>
|
|
45
|
+
</li>
|
|
46
|
+
<li ksd-card>
|
|
47
|
+
<h2>Item 2</h2>
|
|
48
|
+
<p>Description</p>
|
|
49
|
+
</li>
|
|
50
|
+
<li ksd-card>
|
|
51
|
+
<h2>Item 3</h2>
|
|
52
|
+
<p>Description</p>
|
|
53
|
+
</li>
|
|
54
|
+
<li ksd-card>
|
|
55
|
+
<h2>Item 4</h2>
|
|
56
|
+
<p>Description</p>
|
|
57
|
+
</li>
|
|
58
|
+
</ul>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Navigasjonskort
|
|
62
|
+
|
|
63
|
+
`Card` kan brukes som navigasjonskort for å ta brukeren videre til en annen side.
|
|
64
|
+
|
|
65
|
+
#### Kort med lenke i tittel
|
|
66
|
+
|
|
67
|
+
Dersom kortet skal lenke til en annen side, kan du legge en lenke i kortets tittel. Hele kortet blir da automatisk klikkbart, men skjermlesere får en bedre brukeropplevelse enn dersom hele kortet var en lenke.
|
|
68
|
+
Dette fordi kun lenken istedenfor hele kortets innhold vil bli lest opp ved fokus.
|
|
69
|
+
|
|
70
|
+
```html
|
|
71
|
+
<article ksd-card>
|
|
72
|
+
<h2><a href="/">Whole card is clickable because of this link</a></h2>
|
|
73
|
+
</article>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### Kort som er en lenke
|
|
77
|
+
|
|
78
|
+
Hele kortet kan bli en lenke ved å bruke anchor-tag som host-element. Dette er nyttig hvis du ønsker at hele kortets innhold skal bli lest opp.
|
|
79
|
+
|
|
80
|
+
> ⚠ Vær oppmerksom på at dette vanligvis ikke er anbefalt, da det ofte er forstyrrende for skjermleserbrukere hvis kortet har mye innhold.
|
|
81
|
+
|
|
82
|
+
```html
|
|
83
|
+
<a ksd-card>
|
|
84
|
+
<h2>My heading</h2>
|
|
85
|
+
<p>My paragraph</p>
|
|
86
|
+
</a>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Retningslinjer
|
|
90
|
+
|
|
91
|
+
`Card` er en fleksibel komponent som brukes til å strukturere og presentere innhold på en tydelig og visuelt avgrenset måte. Kortet kan inneholde ulike typer innhold, som tekst, media og lenker, og brukes ofte i oversikter, navigasjon eller for å fremheve enkeltemner.
|
|
92
|
+
|
|
93
|
+
#### Passer til:
|
|
94
|
+
|
|
95
|
+
- Å gruppere innhold eller funksjonalitet som du vil skille ut fra resten av innholdet.
|
|
96
|
+
|
|
97
|
+
#### Passer ikke til:
|
|
98
|
+
|
|
99
|
+
- Å vise lange tekstblokker eller detaljerte forklaringer.
|
|
100
|
+
- Viktige meldinger som bør presenteres som varsler (bruk heller Alert).
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { fireEvent, render, screen } from '@testing-library/angular'
|
|
2
|
+
import userEvent from '@testing-library/user-event'
|
|
3
|
+
import { vi } from 'vitest'
|
|
4
|
+
import { Card } from './card'
|
|
5
|
+
|
|
6
|
+
it('should render card', async () => {
|
|
7
|
+
await render(
|
|
8
|
+
`
|
|
9
|
+
<article ksd-card>My card</article>
|
|
10
|
+
`,
|
|
11
|
+
{ imports: [Card] },
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
const card = screen.getByRole('article')
|
|
15
|
+
expect(card).toHaveClass('ds-card')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('clicking anywhere on the card triggers the inner link', async () => {
|
|
19
|
+
await render(
|
|
20
|
+
`
|
|
21
|
+
<article ksd-card>
|
|
22
|
+
<h2><a href="https://vg.no">My link</a></h2>
|
|
23
|
+
<p>My paragraph</p>
|
|
24
|
+
</article>
|
|
25
|
+
`,
|
|
26
|
+
{ imports: [Card] },
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
const user = userEvent.setup()
|
|
30
|
+
const card = screen.getByRole('article')
|
|
31
|
+
|
|
32
|
+
// Spy on the link inside the card
|
|
33
|
+
const clickSpy = vi
|
|
34
|
+
.spyOn(HTMLAnchorElement.prototype, 'click')
|
|
35
|
+
.mockImplementation(() => undefined)
|
|
36
|
+
|
|
37
|
+
await user.click(card)
|
|
38
|
+
|
|
39
|
+
// Expect that the link inside the card has been clicked
|
|
40
|
+
expect(clickSpy).toHaveBeenCalledTimes(1)
|
|
41
|
+
clickSpy.mockRestore()
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
it('opens link in new tab with noopener,noreferrer on meta/ctrl click', async () => {
|
|
45
|
+
await render(
|
|
46
|
+
`
|
|
47
|
+
<article ksd-card>
|
|
48
|
+
<h2><a href="https://vg.no">My link</a></h2>
|
|
49
|
+
<p>My paragraph</p>
|
|
50
|
+
</article>
|
|
51
|
+
`,
|
|
52
|
+
{ imports: [Card] },
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
const card = screen.getByRole('article')
|
|
56
|
+
const anchor = screen.getByRole('link') as HTMLAnchorElement
|
|
57
|
+
|
|
58
|
+
const openSpy = vi.spyOn(window, 'open').mockImplementation(() => null)
|
|
59
|
+
|
|
60
|
+
fireEvent.click(card, { ctrlKey: true })
|
|
61
|
+
|
|
62
|
+
expect(openSpy).toHaveBeenCalledTimes(1)
|
|
63
|
+
expect(openSpy).toHaveBeenCalledWith(
|
|
64
|
+
anchor.href,
|
|
65
|
+
'_blank',
|
|
66
|
+
'noopener,noreferrer',
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
openSpy.mockRestore()
|
|
70
|
+
})
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import {
|
|
2
|
+
argsToTemplate,
|
|
3
|
+
moduleMetadata,
|
|
4
|
+
type Meta,
|
|
5
|
+
type StoryObj,
|
|
6
|
+
} from '@storybook/angular'
|
|
7
|
+
import { CommonArgs, commonArgTypes } from '../../../.storybook/default-args'
|
|
8
|
+
import { Card } from './card'
|
|
9
|
+
import { CardBlock } from './card-block'
|
|
10
|
+
|
|
11
|
+
type CardArgs = CommonArgs & {
|
|
12
|
+
variant: 'default' | 'tinted'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const meta: Meta<CardArgs> = {
|
|
16
|
+
component: Card,
|
|
17
|
+
title: 'Komponenter/Card',
|
|
18
|
+
argTypes: {
|
|
19
|
+
...commonArgTypes,
|
|
20
|
+
variant: {
|
|
21
|
+
options: ['default', 'tinted'],
|
|
22
|
+
control: { type: 'radio' },
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
decorators: [
|
|
26
|
+
moduleMetadata({
|
|
27
|
+
imports: [Card, CardBlock],
|
|
28
|
+
}),
|
|
29
|
+
],
|
|
30
|
+
}
|
|
31
|
+
export default meta
|
|
32
|
+
type Story = StoryObj<CardArgs>
|
|
33
|
+
|
|
34
|
+
export const Preview: Story = {
|
|
35
|
+
render: (args) => ({
|
|
36
|
+
props: args,
|
|
37
|
+
template: `
|
|
38
|
+
<div style="max-width: 320px;">
|
|
39
|
+
<article ksd-card ${argsToTemplate(args)}>
|
|
40
|
+
<h2>Card</h2>
|
|
41
|
+
<p>Most provide as with carried business are much better more the perfected designer. Writing slightly explain desk unable at supposedly about this</p>
|
|
42
|
+
<p data-size="sm">Footer text</p>
|
|
43
|
+
</article>
|
|
44
|
+
</div>
|
|
45
|
+
`,
|
|
46
|
+
}),
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const CardWithBlocks: Story = {
|
|
50
|
+
render: (args) => ({
|
|
51
|
+
props: args,
|
|
52
|
+
template: `
|
|
53
|
+
<div style="max-width: 320px;">
|
|
54
|
+
<article ksd-card ${argsToTemplate(args)}>
|
|
55
|
+
<h2 ksd-card-block>Use blocks to section the card</h2>
|
|
56
|
+
<p ksd-card-block>Most provide as with carried business are much better more the perfected designer. Writing slightly explain desk unable at supposedly about this</p>
|
|
57
|
+
<p ksd-card-block>Valgfri fotnote</p>
|
|
58
|
+
</article>
|
|
59
|
+
</div>
|
|
60
|
+
`,
|
|
61
|
+
}),
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export const ListOfCards: Story = {
|
|
65
|
+
render: (args) => ({
|
|
66
|
+
props: args,
|
|
67
|
+
template: `
|
|
68
|
+
<ul style="display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 1rem;">
|
|
69
|
+
<li ksd-card ${argsToTemplate(args)}>
|
|
70
|
+
<h2>Item 1</h2>
|
|
71
|
+
<p>Description</p>
|
|
72
|
+
</li>
|
|
73
|
+
<li ksd-card ${argsToTemplate(args)}>
|
|
74
|
+
<h2>Item 2</h2>
|
|
75
|
+
<p>Description</p>
|
|
76
|
+
</li>
|
|
77
|
+
<li ksd-card ${argsToTemplate(args)}>
|
|
78
|
+
<h2>Item 3</h2>
|
|
79
|
+
<p>Description</p>
|
|
80
|
+
</li>
|
|
81
|
+
<li ksd-card ${argsToTemplate(args)}>
|
|
82
|
+
<h2>Item 4</h2>
|
|
83
|
+
<p>Description</p>
|
|
84
|
+
</li>
|
|
85
|
+
</ul>
|
|
86
|
+
`,
|
|
87
|
+
}),
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export const AsLink: Story = {
|
|
91
|
+
render: (args) => ({
|
|
92
|
+
props: args,
|
|
93
|
+
template: `
|
|
94
|
+
<div style="max-width: 320px;">
|
|
95
|
+
<article ksd-card ${argsToTemplate(args)}>
|
|
96
|
+
<h2><a href="/">Whole card is clickable when link is present inside heading</a></h2>
|
|
97
|
+
</article>
|
|
98
|
+
</div>
|
|
99
|
+
`,
|
|
100
|
+
}),
|
|
101
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Component, ElementRef, inject, input } from '@angular/core'
|
|
2
|
+
import { CommonInputs } from '../common-inputs'
|
|
3
|
+
|
|
4
|
+
@Component({
|
|
5
|
+
selector: '[ksd-card]',
|
|
6
|
+
template: ` <ng-content /> `,
|
|
7
|
+
hostDirectives: [
|
|
8
|
+
{
|
|
9
|
+
directive: CommonInputs,
|
|
10
|
+
inputs: ['data-size', 'data-color'],
|
|
11
|
+
},
|
|
12
|
+
],
|
|
13
|
+
host: {
|
|
14
|
+
class: 'ds-card',
|
|
15
|
+
'[attr.data-variant]': 'variant()',
|
|
16
|
+
'(click)': 'handleClick($event)',
|
|
17
|
+
},
|
|
18
|
+
})
|
|
19
|
+
export class Card {
|
|
20
|
+
/**
|
|
21
|
+
* Change the background color of the card
|
|
22
|
+
* @default 'default'
|
|
23
|
+
*/
|
|
24
|
+
public variant = input<'tinted' | 'default'>('default')
|
|
25
|
+
private elementRef = inject(ElementRef)
|
|
26
|
+
|
|
27
|
+
private projectedLink() {
|
|
28
|
+
const el = this.elementRef.nativeElement
|
|
29
|
+
return el?.querySelector(
|
|
30
|
+
'h1 a, h2 a, h3 a, h4 a, h5 a, h6 a',
|
|
31
|
+
) as HTMLAnchorElement | null
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
protected handleClick = (event: MouseEvent) => {
|
|
35
|
+
const link = this.projectedLink()
|
|
36
|
+
if (!link) return
|
|
37
|
+
|
|
38
|
+
if (event.metaKey || event.ctrlKey) {
|
|
39
|
+
window.open(link.href, '_blank', 'noopener,noreferrer')
|
|
40
|
+
} else {
|
|
41
|
+
link.click()
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|