@fellesdatakatalog/ui 1.0.15 → 1.0.16
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/package.json +11 -2
- package/src/components/app-bar/app-bar.stories.tsx +22 -0
- package/src/components/app-bar/index.tsx +12 -0
- package/src/components/app-bar/styles.module.scss +7 -0
- package/src/components/checkbox-group/checkbox-group.stories.tsx +143 -0
- package/src/components/checkbox-group/index.tsx +105 -0
- package/src/components/copy-button/copy-button.stories.tsx +28 -0
- package/src/components/copy-button/index.tsx +54 -0
- package/src/components/core/ds-overrides.scss +105 -0
- package/src/components/core/mixins.scss +143 -0
- package/src/components/core/reset.css +131 -0
- package/src/components/core/tokens.scss +26 -0
- package/src/components/helptext/helptext.module.scss +46 -0
- package/src/components/helptext/helptext.stories.tsx +42 -0
- package/src/components/helptext/index.tsx +37 -0
- package/src/components/hstack/hstack.module.scss +5 -0
- package/src/components/hstack/hstack.stories.tsx +38 -0
- package/src/components/hstack/index.tsx +17 -0
- package/src/components/org-logo/index.tsx +27 -0
- package/src/components/org-logo/org-logo.stories.tsx +35 -0
- package/src/components/org-logo/styles.module.scss +20 -0
- package/src/components/radio-group/index.tsx +101 -0
- package/src/components/radio-group/radio-group.stories.tsx +141 -0
- package/src/components/tag-list/index.tsx +39 -0
- package/src/components/tag-list/styles.module.scss +8 -0
- package/src/components/tag-list/tag-list.stories.tsx +107 -0
- package/src/components/vstack/index.tsx +16 -0
- package/src/components/vstack/vstack.module.scss +5 -0
- package/src/components/vstack/vstack.stories.tsx +37 -0
- package/src/index.ts +11 -0
- package/src/styles/storybook.scss +34 -0
- package/src/types/scss.d.ts +9 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/* http://meyerweb.com/eric/tools/css/reset/
|
|
2
|
+
v2.0 | 20110126
|
|
3
|
+
License: none (public domain)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/* This is file is used to neutrilize default css properties defined by the browsers (or other parties) */
|
|
7
|
+
@layer reset {
|
|
8
|
+
html,
|
|
9
|
+
body,
|
|
10
|
+
div,
|
|
11
|
+
span,
|
|
12
|
+
applet,
|
|
13
|
+
object,
|
|
14
|
+
iframe,
|
|
15
|
+
h1,
|
|
16
|
+
h2,
|
|
17
|
+
h3,
|
|
18
|
+
h4,
|
|
19
|
+
h5,
|
|
20
|
+
h6,
|
|
21
|
+
p,
|
|
22
|
+
blockquote,
|
|
23
|
+
pre,
|
|
24
|
+
a,
|
|
25
|
+
abbr,
|
|
26
|
+
acronym,
|
|
27
|
+
address,
|
|
28
|
+
big,
|
|
29
|
+
cite,
|
|
30
|
+
code,
|
|
31
|
+
del,
|
|
32
|
+
dfn,
|
|
33
|
+
em,
|
|
34
|
+
img,
|
|
35
|
+
ins,
|
|
36
|
+
kbd,
|
|
37
|
+
q,
|
|
38
|
+
s,
|
|
39
|
+
samp,
|
|
40
|
+
small,
|
|
41
|
+
strike,
|
|
42
|
+
strong,
|
|
43
|
+
sub,
|
|
44
|
+
sup,
|
|
45
|
+
tt,
|
|
46
|
+
var,
|
|
47
|
+
b,
|
|
48
|
+
u,
|
|
49
|
+
i,
|
|
50
|
+
center,
|
|
51
|
+
dl,
|
|
52
|
+
dt,
|
|
53
|
+
dd,
|
|
54
|
+
ol,
|
|
55
|
+
ul,
|
|
56
|
+
li,
|
|
57
|
+
fieldset,
|
|
58
|
+
form,
|
|
59
|
+
label,
|
|
60
|
+
legend,
|
|
61
|
+
table,
|
|
62
|
+
caption,
|
|
63
|
+
tbody,
|
|
64
|
+
tfoot,
|
|
65
|
+
thead,
|
|
66
|
+
tr,
|
|
67
|
+
th,
|
|
68
|
+
td,
|
|
69
|
+
article,
|
|
70
|
+
aside,
|
|
71
|
+
canvas,
|
|
72
|
+
details,
|
|
73
|
+
embed,
|
|
74
|
+
figure,
|
|
75
|
+
figcaption,
|
|
76
|
+
footer,
|
|
77
|
+
header,
|
|
78
|
+
hgroup,
|
|
79
|
+
menu,
|
|
80
|
+
nav,
|
|
81
|
+
output,
|
|
82
|
+
ruby,
|
|
83
|
+
section,
|
|
84
|
+
summary,
|
|
85
|
+
time,
|
|
86
|
+
mark,
|
|
87
|
+
audio,
|
|
88
|
+
video {
|
|
89
|
+
margin: 0;
|
|
90
|
+
padding: 0;
|
|
91
|
+
border: 0;
|
|
92
|
+
font-size: inherit;
|
|
93
|
+
vertical-align: baseline;
|
|
94
|
+
}
|
|
95
|
+
/* HTML5 display-role reset for older browsers */
|
|
96
|
+
article,
|
|
97
|
+
aside,
|
|
98
|
+
details,
|
|
99
|
+
figcaption,
|
|
100
|
+
figure,
|
|
101
|
+
footer,
|
|
102
|
+
header,
|
|
103
|
+
hgroup,
|
|
104
|
+
menu,
|
|
105
|
+
nav,
|
|
106
|
+
section {
|
|
107
|
+
display: block;
|
|
108
|
+
}
|
|
109
|
+
body {
|
|
110
|
+
line-height: 1.5rem;
|
|
111
|
+
}
|
|
112
|
+
ol,
|
|
113
|
+
ul {
|
|
114
|
+
list-style: none;
|
|
115
|
+
}
|
|
116
|
+
blockquote,
|
|
117
|
+
q {
|
|
118
|
+
quotes: none;
|
|
119
|
+
}
|
|
120
|
+
blockquote:before,
|
|
121
|
+
blockquote:after,
|
|
122
|
+
q:before,
|
|
123
|
+
q:after {
|
|
124
|
+
content: '';
|
|
125
|
+
content: none;
|
|
126
|
+
}
|
|
127
|
+
table {
|
|
128
|
+
border-collapse: collapse;
|
|
129
|
+
border-spacing: 0;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
@import './reset.css';
|
|
2
|
+
@import '@digdir/designsystemet-css';
|
|
3
|
+
@import '@digdir/designsystemet-theme';
|
|
4
|
+
@import './ds-overrides.scss';
|
|
5
|
+
|
|
6
|
+
:root {
|
|
7
|
+
// Colors
|
|
8
|
+
--fdk-color-navy: #1e2b3c;
|
|
9
|
+
--fdk-color-navy-dark: color-mix(in srgb, var(--fdk-color-navy), #000 60%);
|
|
10
|
+
--fdk-color-blue-lightest: #ddeaf6;
|
|
11
|
+
--fdk-color-blue-light: #1e98f5;
|
|
12
|
+
--fdk-color-blue: #0062ba;
|
|
13
|
+
--fdk-color-blue-dark: #004f96;
|
|
14
|
+
--fdk-color-cyan: #8eaacb;
|
|
15
|
+
--fdk-color-orange: var(--ds-color-brand2-base-default);
|
|
16
|
+
--fdk-color-red: #b81a1a;
|
|
17
|
+
|
|
18
|
+
// Other variables
|
|
19
|
+
--fdk-color-interactive: var(--fdk-color-blue);
|
|
20
|
+
--fdk-color-interactive-negative: var(--fdk-color-cyan);
|
|
21
|
+
--fdk-color-text: var(--fdk-color-navy);
|
|
22
|
+
--fdk-color-text-negative: white;
|
|
23
|
+
--fdk-color-link: var(--fdk-color-interactive);
|
|
24
|
+
--fdk-color-link-negative: var(--fdk-color-interactive-negative);
|
|
25
|
+
--custom-helptext-size: 1.25rem;
|
|
26
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
.helptext {
|
|
2
|
+
--dsc-helptext-icon-size: 65%;
|
|
3
|
+
--dsc-helptext-icon-url: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 8 14'%3E%3Cpath fill='%23000' fill-rule='evenodd' d='M4 11a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM4 0c2.2 0 4 1.66 4 3.7 0 .98-.42 1.9-1.17 2.6l-.6.54-.29.29c-.48.46-.87.93-.94 1.41V9.5H3v-.81c0-1.26.84-2.22 1.68-3L5.42 5C5.8 4.65 6 4.2 6 3.7 6 2.66 5.1 1.83 4 1.83c-1.06 0-1.92.75-2 1.7v.15H0C0 1.66 1.8 0 4 0Z' clip-rule='evenodd'/%3E%3C/svg%3E");
|
|
4
|
+
--dsc-helptext-size: var(--custom-helptext-size, var(--ds-size-7));
|
|
5
|
+
|
|
6
|
+
border-radius: var(--ds-border-radius-full);
|
|
7
|
+
border: max(1px, calc(var(--ds-size-1) / 2)) solid; /* Allow border-width to grow with font-size */
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
height: var(--dsc-helptext-size);
|
|
10
|
+
min-height: 0;
|
|
11
|
+
min-width: 0;
|
|
12
|
+
padding: 0;
|
|
13
|
+
position: relative;
|
|
14
|
+
width: var(--dsc-helptext-size);
|
|
15
|
+
|
|
16
|
+
@media (forced-colors: active) {
|
|
17
|
+
color: ButtonText;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
&::before {
|
|
21
|
+
content: '';
|
|
22
|
+
border-radius: inherit;
|
|
23
|
+
background: currentcolor;
|
|
24
|
+
mask-composite: exclude;
|
|
25
|
+
mask-image: var(--dsc-helptext-icon-url);
|
|
26
|
+
mask-position: center;
|
|
27
|
+
mask-repeat: no-repeat;
|
|
28
|
+
mask-size: var(--dsc-helptext-icon-size) var(--dsc-helptext-icon-size), cover;
|
|
29
|
+
scale: 1.1; /* Hide tiny half pixel rendeing bug */
|
|
30
|
+
width: 100%;
|
|
31
|
+
height: 100%;
|
|
32
|
+
|
|
33
|
+
@media (forced-colors: active) {
|
|
34
|
+
background: ButtonText;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
&:has(+ :popover-open)::before {
|
|
39
|
+
mask-image: var(--dsc-helptext-icon-url), linear-gradient(currentcolor, currentcolor); /* Cut icon out of currentcolor surface */
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
@media print {
|
|
43
|
+
display: none;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
|
|
3
|
+
import { HelpText } from '.';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof HelpText> = {
|
|
6
|
+
component: HelpText,
|
|
7
|
+
title: 'HelpText',
|
|
8
|
+
parameters: {
|
|
9
|
+
docs: {
|
|
10
|
+
description: {
|
|
11
|
+
component: 'A help text component that displays additional information in a popover when clicked.',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
argTypes: {
|
|
16
|
+
placement: {
|
|
17
|
+
control: 'select',
|
|
18
|
+
options: ['right', 'bottom', 'left', 'top'],
|
|
19
|
+
description: 'Placement of the popover',
|
|
20
|
+
},
|
|
21
|
+
'aria-label': {
|
|
22
|
+
control: 'text',
|
|
23
|
+
description: 'Required descriptive label for screen readers',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default meta;
|
|
29
|
+
type Story = StoryObj<typeof HelpText>;
|
|
30
|
+
|
|
31
|
+
export const Default: Story = {
|
|
32
|
+
args: {
|
|
33
|
+
'aria-label': 'Help information',
|
|
34
|
+
placement: 'right',
|
|
35
|
+
children: 'This is helpful information that appears when you click the help icon.',
|
|
36
|
+
},
|
|
37
|
+
render: (args) => (
|
|
38
|
+
<div style={{ padding: '2rem', display: 'flex', alignItems: 'center', gap: '1rem' }}>
|
|
39
|
+
<HelpText {...args} />
|
|
40
|
+
</div>
|
|
41
|
+
),
|
|
42
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { Popover } from '@digdir/designsystemet-react';
|
|
3
|
+
import styles from './helptext.module.scss';
|
|
4
|
+
|
|
5
|
+
export type HelpTextProps = {
|
|
6
|
+
/**
|
|
7
|
+
* Required descriptive label for screen readers.
|
|
8
|
+
**/
|
|
9
|
+
"aria-label": string;
|
|
10
|
+
/**
|
|
11
|
+
* Placement of the Popover.
|
|
12
|
+
* @default 'right'
|
|
13
|
+
*/
|
|
14
|
+
placement?: "right" | "bottom" | "left" | "top";
|
|
15
|
+
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "color">;
|
|
16
|
+
|
|
17
|
+
export const HelpText = React.forwardRef<HTMLButtonElement, HelpTextProps>(
|
|
18
|
+
function HelpText(
|
|
19
|
+
{ placement = "right", children, ...rest },
|
|
20
|
+
ref
|
|
21
|
+
) {
|
|
22
|
+
return (
|
|
23
|
+
<Popover.TriggerContext>
|
|
24
|
+
<Popover.Trigger
|
|
25
|
+
className={styles.helptext}
|
|
26
|
+
ref={ref}
|
|
27
|
+
variant="tertiary"
|
|
28
|
+
data-color="info"
|
|
29
|
+
{...rest}
|
|
30
|
+
/>
|
|
31
|
+
<Popover placement={placement} data-color="info">
|
|
32
|
+
{children}
|
|
33
|
+
</Popover>
|
|
34
|
+
</Popover.TriggerContext>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
|
|
3
|
+
import HStack from '.';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof HStack> = {
|
|
6
|
+
component: HStack,
|
|
7
|
+
title: 'HStack',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof HStack>;
|
|
12
|
+
|
|
13
|
+
const Item = ({ children }: { children: React.ReactNode }) => (
|
|
14
|
+
<span
|
|
15
|
+
style={{
|
|
16
|
+
display: 'inline-block',
|
|
17
|
+
padding: '0.25rem 0.5rem',
|
|
18
|
+
background: '#EEF0F2',
|
|
19
|
+
borderRadius: 4,
|
|
20
|
+
}}
|
|
21
|
+
>
|
|
22
|
+
{children}
|
|
23
|
+
</span>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
export const Primary: Story = {
|
|
27
|
+
render: () => (
|
|
28
|
+
<div style={{ padding: '1rem' }}>
|
|
29
|
+
<HStack>
|
|
30
|
+
<Item>One</Item>
|
|
31
|
+
<Item>Two</Item>
|
|
32
|
+
<Item>Three</Item>
|
|
33
|
+
</HStack>
|
|
34
|
+
</div>
|
|
35
|
+
),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import cn from 'classnames';
|
|
3
|
+
|
|
4
|
+
import styles from './hstack.module.scss';
|
|
5
|
+
|
|
6
|
+
const HStack = ({ children, className, ...props }: React.HTMLAttributes<HTMLDivElement>) => {
|
|
7
|
+
return (
|
|
8
|
+
<div
|
|
9
|
+
className={cn(styles.wrapper, className)}
|
|
10
|
+
{...props}
|
|
11
|
+
>
|
|
12
|
+
{children}
|
|
13
|
+
</div>
|
|
14
|
+
);
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default HStack;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import cn from 'classnames';
|
|
3
|
+
import { Buildings3Icon } from '@navikt/aksel-icons';
|
|
4
|
+
import styles from './styles.module.scss';
|
|
5
|
+
|
|
6
|
+
export type OrgLogoProps = {
|
|
7
|
+
orgLogoSrc?: string | null;
|
|
8
|
+
className?: string;
|
|
9
|
+
} & React.HTMLAttributes<HTMLDivElement>;
|
|
10
|
+
|
|
11
|
+
const OrgLogo = ({ className, orgLogoSrc, ...props }: OrgLogoProps) => {
|
|
12
|
+
return (
|
|
13
|
+
<div className={cn(styles.orgLogo, className)} {...props}>
|
|
14
|
+
{orgLogoSrc ? (
|
|
15
|
+
<img
|
|
16
|
+
aria-hidden
|
|
17
|
+
src={orgLogoSrc}
|
|
18
|
+
alt=''
|
|
19
|
+
/>
|
|
20
|
+
) : (
|
|
21
|
+
<Buildings3Icon aria-hidden />
|
|
22
|
+
)}
|
|
23
|
+
</div>
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export { OrgLogo };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
|
|
3
|
+
import { OrgLogo } from '.';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof OrgLogo> = {
|
|
6
|
+
component: OrgLogo,
|
|
7
|
+
title: 'OrgLogo',
|
|
8
|
+
parameters: {
|
|
9
|
+
layout: 'centered',
|
|
10
|
+
},
|
|
11
|
+
argTypes: {
|
|
12
|
+
orgLogoSrc: {
|
|
13
|
+
control: 'text',
|
|
14
|
+
description: 'URL of the organization logo image',
|
|
15
|
+
},
|
|
16
|
+
className: {
|
|
17
|
+
control: 'text',
|
|
18
|
+
description: 'Additional CSS classes',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export default meta;
|
|
24
|
+
type Story = StoryObj<typeof OrgLogo>;
|
|
25
|
+
|
|
26
|
+
export const Default: Story = {
|
|
27
|
+
args: {
|
|
28
|
+
orgLogoSrc: 'https://orglogo.digdir.no/api/emblem/svg/991825827',
|
|
29
|
+
},
|
|
30
|
+
render: (args) => (
|
|
31
|
+
<div style={{ maxWidth: '2rem', padding: '1rem' }}>
|
|
32
|
+
<OrgLogo {...args} />
|
|
33
|
+
</div>
|
|
34
|
+
),
|
|
35
|
+
};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
.orgLogo {
|
|
2
|
+
display: flex;
|
|
3
|
+
width: 1em;
|
|
4
|
+
height: 1em;
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
align-items: center;
|
|
7
|
+
justify-content: center;
|
|
8
|
+
position: relative;
|
|
9
|
+
|
|
10
|
+
svg,
|
|
11
|
+
img {
|
|
12
|
+
width: 100%;
|
|
13
|
+
height: 100%;
|
|
14
|
+
object-fit: contain;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
svg {
|
|
18
|
+
color: var(--color-text);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import React, { type ReactNode } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Fieldset,
|
|
4
|
+
Radio,
|
|
5
|
+
ValidationMessage,
|
|
6
|
+
type UseRadioGroupProps,
|
|
7
|
+
useRadioGroup,
|
|
8
|
+
} from '@digdir/designsystemet-react';
|
|
9
|
+
|
|
10
|
+
export interface RadioGroupProps extends Omit<UseRadioGroupProps, 'value' | 'onChange'> {
|
|
11
|
+
/**
|
|
12
|
+
* The legend text for the fieldset
|
|
13
|
+
*/
|
|
14
|
+
legend?: ReactNode;
|
|
15
|
+
/**
|
|
16
|
+
* Optional description text for the fieldset
|
|
17
|
+
*/
|
|
18
|
+
description?: ReactNode;
|
|
19
|
+
/**
|
|
20
|
+
* Array of radio options
|
|
21
|
+
*/
|
|
22
|
+
options: Array<{
|
|
23
|
+
value: string;
|
|
24
|
+
label: string;
|
|
25
|
+
description?: string;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
/**
|
|
29
|
+
* Controlled value - the selected radio value
|
|
30
|
+
*/
|
|
31
|
+
value?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Callback when the selected value changes
|
|
34
|
+
*/
|
|
35
|
+
onChange?: (value: string) => void;
|
|
36
|
+
/**
|
|
37
|
+
* Optional className for the fieldset
|
|
38
|
+
*/
|
|
39
|
+
className?: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* RadioGroup component that wraps multiple Radio components in a Fieldset.
|
|
44
|
+
* Handles state management, onChange, and value automatically.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* <RadioGroup
|
|
48
|
+
* legend="Hvilken iskremsmak er best?"
|
|
49
|
+
* description="Velg din favorittsmak"
|
|
50
|
+
* options={[
|
|
51
|
+
* { value: 'vanilje', label: 'Vanilje' },
|
|
52
|
+
* { value: 'jordbær', label: 'Jordbær' },
|
|
53
|
+
* { value: 'sjokolade', label: 'Sjokolade' },
|
|
54
|
+
* ]}
|
|
55
|
+
* value={selectedValue}
|
|
56
|
+
* onChange={(value) => setSelectedValue(value)}
|
|
57
|
+
* />
|
|
58
|
+
*/
|
|
59
|
+
export const RadioGroup = ({
|
|
60
|
+
legend,
|
|
61
|
+
description,
|
|
62
|
+
options,
|
|
63
|
+
value,
|
|
64
|
+
onChange,
|
|
65
|
+
error,
|
|
66
|
+
disabled,
|
|
67
|
+
readOnly,
|
|
68
|
+
required,
|
|
69
|
+
name,
|
|
70
|
+
className,
|
|
71
|
+
}: RadioGroupProps) => {
|
|
72
|
+
const { getRadioProps, validationMessageProps } = useRadioGroup({
|
|
73
|
+
value,
|
|
74
|
+
onChange: onChange ? (nextValue) => onChange(nextValue) : undefined,
|
|
75
|
+
error,
|
|
76
|
+
disabled,
|
|
77
|
+
readOnly,
|
|
78
|
+
required,
|
|
79
|
+
name,
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<Fieldset className={className}>
|
|
84
|
+
{legend && <Fieldset.Legend>{legend}</Fieldset.Legend>}
|
|
85
|
+
{description && <Fieldset.Description>{description}</Fieldset.Description>}
|
|
86
|
+
{options.map((option) => (
|
|
87
|
+
<Radio
|
|
88
|
+
key={option.value}
|
|
89
|
+
label={option.label}
|
|
90
|
+
description={option.description}
|
|
91
|
+
disabled={option.disabled}
|
|
92
|
+
{...getRadioProps(option.value)}
|
|
93
|
+
/>
|
|
94
|
+
))}
|
|
95
|
+
{error && <ValidationMessage {...validationMessageProps} />}
|
|
96
|
+
</Fieldset>
|
|
97
|
+
);
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
export default RadioGroup;
|
|
101
|
+
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import RadioGroup from '.';
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof RadioGroup> = {
|
|
6
|
+
component: RadioGroup,
|
|
7
|
+
title: 'RadioGroup',
|
|
8
|
+
argTypes: {
|
|
9
|
+
legend: {
|
|
10
|
+
control: 'text',
|
|
11
|
+
description: 'The legend text for the fieldset',
|
|
12
|
+
},
|
|
13
|
+
description: {
|
|
14
|
+
control: 'text',
|
|
15
|
+
description: 'Optional description text',
|
|
16
|
+
},
|
|
17
|
+
value: {
|
|
18
|
+
control: 'text',
|
|
19
|
+
description: 'Controlled value - the selected radio value',
|
|
20
|
+
},
|
|
21
|
+
error: {
|
|
22
|
+
control: 'text',
|
|
23
|
+
description: 'Error message to display',
|
|
24
|
+
},
|
|
25
|
+
disabled: {
|
|
26
|
+
control: 'boolean',
|
|
27
|
+
description: 'Disable all radios',
|
|
28
|
+
},
|
|
29
|
+
readOnly: {
|
|
30
|
+
control: 'boolean',
|
|
31
|
+
description: 'Make all radios read-only',
|
|
32
|
+
},
|
|
33
|
+
required: {
|
|
34
|
+
control: 'boolean',
|
|
35
|
+
description: 'Make all radios required',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export default meta;
|
|
41
|
+
type Story = StoryObj<typeof RadioGroup>;
|
|
42
|
+
|
|
43
|
+
const defaultOptions = [
|
|
44
|
+
{ value: 'vanilje', label: 'Vanilje' },
|
|
45
|
+
{ value: 'jordbær', label: 'Jordbær' },
|
|
46
|
+
{ value: 'sjokolade', label: 'Sjokolade' },
|
|
47
|
+
{ value: 'spiser-ikke-is', label: 'Jeg spiser ikke iskrem' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
export const Primary: Story = {
|
|
51
|
+
render: () => {
|
|
52
|
+
const [value, setValue] = useState('vanilje');
|
|
53
|
+
return (
|
|
54
|
+
<RadioGroup
|
|
55
|
+
legend="Hvilken iskremsmak er best?"
|
|
56
|
+
description="Velg din favorittsmak blant alternativene."
|
|
57
|
+
options={defaultOptions}
|
|
58
|
+
value={value}
|
|
59
|
+
onChange={setValue}
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
export const Uncontrolled: Story = {
|
|
66
|
+
render: () => (
|
|
67
|
+
<RadioGroup
|
|
68
|
+
legend="Hvilken iskremsmak er best?"
|
|
69
|
+
description="Velg din favorittsmak blant alternativene."
|
|
70
|
+
options={defaultOptions}
|
|
71
|
+
/>
|
|
72
|
+
),
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
export const WithError: Story = {
|
|
76
|
+
render: () => (
|
|
77
|
+
<RadioGroup
|
|
78
|
+
legend="Hvilken bydel bor du i?"
|
|
79
|
+
description="Trondheim er delt inn i fire bydeler"
|
|
80
|
+
options={[
|
|
81
|
+
{ value: 'ostbyen', label: 'Østbyen' },
|
|
82
|
+
{ value: 'lerkendal', label: 'Lerkendal' },
|
|
83
|
+
{ value: 'heimdal', label: 'Heimdal' },
|
|
84
|
+
{ value: 'midtbyen', label: 'Midtbyen' },
|
|
85
|
+
]}
|
|
86
|
+
error="Du må velge en bydel før du kan fortsette."
|
|
87
|
+
required
|
|
88
|
+
/>
|
|
89
|
+
),
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
export const Disabled: Story = {
|
|
93
|
+
render: () => (
|
|
94
|
+
<RadioGroup
|
|
95
|
+
legend="Hvilken iskremsmak er best?"
|
|
96
|
+
options={defaultOptions}
|
|
97
|
+
disabled
|
|
98
|
+
/>
|
|
99
|
+
),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export const ReadOnly: Story = {
|
|
103
|
+
render: () => (
|
|
104
|
+
<RadioGroup
|
|
105
|
+
legend="Hvilken bydel bor du i?"
|
|
106
|
+
options={[
|
|
107
|
+
{ value: 'ostbyen', label: 'Østbyen' },
|
|
108
|
+
{ value: 'lerkendal', label: 'Lerkendal' },
|
|
109
|
+
{ value: 'heimdal', label: 'Heimdal', disabled: true },
|
|
110
|
+
{ value: 'midtbyen', label: 'Midtbyen' },
|
|
111
|
+
]}
|
|
112
|
+
value="heimdal"
|
|
113
|
+
readOnly
|
|
114
|
+
/>
|
|
115
|
+
),
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
export const WithDescriptions: Story = {
|
|
119
|
+
render: () => {
|
|
120
|
+
const [value, setValue] = useState('');
|
|
121
|
+
return (
|
|
122
|
+
<RadioGroup
|
|
123
|
+
legend="Velg pizza"
|
|
124
|
+
description="Alle pizzaene er laget på våre egne nybakte bunner"
|
|
125
|
+
options={[
|
|
126
|
+
{ value: 'ost', label: 'Bare ost' },
|
|
127
|
+
{
|
|
128
|
+
value: 'dobbeldekker',
|
|
129
|
+
label: 'Dobbeldekker',
|
|
130
|
+
description: 'Chorizo spesial med kokkens luksuskylling',
|
|
131
|
+
},
|
|
132
|
+
{ value: 'flammen', label: 'Flammen' },
|
|
133
|
+
{ value: 'snadder', label: 'Snadder' },
|
|
134
|
+
]}
|
|
135
|
+
value={value}
|
|
136
|
+
onChange={setValue}
|
|
137
|
+
/>
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
|