fds-vue-core 2.1.4 → 2.1.6
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/components.d.ts +8 -0
- package/configs/tsconfig.base.json +2 -1
- package/dist/fds-vue-core.cjs.js +35 -15
- package/dist/fds-vue-core.cjs.js.map +1 -1
- package/dist/fds-vue-core.es.js +35 -15
- package/dist/fds-vue-core.es.js.map +1 -1
- package/dist/global-components.d.ts +35 -33
- package/package.json +23 -21
- package/src/.DS_Store +0 -0
- package/src/App.vue +133 -0
- package/src/apply.css +60 -0
- package/src/assets/icons.ts +517 -0
- package/src/components/Blocks/FdsBlockAlert/FdsBlockAlert.stories.ts +94 -0
- package/src/components/Blocks/FdsBlockAlert/FdsBlockAlert.vue +112 -0
- package/src/components/Blocks/FdsBlockAlert/types.ts +12 -0
- package/src/components/Blocks/FdsBlockContent/FdsBlockContent.stories.ts +110 -0
- package/src/components/Blocks/FdsBlockContent/FdsBlockContent.vue +66 -0
- package/src/components/Blocks/FdsBlockContent/types.ts +6 -0
- package/src/components/Blocks/FdsBlockExpander/FdsBlockExpander.stories.ts +123 -0
- package/src/components/Blocks/FdsBlockExpander/FdsBlockExpander.vue +87 -0
- package/src/components/Blocks/FdsBlockExpander/types.ts +8 -0
- package/src/components/Blocks/FdsBlockInfo/FdsBlockInfo.stories.ts +110 -0
- package/src/components/Blocks/FdsBlockInfo/FdsBlockInfo.vue +75 -0
- package/src/components/Blocks/FdsBlockInfo/types.ts +9 -0
- package/src/components/Blocks/FdsBlockLink/FdsBlockLink.css +9 -0
- package/src/components/Blocks/FdsBlockLink/FdsBlockLink.stories.ts +179 -0
- package/src/components/Blocks/FdsBlockLink/FdsBlockLink.vue +149 -0
- package/src/components/Blocks/FdsBlockLink/types.ts +14 -0
- package/src/components/Buttons/ButtonBaseProps.ts +18 -0
- package/src/components/Buttons/FdsButtonCopy/FdsButtonCopy.stories.ts +53 -0
- package/src/components/Buttons/FdsButtonCopy/FdsButtonCopy.vue +87 -0
- package/src/components/Buttons/FdsButtonCopy/types.ts +8 -0
- package/src/components/Buttons/FdsButtonDownload/FdsButtonDownload.stories.ts +111 -0
- package/src/components/Buttons/FdsButtonDownload/FdsButtonDownload.vue +187 -0
- package/src/components/Buttons/FdsButtonIcon/FdsButtonIcon.stories.ts +55 -0
- package/src/components/Buttons/FdsButtonIcon/FdsButtonIcon.vue +57 -0
- package/src/components/Buttons/FdsButtonIcon/types.ts +12 -0
- package/src/components/Buttons/FdsButtonMinor/FdsButtonMinor.stories.ts +68 -0
- package/src/components/Buttons/FdsButtonMinor/FdsButtonMinor.vue +126 -0
- package/src/components/Buttons/FdsButtonPrimary/FdsButtonPrimary.stories.ts +86 -0
- package/src/components/Buttons/FdsButtonPrimary/FdsButtonPrimary.vue +107 -0
- package/src/components/Buttons/FdsButtonSecondary/FdsButtonSecondary.stories.ts +68 -0
- package/src/components/Buttons/FdsButtonSecondary/FdsButtonSecondary.vue +107 -0
- package/src/components/FdsIcon/FdsIcon.stories.ts +69 -0
- package/src/components/FdsIcon/FdsIcon.vue +34 -0
- package/src/components/FdsIcon/types.ts +9 -0
- package/src/components/FdsModal/FdsModal.stories.ts +241 -0
- package/src/components/FdsModal/FdsModal.vue +269 -0
- package/src/components/FdsModal/types.ts +12 -0
- package/src/components/FdsPagination/FdsPagination.stories.ts +109 -0
- package/src/components/FdsPagination/FdsPagination.vue +193 -0
- package/src/components/FdsPagination/types.ts +6 -0
- package/src/components/FdsSearchSelect/FdsSearchSelect.stories.ts +428 -0
- package/src/components/FdsSearchSelect/FdsSearchSelect.vue +621 -0
- package/src/components/FdsSearchSelect/types.ts +25 -0
- package/src/components/FdsSpinner/FdsSpinner.stories.ts +31 -0
- package/src/components/FdsSpinner/FdsSpinner.vue +90 -0
- package/src/components/FdsSpinner/types.ts +6 -0
- package/src/components/FdsSticker/FdsSticker.stories.ts +148 -0
- package/src/components/FdsSticker/FdsSticker.vue +44 -0
- package/src/components/FdsSticker/types.ts +4 -0
- package/src/components/FdsTreeView/FdsTreeView.stories.ts +136 -0
- package/src/components/FdsTreeView/FdsTreeView.vue +162 -0
- package/src/components/FdsTreeView/TreeNode.vue +383 -0
- package/src/components/FdsTreeView/types.ts +141 -0
- package/src/components/FdsTreeView/useTreeState.ts +607 -0
- package/src/components/FdsTreeView/utils.ts +69 -0
- package/src/components/FdsTruncatedText/FdsTruncatedText.stories.ts +78 -0
- package/src/components/FdsTruncatedText/FdsTruncatedText.vue +85 -0
- package/src/components/FdsTruncatedText/types.ts +6 -0
- package/src/components/Form/FdsCheckbox/FdsCheckbox.stories.ts +275 -0
- package/src/components/Form/FdsCheckbox/FdsCheckbox.vue +155 -0
- package/src/components/Form/FdsCheckbox/types.ts +10 -0
- package/src/components/Form/FdsInput/FdsInput.stories.ts +319 -0
- package/src/components/Form/FdsInput/FdsInput.vue +233 -0
- package/src/components/Form/FdsInput/types.ts +25 -0
- package/src/components/Form/FdsRadio/FdsRadio.stories.ts +63 -0
- package/src/components/Form/FdsRadio/FdsRadio.vue +88 -0
- package/src/components/Form/FdsRadio/types.ts +12 -0
- package/src/components/Form/FdsSelect/FdsSelect.stories.ts +78 -0
- package/src/components/Form/FdsSelect/FdsSelect.vue +136 -0
- package/src/components/Form/FdsSelect/types.ts +13 -0
- package/src/components/Form/FdsTextarea/FdsTextarea.stories.ts +52 -0
- package/src/components/Form/FdsTextarea/FdsTextarea.vue +110 -0
- package/src/components/Form/FdsTextarea/types.ts +12 -0
- package/src/components/Table/FdsTable/FdsTable.stories.ts +221 -0
- package/src/components/Table/FdsTable/FdsTable.vue +25 -0
- package/src/components/Table/FdsTable/types.ts +4 -0
- package/src/components/Table/FdsTableHead/FdsTableHead.stories.ts +151 -0
- package/src/components/Table/FdsTableHead/FdsTableHead.vue +54 -0
- package/src/components/Table/FdsTableHead/types.ts +5 -0
- package/src/components/Tabs/FdsTabs/FdsTabs.stories.ts +247 -0
- package/src/components/Tabs/FdsTabs/FdsTabs.vue +27 -0
- package/src/components/Tabs/FdsTabs/types.ts +4 -0
- package/src/components/Tabs/FdsTabsItem/FdsTabsItem.vue +125 -0
- package/src/components/Tabs/FdsTabsItem/types.ts +16 -0
- package/src/components/Typography/FdsHeading/FdsHeading.stories.ts +93 -0
- package/src/components/Typography/FdsHeading/FdsHeading.vue +51 -0
- package/src/components/Typography/FdsHeading/types.ts +5 -0
- package/src/components/Typography/FdsListHeading/FdsListHeading.stories.ts +58 -0
- package/src/components/Typography/FdsListHeading/FdsListHeading.vue +62 -0
- package/src/components/Typography/FdsListHeading/types.ts +8 -0
- package/src/components/Typography/FdsSeparator/FdsSeparator.stories.ts +31 -0
- package/src/components/Typography/FdsSeparator/FdsSeparator.vue +5 -0
- package/src/components/Typography/FdsText/FdsText.stories.ts +66 -0
- package/src/components/Typography/FdsText/FdsText.vue +28 -0
- package/src/components/Typography/FdsText/types.ts +3 -0
- package/src/composables/useBoldQuery.ts +29 -0
- package/src/composables/useElementFinalSize.ts +24 -0
- package/src/composables/useHasSlots.ts +17 -0
- package/src/composables/useIsPid.ts +48 -0
- package/src/docs/Start/Start.mdx +12 -0
- package/src/docs/Usage.md +117 -0
- package/src/fonts.css +28 -0
- package/src/global-components.ts +75 -0
- package/src/index.ts +180 -0
- package/src/main.ts +7 -0
- package/src/slot-styles.css +93 -0
- package/src/style.css +89 -0
- package/src/tokens.css +252 -0
- package/dist/index.d.ts +0 -2
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import { ref } from 'vue'
|
|
3
|
+
import FdsInput from './FdsInput.vue'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof FdsInput> = {
|
|
6
|
+
title: 'FDS/Form/FdsInput',
|
|
7
|
+
component: FdsInput,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
value: {
|
|
11
|
+
control: { type: 'text' },
|
|
12
|
+
description: 'Input value (use v-model for two-way binding)',
|
|
13
|
+
},
|
|
14
|
+
label: {
|
|
15
|
+
control: { type: 'text' },
|
|
16
|
+
description: 'Label text displayed above the input',
|
|
17
|
+
},
|
|
18
|
+
disabled: {
|
|
19
|
+
control: { type: 'boolean' },
|
|
20
|
+
description: 'Disables the input field',
|
|
21
|
+
},
|
|
22
|
+
valid: {
|
|
23
|
+
control: { type: 'select' },
|
|
24
|
+
options: ['true', 'false', 'null'],
|
|
25
|
+
description: 'Validation state: "true" for valid, "false" for invalid, "null" for neutral',
|
|
26
|
+
},
|
|
27
|
+
optional: {
|
|
28
|
+
control: { type: 'boolean' },
|
|
29
|
+
description: 'Marks the field as optional (hides invalid message if true)',
|
|
30
|
+
},
|
|
31
|
+
invalidMessage: {
|
|
32
|
+
control: { type: 'text' },
|
|
33
|
+
description: 'Error message displayed when validation is false',
|
|
34
|
+
},
|
|
35
|
+
required: {
|
|
36
|
+
control: { type: 'boolean' },
|
|
37
|
+
description: 'Marks the field as required',
|
|
38
|
+
},
|
|
39
|
+
labelLeft: {
|
|
40
|
+
control: { type: 'boolean' },
|
|
41
|
+
description: 'If true, positions the label to the left of the input',
|
|
42
|
+
},
|
|
43
|
+
meta: {
|
|
44
|
+
control: { type: 'text' },
|
|
45
|
+
description: 'Meta text displayed below the label',
|
|
46
|
+
},
|
|
47
|
+
type: {
|
|
48
|
+
control: { type: 'select' },
|
|
49
|
+
options: ['text', 'search', 'email', 'password'],
|
|
50
|
+
description: 'Input type',
|
|
51
|
+
},
|
|
52
|
+
clearButton: {
|
|
53
|
+
control: { type: 'boolean' },
|
|
54
|
+
description: 'Shows a clear button when input has a value',
|
|
55
|
+
},
|
|
56
|
+
name: {
|
|
57
|
+
control: { type: 'text' },
|
|
58
|
+
description: 'HTML name attribute for the input',
|
|
59
|
+
},
|
|
60
|
+
id: {
|
|
61
|
+
control: { type: 'text' },
|
|
62
|
+
description: 'HTML id attribute for the input',
|
|
63
|
+
},
|
|
64
|
+
passwordLabels: {
|
|
65
|
+
control: { type: 'object' },
|
|
66
|
+
description: 'Array with [showLabel, hideLabel] for password toggle button',
|
|
67
|
+
},
|
|
68
|
+
locale: {
|
|
69
|
+
control: { type: 'select' },
|
|
70
|
+
options: ['sv', 'en'],
|
|
71
|
+
description: 'Locale for translations ("sv" for Swedish, "en" for English)',
|
|
72
|
+
},
|
|
73
|
+
size: {
|
|
74
|
+
control: { type: 'number' },
|
|
75
|
+
description: 'HTML size attribute for the input',
|
|
76
|
+
},
|
|
77
|
+
maxlength: {
|
|
78
|
+
control: { type: 'number' },
|
|
79
|
+
description: 'Maximum length of the input value',
|
|
80
|
+
},
|
|
81
|
+
mask: {
|
|
82
|
+
control: { type: 'text' },
|
|
83
|
+
description: 'IMask pattern string (e.g., "00000000-0000" for personnummer)',
|
|
84
|
+
},
|
|
85
|
+
maskOptions: {
|
|
86
|
+
control: { type: 'object' },
|
|
87
|
+
description: 'Additional options for IMask (e.g., { lazy: true, placeholderChar: "" })',
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
args: {
|
|
91
|
+
value: '',
|
|
92
|
+
label: 'Label',
|
|
93
|
+
labelLeft: false,
|
|
94
|
+
meta: '',
|
|
95
|
+
valid: 'null',
|
|
96
|
+
optional: false,
|
|
97
|
+
invalidMessage: '',
|
|
98
|
+
clearButton: false,
|
|
99
|
+
disabled: false,
|
|
100
|
+
required: false,
|
|
101
|
+
type: 'text',
|
|
102
|
+
passwordLabels: undefined,
|
|
103
|
+
locale: 'sv',
|
|
104
|
+
size: undefined,
|
|
105
|
+
maxlength: undefined,
|
|
106
|
+
mask: undefined,
|
|
107
|
+
maskOptions: undefined,
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export default meta
|
|
112
|
+
type Story = StoryObj<typeof meta>
|
|
113
|
+
|
|
114
|
+
export const Basic: Story = {
|
|
115
|
+
render: (args) => ({
|
|
116
|
+
components: { FdsInput },
|
|
117
|
+
setup: () => ({ args }),
|
|
118
|
+
template: '<FdsInput v-bind="args" />',
|
|
119
|
+
}),
|
|
120
|
+
args: {
|
|
121
|
+
label: 'Namn',
|
|
122
|
+
meta: 'Ange ditt fullständiga namn',
|
|
123
|
+
},
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const WithVModel: Story = {
|
|
127
|
+
render: () => ({
|
|
128
|
+
components: { FdsInput },
|
|
129
|
+
setup() {
|
|
130
|
+
const text = ref('')
|
|
131
|
+
return { text }
|
|
132
|
+
},
|
|
133
|
+
template: `
|
|
134
|
+
<div>
|
|
135
|
+
<FdsInput v-model="text" label="Sök" placeholder="Sök..." clearButton />
|
|
136
|
+
<p class="mt-4">Värde: {{ text }}</p>
|
|
137
|
+
</div>
|
|
138
|
+
`,
|
|
139
|
+
}),
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export const WithMeta: Story = {
|
|
143
|
+
render: (args) => ({
|
|
144
|
+
components: { FdsInput },
|
|
145
|
+
setup: () => ({ args }),
|
|
146
|
+
template: '<FdsInput v-bind="args" meta="Detta är hjälptext som visas under labeln" />',
|
|
147
|
+
}),
|
|
148
|
+
args: {
|
|
149
|
+
label: 'E-postadress',
|
|
150
|
+
},
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export const WithValidation: Story = {
|
|
154
|
+
render: () => ({
|
|
155
|
+
components: { FdsInput },
|
|
156
|
+
setup() {
|
|
157
|
+
const email = ref('')
|
|
158
|
+
const valid = ref<string>('null')
|
|
159
|
+
|
|
160
|
+
const validateEmail = (value: string) => {
|
|
161
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
|
|
162
|
+
if (value.length === 0) {
|
|
163
|
+
valid.value = 'null'
|
|
164
|
+
} else if (emailRegex.test(value)) {
|
|
165
|
+
valid.value = 'true'
|
|
166
|
+
} else {
|
|
167
|
+
valid.value = 'false'
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return { email, valid, validateEmail }
|
|
172
|
+
},
|
|
173
|
+
template: `
|
|
174
|
+
<FdsInput
|
|
175
|
+
v-model="email"
|
|
176
|
+
label="E-postadress"
|
|
177
|
+
type="email"
|
|
178
|
+
:valid="valid"
|
|
179
|
+
invalid-message="Ange en giltig e-postadress"
|
|
180
|
+
@input="validateEmail(email)"
|
|
181
|
+
/>
|
|
182
|
+
`,
|
|
183
|
+
}),
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export const Disabled: Story = {
|
|
187
|
+
render: (args) => ({
|
|
188
|
+
components: { FdsInput },
|
|
189
|
+
setup: () => ({ args }),
|
|
190
|
+
template: '<FdsInput v-bind="args" />',
|
|
191
|
+
}),
|
|
192
|
+
args: {
|
|
193
|
+
label: 'Inaktiverat fält',
|
|
194
|
+
value: 'Detta värde kan inte ändras',
|
|
195
|
+
disabled: true,
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export const Required: Story = {
|
|
200
|
+
render: (args) => ({
|
|
201
|
+
components: { FdsInput },
|
|
202
|
+
setup: () => ({ args }),
|
|
203
|
+
template: '<FdsInput v-bind="args" />',
|
|
204
|
+
}),
|
|
205
|
+
args: {
|
|
206
|
+
label: 'Obligatoriskt fält',
|
|
207
|
+
required: true,
|
|
208
|
+
invalidMessage: 'Detta fält är obligatoriskt',
|
|
209
|
+
},
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
export const WithLabelLeft: Story = {
|
|
213
|
+
render: (args) => ({
|
|
214
|
+
components: { FdsInput },
|
|
215
|
+
setup: () => ({ args }),
|
|
216
|
+
template: '<FdsInput v-bind="args" />',
|
|
217
|
+
}),
|
|
218
|
+
args: {
|
|
219
|
+
label: 'Label till vänster',
|
|
220
|
+
labelLeft: true,
|
|
221
|
+
meta: 'Meta-text visas här',
|
|
222
|
+
},
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export const Password: Story = {
|
|
226
|
+
render: () => ({
|
|
227
|
+
components: { FdsInput },
|
|
228
|
+
setup() {
|
|
229
|
+
const password = ref('')
|
|
230
|
+
return { password }
|
|
231
|
+
},
|
|
232
|
+
template: `
|
|
233
|
+
<FdsInput
|
|
234
|
+
v-model="password"
|
|
235
|
+
label="Lösenord"
|
|
236
|
+
type="password"
|
|
237
|
+
meta="Minst 8 tecken"
|
|
238
|
+
/>
|
|
239
|
+
`,
|
|
240
|
+
}),
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export const Search: Story = {
|
|
244
|
+
render: () => ({
|
|
245
|
+
components: { FdsInput },
|
|
246
|
+
setup() {
|
|
247
|
+
const searchTerm = ref('')
|
|
248
|
+
return { searchTerm }
|
|
249
|
+
},
|
|
250
|
+
template: `
|
|
251
|
+
<FdsInput
|
|
252
|
+
v-model="searchTerm"
|
|
253
|
+
label="Sök"
|
|
254
|
+
type="search"
|
|
255
|
+
clearButton
|
|
256
|
+
placeholder="Sök efter..."
|
|
257
|
+
/>
|
|
258
|
+
`,
|
|
259
|
+
}),
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export const WithMask: Story = {
|
|
263
|
+
render: () => ({
|
|
264
|
+
components: { FdsInput },
|
|
265
|
+
setup() {
|
|
266
|
+
const personnummer = ref('')
|
|
267
|
+
return { personnummer }
|
|
268
|
+
},
|
|
269
|
+
template: `
|
|
270
|
+
<div>
|
|
271
|
+
<FdsInput
|
|
272
|
+
v-model="personnummer"
|
|
273
|
+
label="Personnummer"
|
|
274
|
+
mask="00000000-0000"
|
|
275
|
+
:maskOptions="{ lazy: true }"
|
|
276
|
+
meta="Format: yyyymmdd-nnnn"
|
|
277
|
+
/>
|
|
278
|
+
<p class="mt-4">Värde: {{ personnummer }}</p>
|
|
279
|
+
</div>
|
|
280
|
+
`,
|
|
281
|
+
}),
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
export const WithMaskPhone: Story = {
|
|
285
|
+
render: () => ({
|
|
286
|
+
components: { FdsInput },
|
|
287
|
+
setup() {
|
|
288
|
+
const phone = ref('')
|
|
289
|
+
return { phone }
|
|
290
|
+
},
|
|
291
|
+
template: `
|
|
292
|
+
<div>
|
|
293
|
+
<FdsInput
|
|
294
|
+
v-model="phone"
|
|
295
|
+
label="Telefonnummer"
|
|
296
|
+
mask="+46 (000) 000-00-00"
|
|
297
|
+
:maskOptions="{ lazy: true }"
|
|
298
|
+
/>
|
|
299
|
+
<p class="mt-4">Värde: {{ phone }}</p>
|
|
300
|
+
</div>
|
|
301
|
+
`,
|
|
302
|
+
}),
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export const AllStates: Story = {
|
|
306
|
+
render: () => ({
|
|
307
|
+
components: { FdsInput },
|
|
308
|
+
setup: () => ({}),
|
|
309
|
+
template: `
|
|
310
|
+
<div class="space-y-6">
|
|
311
|
+
<FdsInput label="Neutral" value="Neutralt tillstånd" />
|
|
312
|
+
<FdsInput label="Valid" value="Giltigt värde" valid="true" />
|
|
313
|
+
<FdsInput label="Invalid" value="Ogiltigt värde" valid="false" invalid-message="Detta är ett felmeddelande" />
|
|
314
|
+
<FdsInput label="Disabled" value="Inaktiverat" disabled />
|
|
315
|
+
<FdsInput label="Required" required invalid-message="Detta fält är obligatoriskt" />
|
|
316
|
+
</div>
|
|
317
|
+
`,
|
|
318
|
+
}),
|
|
319
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
3
|
+
// @ts-ignore - IMask types may not be resolved in build
|
|
4
|
+
import IMask from 'imask'
|
|
5
|
+
import FdsButtonIcon from '@/components/Buttons/FdsButtonIcon/FdsButtonIcon.vue'
|
|
6
|
+
import FdsButtonMinor from '@/components/Buttons/FdsButtonMinor/FdsButtonMinor.vue'
|
|
7
|
+
import FdsIcon from '@/components/FdsIcon/FdsIcon.vue'
|
|
8
|
+
import type { FdsInputProps } from './types'
|
|
9
|
+
|
|
10
|
+
// Support both v-model (modelValue) and :value prop for backward compatibility
|
|
11
|
+
const modelValue = defineModel<string>({ default: undefined, required: false })
|
|
12
|
+
|
|
13
|
+
const props = withDefaults(defineProps<FdsInputProps>(), {
|
|
14
|
+
value: undefined,
|
|
15
|
+
label: undefined,
|
|
16
|
+
disabled: false,
|
|
17
|
+
valid: undefined,
|
|
18
|
+
optional: false,
|
|
19
|
+
required: false,
|
|
20
|
+
labelLeft: false,
|
|
21
|
+
meta: undefined,
|
|
22
|
+
type: 'text',
|
|
23
|
+
passwordLabels: undefined,
|
|
24
|
+
clearButton: false,
|
|
25
|
+
name: undefined,
|
|
26
|
+
id: undefined,
|
|
27
|
+
mask: undefined,
|
|
28
|
+
maskOptions: undefined,
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
const emit = defineEmits<{
|
|
32
|
+
(e: 'input', ev: Event): void
|
|
33
|
+
(e: 'clearInput'): void
|
|
34
|
+
(e: 'update:value', value: string): void
|
|
35
|
+
}>()
|
|
36
|
+
|
|
37
|
+
const autoId = `fds-input-${Math.random().toString(36).slice(2, 9)}`
|
|
38
|
+
const inputId = computed(() => props.id ?? autoId)
|
|
39
|
+
|
|
40
|
+
const clearButtonLabel = computed(() => (props.locale === 'sv' ? 'Rensa input' : 'Clear input'))
|
|
41
|
+
|
|
42
|
+
const showInvalidMessage = computed(() => props.valid === 'false' && !props.optional && props.invalidMessage)
|
|
43
|
+
const isInvalid = computed(() => props.valid === 'false' && !props.optional && !props.disabled)
|
|
44
|
+
const isValid = computed(() => props.valid === 'true')
|
|
45
|
+
const showPasswordToggle = computed(() => isPasswordType.value && internalValue.value.length > 0)
|
|
46
|
+
|
|
47
|
+
const inputClasses = computed(() => [
|
|
48
|
+
'block w-full rounded-md border border-gray-500 px-3 py-[calc(0.75rem-1px)] mb-6',
|
|
49
|
+
'focus:outline-2 focus:outline-blue-500 -outline-offset-2 focus:border-transparent',
|
|
50
|
+
props.disabled ? 'outline-dashed outline-2 outline-gray-400 cursor-not-allowed border-transparent' : 'bg-white',
|
|
51
|
+
isInvalid.value && 'outline-2 outline-red-600',
|
|
52
|
+
])
|
|
53
|
+
|
|
54
|
+
const validationIconClasses = computed(() => [
|
|
55
|
+
'absolute flex gap-2 right-4 top-1/2 -translate-y-1/2 flex items-center justify-end',
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
// Use modelValue if bound via v-model, otherwise use value prop
|
|
59
|
+
const internalValue = computed({
|
|
60
|
+
get: () =>
|
|
61
|
+
// If modelValue is explicitly set (via v-model), use it
|
|
62
|
+
// Otherwise fall back to value prop
|
|
63
|
+
modelValue.value !== undefined ? modelValue.value : (props.value ?? ''),
|
|
64
|
+
set: (newValue: string) => {
|
|
65
|
+
// Update modelValue if it's being used
|
|
66
|
+
if (modelValue.value !== undefined) {
|
|
67
|
+
modelValue.value = newValue
|
|
68
|
+
}
|
|
69
|
+
// Always emit update:value for backward compatibility
|
|
70
|
+
emit('update:value', newValue)
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
function onClear() {
|
|
75
|
+
internalValue.value = ''
|
|
76
|
+
emit('clearInput')
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const showPassword = ref(false)
|
|
80
|
+
const isPasswordType = computed(() => props.type === 'password')
|
|
81
|
+
|
|
82
|
+
function toggleShowPassword() {
|
|
83
|
+
showPassword.value = !showPassword.value
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// IMask setup
|
|
87
|
+
const inputRef = ref<HTMLInputElement | null>(null)
|
|
88
|
+
|
|
89
|
+
let maskInstance: any = null
|
|
90
|
+
|
|
91
|
+
const createMask = () => {
|
|
92
|
+
// Destroy existing mask if any
|
|
93
|
+
if (maskInstance) {
|
|
94
|
+
maskInstance.destroy()
|
|
95
|
+
maskInstance = null
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Create new mask if mask prop is provided and input exists
|
|
99
|
+
if (props.mask && inputRef.value) {
|
|
100
|
+
const maskConfig = {
|
|
101
|
+
mask: props.mask,
|
|
102
|
+
...props.maskOptions,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
maskInstance = IMask(inputRef.value, maskConfig as any)
|
|
106
|
+
|
|
107
|
+
// Sync mask value with internalValue
|
|
108
|
+
if (internalValue.value) {
|
|
109
|
+
maskInstance.value = internalValue.value
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Update internalValue when mask value changes
|
|
113
|
+
maskInstance.on('accept', () => {
|
|
114
|
+
if (maskInstance) {
|
|
115
|
+
internalValue.value = maskInstance.value
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
onMounted(() => {
|
|
122
|
+
nextTick(() => {
|
|
123
|
+
createMask()
|
|
124
|
+
})
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
onBeforeUnmount(() => {
|
|
128
|
+
if (maskInstance) {
|
|
129
|
+
maskInstance.destroy()
|
|
130
|
+
maskInstance = null
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
// Watch for mask prop changes and recreate mask
|
|
135
|
+
watch(
|
|
136
|
+
() => props.mask,
|
|
137
|
+
() => {
|
|
138
|
+
nextTick(() => {
|
|
139
|
+
createMask()
|
|
140
|
+
})
|
|
141
|
+
},
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
// Watch for external value changes and update mask
|
|
145
|
+
watch(
|
|
146
|
+
() => props.value,
|
|
147
|
+
(newValue) => {
|
|
148
|
+
if (maskInstance && newValue !== undefined && newValue !== maskInstance.value) {
|
|
149
|
+
maskInstance.value = newValue
|
|
150
|
+
}
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
watch(
|
|
155
|
+
() => modelValue.value,
|
|
156
|
+
(newValue) => {
|
|
157
|
+
if (maskInstance && newValue !== undefined && newValue !== maskInstance.value) {
|
|
158
|
+
maskInstance.value = newValue
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
)
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
<template>
|
|
165
|
+
<div class="w-full mb-4">
|
|
166
|
+
<div :class="{ 'flex flex-row gap-4': labelLeft }">
|
|
167
|
+
<div>
|
|
168
|
+
<label
|
|
169
|
+
v-if="label"
|
|
170
|
+
:for="inputId"
|
|
171
|
+
class="block font-bold text-gray-900 cursor-pointer"
|
|
172
|
+
:class="{ 'mb-0': meta, 'mb-1': !meta }"
|
|
173
|
+
>{{ label }}</label
|
|
174
|
+
>
|
|
175
|
+
<div
|
|
176
|
+
v-if="meta"
|
|
177
|
+
class="font-thin"
|
|
178
|
+
:class="{ 'mb-1': !labelLeft }"
|
|
179
|
+
>
|
|
180
|
+
{{ meta }}
|
|
181
|
+
</div>
|
|
182
|
+
</div>
|
|
183
|
+
<div :class="{ 'flex-1': labelLeft }">
|
|
184
|
+
<div class="relative">
|
|
185
|
+
<input
|
|
186
|
+
ref="inputRef"
|
|
187
|
+
:id="inputId"
|
|
188
|
+
:name="name || undefined"
|
|
189
|
+
:type="isPasswordType ? (showPassword ? 'text' : 'password') : type"
|
|
190
|
+
:disabled="disabled"
|
|
191
|
+
:required="required"
|
|
192
|
+
:size="size as any"
|
|
193
|
+
:maxlength="maxlength as any"
|
|
194
|
+
v-model="internalValue"
|
|
195
|
+
:aria-invalid="valid === 'false' ? 'true' : undefined"
|
|
196
|
+
:class="inputClasses"
|
|
197
|
+
v-bind="$attrs"
|
|
198
|
+
@input="$emit('input', $event)"
|
|
199
|
+
/>
|
|
200
|
+
<div :class="validationIconClasses">
|
|
201
|
+
<FdsIcon
|
|
202
|
+
v-if="isInvalid"
|
|
203
|
+
name="alert"
|
|
204
|
+
class="fill-red-600"
|
|
205
|
+
/>
|
|
206
|
+
<FdsIcon
|
|
207
|
+
v-if="isValid"
|
|
208
|
+
name="bigSuccess"
|
|
209
|
+
/>
|
|
210
|
+
<FdsButtonIcon
|
|
211
|
+
v-if="clearButton && !!internalValue && !disabled"
|
|
212
|
+
icon="cross"
|
|
213
|
+
:ariaLabel="clearButtonLabel"
|
|
214
|
+
@click="onClear"
|
|
215
|
+
/>
|
|
216
|
+
<FdsButtonMinor
|
|
217
|
+
v-if="showPasswordToggle"
|
|
218
|
+
:icon="showPassword ? 'viewOff' : 'viewOn'"
|
|
219
|
+
text=""
|
|
220
|
+
@click="toggleShowPassword"
|
|
221
|
+
/>
|
|
222
|
+
</div>
|
|
223
|
+
</div>
|
|
224
|
+
</div>
|
|
225
|
+
</div>
|
|
226
|
+
<div
|
|
227
|
+
v-if="showInvalidMessage"
|
|
228
|
+
class="text-red-600 font-bold mt-1"
|
|
229
|
+
>
|
|
230
|
+
{{ invalidMessage }}
|
|
231
|
+
</div>
|
|
232
|
+
</div>
|
|
233
|
+
</template>
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export interface FdsInputProps {
|
|
2
|
+
value?: string
|
|
3
|
+
label?: string
|
|
4
|
+
disabled?: boolean
|
|
5
|
+
valid?: string
|
|
6
|
+
optional?: boolean
|
|
7
|
+
invalidMessage?: string
|
|
8
|
+
required?: boolean
|
|
9
|
+
labelLeft?: boolean
|
|
10
|
+
meta?: string
|
|
11
|
+
type?: 'text' | 'search' | 'email' | 'password'
|
|
12
|
+
clearButton?: boolean
|
|
13
|
+
name?: string
|
|
14
|
+
id?: string
|
|
15
|
+
passwordLabels?: string[]
|
|
16
|
+
locale?: 'sv' | 'en'
|
|
17
|
+
size?: number | string
|
|
18
|
+
maxlength?: number
|
|
19
|
+
mask?: string | RegExp | Array<string | RegExp>
|
|
20
|
+
maskOptions?: {
|
|
21
|
+
lazy?: boolean
|
|
22
|
+
placeholderChar?: string
|
|
23
|
+
[key: string]: unknown
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/vue3'
|
|
2
|
+
import { ref, watch } from 'vue'
|
|
3
|
+
import FdsRadio from './FdsRadio.vue'
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof FdsRadio> = {
|
|
6
|
+
title: 'FDS/Form/FdsRadio',
|
|
7
|
+
component: FdsRadio,
|
|
8
|
+
tags: ['autodocs'],
|
|
9
|
+
argTypes: {
|
|
10
|
+
label: { control: 'text' },
|
|
11
|
+
checked: { control: 'boolean' },
|
|
12
|
+
disabled: { control: 'boolean' },
|
|
13
|
+
name: { control: 'text' },
|
|
14
|
+
value: { control: 'text' },
|
|
15
|
+
id: { control: 'text' },
|
|
16
|
+
},
|
|
17
|
+
args: {
|
|
18
|
+
label: 'Radio label',
|
|
19
|
+
checked: false,
|
|
20
|
+
disabled: false,
|
|
21
|
+
name: 'group1',
|
|
22
|
+
value: 'optionA',
|
|
23
|
+
id: undefined,
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default meta
|
|
28
|
+
type Story = StoryObj<typeof meta>
|
|
29
|
+
|
|
30
|
+
export const Default: Story = {
|
|
31
|
+
render: (args) => ({
|
|
32
|
+
components: { FdsRadio },
|
|
33
|
+
setup() {
|
|
34
|
+
const checked = ref(args.checked)
|
|
35
|
+
watch(
|
|
36
|
+
() => args.checked,
|
|
37
|
+
(v) => (checked.value = v),
|
|
38
|
+
)
|
|
39
|
+
return { args, checked }
|
|
40
|
+
},
|
|
41
|
+
template: `
|
|
42
|
+
<FdsRadio v-bind="args" v-model:checked="checked" />
|
|
43
|
+
`,
|
|
44
|
+
}),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const Group: Story = {
|
|
48
|
+
render: () => ({
|
|
49
|
+
components: { FdsRadio },
|
|
50
|
+
setup() {
|
|
51
|
+
const selected = ref('a')
|
|
52
|
+
return { selected }
|
|
53
|
+
},
|
|
54
|
+
template: `
|
|
55
|
+
<div class="space-y-2">
|
|
56
|
+
<FdsRadio name="g1" value="a" v-model:checked="selected === 'a'" @change="(v) => selected = v ? 'a' : selected">Alternative A</FdsRadio>
|
|
57
|
+
<FdsRadio name="g1" value="b" v-model:checked="selected === 'b'" @change="(v) => selected = v ? 'b' : selected">Alternative B</FdsRadio>
|
|
58
|
+
<FdsRadio name="g1" value="c" v-model:checked="selected === 'c'" @change="(v) => selected = v ? 'c' : selected">Alternative C</FdsRadio>
|
|
59
|
+
<div>Selected: {{ selected }}</div>
|
|
60
|
+
</div>
|
|
61
|
+
`,
|
|
62
|
+
}),
|
|
63
|
+
}
|