fds-vue-core 8.2.2 → 8.2.3
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/dist/components/FdsSearchSelect/FdsSearchSelect.vue.d.ts +2 -0
- package/dist/components/FdsSearchSelectPro/FdsSearchSelectPro.vue.d.ts +2 -0
- package/dist/components/Form/FdsInput/FdsInput.vue.d.ts +1 -0
- package/dist/components/Form/FdsInput/types.d.ts +2 -0
- package/dist/fds-vue-core.cjs.js +155 -33
- package/dist/fds-vue-core.cjs.js.map +1 -1
- package/dist/fds-vue-core.css +1 -1
- package/dist/fds-vue-core.es.js +155 -33
- package/dist/fds-vue-core.es.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/components/Form/FdsInput/FdsInput.stories.ts +27 -1
- package/src/components/Form/FdsInput/FdsInput.vue +169 -29
- package/src/components/Form/FdsInput/types.ts +2 -0
- package/src/components/Form/FdsPhonenumber/FdsPhonenumber.vue +1 -1
- package/src/components/Form/FdsPhonenumber/FdsPhonenumberCountryPicker.vue +1 -1
package/package.json
CHANGED
|
@@ -61,9 +61,13 @@ const meta: Meta<typeof FdsInput> = {
|
|
|
61
61
|
control: { type: 'text' },
|
|
62
62
|
description: 'IMask pattern string (e.g., "00000000-0000" for personnummer)',
|
|
63
63
|
},
|
|
64
|
+
displayChar: {
|
|
65
|
+
control: { type: 'text' },
|
|
66
|
+
description: 'Character shown for unfilled mask positions while typing (e.g. "N" for postnummer)',
|
|
67
|
+
},
|
|
64
68
|
maskOptions: {
|
|
65
69
|
control: { type: 'object' },
|
|
66
|
-
description: 'Additional
|
|
70
|
+
description: 'Additional IMask options. Overrides displayChar defaults when set.',
|
|
67
71
|
},
|
|
68
72
|
maxlength: {
|
|
69
73
|
control: { type: 'number' },
|
|
@@ -83,6 +87,7 @@ const meta: Meta<typeof FdsInput> = {
|
|
|
83
87
|
required: false,
|
|
84
88
|
type: 'text',
|
|
85
89
|
mask: undefined,
|
|
90
|
+
displayChar: undefined,
|
|
86
91
|
maskOptions: undefined,
|
|
87
92
|
maxlength: undefined,
|
|
88
93
|
},
|
|
@@ -302,6 +307,27 @@ export const WithMaskPhone: Story = {
|
|
|
302
307
|
}),
|
|
303
308
|
}
|
|
304
309
|
|
|
310
|
+
export const WithMaskPostalCode: Story = {
|
|
311
|
+
render: () => ({
|
|
312
|
+
components: { FdsInput },
|
|
313
|
+
setup() {
|
|
314
|
+
const postalCode = ref('')
|
|
315
|
+
return { postalCode }
|
|
316
|
+
},
|
|
317
|
+
template: `
|
|
318
|
+
<div>
|
|
319
|
+
<FdsInput
|
|
320
|
+
v-model="postalCode"
|
|
321
|
+
label="Postnummer"
|
|
322
|
+
mask="000 00"
|
|
323
|
+
display-char="N"
|
|
324
|
+
/>
|
|
325
|
+
<p class="mt-4">Värde: {{ postalCode }}</p>
|
|
326
|
+
</div>
|
|
327
|
+
`,
|
|
328
|
+
}),
|
|
329
|
+
}
|
|
330
|
+
|
|
305
331
|
export const AllStates: Story = {
|
|
306
332
|
render: () => ({
|
|
307
333
|
components: { FdsInput },
|
|
@@ -27,6 +27,7 @@ const props = withDefaults(defineProps<FdsInputProps>(), {
|
|
|
27
27
|
meta: undefined,
|
|
28
28
|
clearButton: false,
|
|
29
29
|
mask: undefined,
|
|
30
|
+
displayChar: undefined,
|
|
30
31
|
maskOptions: undefined,
|
|
31
32
|
class: undefined,
|
|
32
33
|
pattern: undefined,
|
|
@@ -49,8 +50,12 @@ const emit = defineEmits<{
|
|
|
49
50
|
const { id, type, maxlength, attrs } = useAttrsWithDefaults(props)
|
|
50
51
|
const { t } = useFdsI18n()
|
|
51
52
|
|
|
52
|
-
// Get type from attrs first (user-provided), then props, with 'text' as fallback
|
|
53
|
-
|
|
53
|
+
// Get type from attrs first (user-provided), then props, with 'text' as fallback.
|
|
54
|
+
// IMask only supports type="text".
|
|
55
|
+
const inputType = computed(() => {
|
|
56
|
+
if (props.mask) return 'text'
|
|
57
|
+
return type.value ?? props.type ?? 'text'
|
|
58
|
+
})
|
|
54
59
|
|
|
55
60
|
const autoId = `fds-input-${Math.random().toString(36).slice(2, 9)}`
|
|
56
61
|
const inputId = computed(() => id.value ?? autoId)
|
|
@@ -138,35 +143,116 @@ const inputPaddingRight = computed(() => {
|
|
|
138
143
|
|
|
139
144
|
const externalClass = computed(() => (props.class ?? attrs.class) as HTMLAttributes['class'] | undefined)
|
|
140
145
|
|
|
141
|
-
const
|
|
142
|
-
|
|
143
|
-
|
|
146
|
+
const maskDisplayValue = ref('')
|
|
147
|
+
const maskPlaceholderChar = ref('')
|
|
148
|
+
|
|
149
|
+
const resolvedPlaceholderChar = computed(() => props.displayChar ?? props.maskOptions?.placeholderChar ?? null)
|
|
150
|
+
|
|
151
|
+
const useMaskPlaceholderStyle = computed(() => {
|
|
152
|
+
if (!props.mask || !resolvedPlaceholderChar.value) return false
|
|
153
|
+
const lazy = props.maskOptions?.lazy ?? (props.displayChar ? false : true)
|
|
154
|
+
return !lazy
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
const maskFieldWrapperClasses = computed(() => {
|
|
158
|
+
if (!useMaskPlaceholderStyle.value) return undefined
|
|
159
|
+
return [
|
|
160
|
+
'rounded-md border border-gray-500 bg-white bg-clip-padding',
|
|
161
|
+
props.disabled && 'outline-dashed outline-2 outline-gray-400 cursor-not-allowed border-transparent',
|
|
162
|
+
isInvalid.value
|
|
163
|
+
? 'outline-2 -outline-offset-2 outline-red-600'
|
|
164
|
+
: 'focus-within:outline-2 focus-within:outline-blue-500 focus-within:outline-offset-0',
|
|
165
|
+
'focus-within:border-transparent',
|
|
166
|
+
]
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
const maskFieldPaddingClasses = 'px-3 py-[calc(0.75rem-1px)]'
|
|
170
|
+
|
|
171
|
+
const maskOverlayClasses = computed(() => [
|
|
172
|
+
'pointer-events-none absolute inset-0 z-[3] flex items-center',
|
|
173
|
+
maskFieldPaddingClasses,
|
|
144
174
|
inputPaddingRight.value,
|
|
145
|
-
'focus:outline-2 focus:outline-blue-500 -outline-offset-2 focus:border-transparent',
|
|
146
|
-
showDateIcon.value &&
|
|
147
|
-
'[&::-webkit-calendar-picker-indicator]:opacity-0 [&::-webkit-calendar-picker-indicator]:pointer-events-none',
|
|
148
|
-
props.disabled
|
|
149
|
-
? 'text-gray-800 outline-dashed outline-2 outline-gray-400 cursor-not-allowed border-transparent'
|
|
150
|
-
: 'bg-white',
|
|
151
|
-
isInvalid.value && 'outline-2 outline-red-600',
|
|
152
|
-
props.inputClass,
|
|
153
175
|
])
|
|
154
176
|
|
|
177
|
+
const maskDisplayChars = computed(() => {
|
|
178
|
+
const display = maskDisplayValue.value
|
|
179
|
+
const placeholder = maskPlaceholderChar.value || resolvedPlaceholderChar.value
|
|
180
|
+
if (!display || !placeholder) return []
|
|
181
|
+
return [...display].map((char) => ({
|
|
182
|
+
char,
|
|
183
|
+
isPlaceholder: char === placeholder,
|
|
184
|
+
}))
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
function maskCharStyle(isPlaceholder: boolean): { color: string } {
|
|
188
|
+
if (isPlaceholder) {
|
|
189
|
+
return { color: props.disabled ? 'var(--color-gray-500)' : 'var(--color-gray-400)' }
|
|
190
|
+
}
|
|
191
|
+
return { color: props.disabled ? 'var(--color-gray-800)' : 'var(--color-gray-900)' }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const inputClasses = computed(() => {
|
|
195
|
+
const shared = [
|
|
196
|
+
'block rounded-md',
|
|
197
|
+
maxlength.value ? '' : 'w-full',
|
|
198
|
+
inputPaddingRight.value,
|
|
199
|
+
showDateIcon.value &&
|
|
200
|
+
'[&::-webkit-calendar-picker-indicator]:opacity-0 [&::-webkit-calendar-picker-indicator]:pointer-events-none',
|
|
201
|
+
props.inputClass,
|
|
202
|
+
]
|
|
203
|
+
|
|
204
|
+
if (useMaskPlaceholderStyle.value) {
|
|
205
|
+
return [
|
|
206
|
+
...shared,
|
|
207
|
+
'relative z-[2] border-0 bg-transparent shadow-none focus:outline-none',
|
|
208
|
+
maskFieldPaddingClasses,
|
|
209
|
+
'text-base leading-6 tabular-nums',
|
|
210
|
+
props.disabled ? 'cursor-not-allowed' : '',
|
|
211
|
+
]
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return [
|
|
215
|
+
...shared,
|
|
216
|
+
'rounded-md border border-gray-500',
|
|
217
|
+
maskFieldPaddingClasses,
|
|
218
|
+
'focus:outline-2 focus:outline-blue-500 -outline-offset-2 focus:border-transparent',
|
|
219
|
+
props.disabled
|
|
220
|
+
? 'text-gray-800 outline-dashed outline-2 outline-gray-400 cursor-not-allowed border-transparent'
|
|
221
|
+
: 'text-gray-900 bg-white',
|
|
222
|
+
isInvalid.value && 'outline-2 outline-red-600',
|
|
223
|
+
]
|
|
224
|
+
})
|
|
225
|
+
|
|
155
226
|
const inputStyle = computed(() => {
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
227
|
+
const size: Record<string, string> = maxlength.value
|
|
228
|
+
? {
|
|
229
|
+
width: `calc(${maxlength.value}ch + 1.5rem + 0.25rem)`,
|
|
230
|
+
maxWidth: '100%',
|
|
231
|
+
}
|
|
232
|
+
: {}
|
|
233
|
+
|
|
234
|
+
if (!useMaskPlaceholderStyle.value) {
|
|
235
|
+
return size
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return {
|
|
239
|
+
...size,
|
|
240
|
+
color: 'transparent',
|
|
241
|
+
WebkitTextFillColor: 'transparent',
|
|
242
|
+
caretColor: props.disabled ? '#1f2937' : '#111827',
|
|
161
243
|
}
|
|
162
|
-
return {}
|
|
163
244
|
})
|
|
164
245
|
|
|
165
246
|
const rightIconsContainerClasses = computed(() => [
|
|
166
|
-
'absolute flex gap-2 top-1/2 -translate-y-1/2 items-center justify-end',
|
|
247
|
+
'absolute z-10 flex gap-2 top-1/2 -translate-y-1/2 items-center justify-end',
|
|
167
248
|
showClearButton.value ? 'right-2' : 'right-3',
|
|
168
249
|
])
|
|
169
250
|
|
|
251
|
+
const inputBindings = computed(() => ({
|
|
252
|
+
...inputAttrs.value,
|
|
253
|
+
...(props.mask ? {} : { value: internalValue.value }),
|
|
254
|
+
}))
|
|
255
|
+
|
|
170
256
|
// Use modelValue if bound via v-model, otherwise use value prop
|
|
171
257
|
const internalValue = computed({
|
|
172
258
|
get: () => (modelValue.value !== undefined ? modelValue.value : (props.value ?? '')),
|
|
@@ -180,6 +266,10 @@ const internalValue = computed({
|
|
|
180
266
|
|
|
181
267
|
function onClear() {
|
|
182
268
|
internalValue.value = ''
|
|
269
|
+
if (maskInstance) {
|
|
270
|
+
maskInstance.value = ''
|
|
271
|
+
nextTick(syncMaskDisplay)
|
|
272
|
+
}
|
|
183
273
|
emit('clearInput')
|
|
184
274
|
}
|
|
185
275
|
|
|
@@ -220,6 +310,29 @@ const inputRef = ref<HTMLInputElement | null>(null)
|
|
|
220
310
|
|
|
221
311
|
let maskInstance: any = null
|
|
222
312
|
|
|
313
|
+
function getMaskEmittedValue(instance: typeof maskInstance): string {
|
|
314
|
+
if (!instance) return ''
|
|
315
|
+
|
|
316
|
+
const raw = String(instance.value ?? '')
|
|
317
|
+
if (!useMaskPlaceholderStyle.value) return raw
|
|
318
|
+
|
|
319
|
+
const placeholder = instance.masked?.placeholderChar ?? resolvedPlaceholderChar.value
|
|
320
|
+
if (!placeholder) return raw
|
|
321
|
+
|
|
322
|
+
return raw.split(placeholder).join('').replace(/\s+$/, '')
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function syncMaskDisplay() {
|
|
326
|
+
if (!inputRef.value) {
|
|
327
|
+
maskDisplayValue.value = ''
|
|
328
|
+
maskPlaceholderChar.value = ''
|
|
329
|
+
return
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
maskPlaceholderChar.value = maskInstance?.masked?.placeholderChar ?? resolvedPlaceholderChar.value ?? ''
|
|
333
|
+
maskDisplayValue.value = maskInstance?.displayValue ?? inputRef.value.value ?? maskInstance?.value ?? ''
|
|
334
|
+
}
|
|
335
|
+
|
|
223
336
|
const createMask = () => {
|
|
224
337
|
// Destroy existing mask if any
|
|
225
338
|
if (maskInstance) {
|
|
@@ -232,6 +345,12 @@ const createMask = () => {
|
|
|
232
345
|
const maskConfig = {
|
|
233
346
|
mask: props.mask,
|
|
234
347
|
...props.maskOptions,
|
|
348
|
+
...(props.displayChar
|
|
349
|
+
? {
|
|
350
|
+
placeholderChar: props.displayChar,
|
|
351
|
+
lazy: props.maskOptions?.lazy ?? false,
|
|
352
|
+
}
|
|
353
|
+
: {}),
|
|
235
354
|
}
|
|
236
355
|
|
|
237
356
|
maskInstance = IMask(inputRef.value, maskConfig as any)
|
|
@@ -241,12 +360,24 @@ const createMask = () => {
|
|
|
241
360
|
maskInstance.value = internalValue.value
|
|
242
361
|
}
|
|
243
362
|
|
|
244
|
-
// Update internalValue when mask value changes
|
|
245
363
|
maskInstance.on('accept', () => {
|
|
246
364
|
if (maskInstance) {
|
|
247
|
-
internalValue.value = maskInstance
|
|
365
|
+
internalValue.value = getMaskEmittedValue(maskInstance)
|
|
366
|
+
nextTick(syncMaskDisplay)
|
|
248
367
|
}
|
|
249
368
|
})
|
|
369
|
+
|
|
370
|
+
maskInstance.on('complete', () => {
|
|
371
|
+
nextTick(syncMaskDisplay)
|
|
372
|
+
})
|
|
373
|
+
|
|
374
|
+
nextTick(() => {
|
|
375
|
+
syncMaskDisplay()
|
|
376
|
+
nextTick(syncMaskDisplay)
|
|
377
|
+
})
|
|
378
|
+
} else {
|
|
379
|
+
maskDisplayValue.value = ''
|
|
380
|
+
maskPlaceholderChar.value = ''
|
|
250
381
|
}
|
|
251
382
|
}
|
|
252
383
|
|
|
@@ -279,14 +410,15 @@ const ariaDescribedBy = computed(() => {
|
|
|
279
410
|
return props.meta ? metaId.value : undefined
|
|
280
411
|
})
|
|
281
412
|
|
|
282
|
-
// Watch for mask prop changes and recreate mask
|
|
413
|
+
// Watch for mask-related prop changes and recreate mask
|
|
283
414
|
watch(
|
|
284
|
-
() => props.mask,
|
|
415
|
+
() => [props.mask, props.displayChar, props.maskOptions] as const,
|
|
285
416
|
() => {
|
|
286
417
|
nextTick(() => {
|
|
287
418
|
createMask()
|
|
288
419
|
})
|
|
289
420
|
},
|
|
421
|
+
{ deep: true },
|
|
290
422
|
)
|
|
291
423
|
|
|
292
424
|
// Watch for external value changes and update mask
|
|
@@ -294,10 +426,11 @@ watch(
|
|
|
294
426
|
watch(
|
|
295
427
|
() => props.value,
|
|
296
428
|
(newValue) => {
|
|
297
|
-
if (maskInstance && newValue !== undefined && newValue !== maskInstance
|
|
429
|
+
if (maskInstance && newValue !== undefined && newValue !== getMaskEmittedValue(maskInstance)) {
|
|
298
430
|
// Only update if input doesn't have focus (user is not editing)
|
|
299
431
|
if (document.activeElement !== inputRef.value) {
|
|
300
432
|
maskInstance.value = newValue
|
|
433
|
+
nextTick(syncMaskDisplay)
|
|
301
434
|
}
|
|
302
435
|
}
|
|
303
436
|
},
|
|
@@ -306,10 +439,11 @@ watch(
|
|
|
306
439
|
watch(
|
|
307
440
|
() => modelValue.value,
|
|
308
441
|
(newValue) => {
|
|
309
|
-
if (maskInstance && newValue !== undefined && newValue !== maskInstance
|
|
442
|
+
if (maskInstance && newValue !== undefined && newValue !== getMaskEmittedValue(maskInstance)) {
|
|
310
443
|
// Only update if input doesn't have focus (user is not editing)
|
|
311
444
|
if (document.activeElement !== inputRef.value) {
|
|
312
445
|
maskInstance.value = newValue
|
|
446
|
+
nextTick(syncMaskDisplay)
|
|
313
447
|
}
|
|
314
448
|
}
|
|
315
449
|
},
|
|
@@ -335,12 +469,11 @@ watch(
|
|
|
335
469
|
</FdsMeta>
|
|
336
470
|
</div>
|
|
337
471
|
<div :class="{ 'flex-1': props.labelLeft }">
|
|
338
|
-
<div class="relative">
|
|
472
|
+
<div class="relative" :class="maskFieldWrapperClasses">
|
|
339
473
|
<input
|
|
340
474
|
ref="inputRef"
|
|
341
475
|
:type="inputType === 'password' ? (showPassword ? 'text' : 'password') : inputType"
|
|
342
476
|
:required="props.required"
|
|
343
|
-
:value="internalValue"
|
|
344
477
|
:disabled="props.disabled"
|
|
345
478
|
:tabindex="props.disabled ? -1 : undefined"
|
|
346
479
|
:aria-invalid="props.valid === false ? 'true' : undefined"
|
|
@@ -353,7 +486,7 @@ watch(
|
|
|
353
486
|
:placeholder="props.placeholder"
|
|
354
487
|
:pattern="props.pattern"
|
|
355
488
|
:searchIcon="props.searchIcon"
|
|
356
|
-
v-bind="
|
|
489
|
+
v-bind="inputBindings"
|
|
357
490
|
@input="handleInputChange"
|
|
358
491
|
@change="handleInputChange"
|
|
359
492
|
@blur="$emit('blur', $event)"
|
|
@@ -364,6 +497,13 @@ watch(
|
|
|
364
497
|
}
|
|
365
498
|
"
|
|
366
499
|
/>
|
|
500
|
+
<div v-if="useMaskPlaceholderStyle" :class="maskOverlayClasses" aria-hidden="true">
|
|
501
|
+
<span class="whitespace-pre text-base leading-6 tabular-nums">
|
|
502
|
+
<span v-for="(item, index) in maskDisplayChars" :key="index" :style="maskCharStyle(item.isPlaceholder)">
|
|
503
|
+
{{ item.char }}
|
|
504
|
+
</span>
|
|
505
|
+
</span>
|
|
506
|
+
</div>
|
|
367
507
|
<div :class="rightIconsContainerClasses">
|
|
368
508
|
<FdsButtonIcon
|
|
369
509
|
v-if="showDateIcon"
|
|
@@ -9,6 +9,8 @@ export interface FdsInputProps {
|
|
|
9
9
|
labelLeft?: boolean
|
|
10
10
|
clearButton?: boolean
|
|
11
11
|
mask?: string | RegExp | Array<string | RegExp>
|
|
12
|
+
/** Character shown for unfilled mask positions while typing (maps to IMask placeholderChar). */
|
|
13
|
+
displayChar?: string
|
|
12
14
|
modelValue?: string
|
|
13
15
|
value?: string
|
|
14
16
|
type?: string
|
|
@@ -139,7 +139,7 @@ function onNoCountryResults(value: boolean) {
|
|
|
139
139
|
<div v-if="meta" class="font-thin mb-1">
|
|
140
140
|
{{ meta }}
|
|
141
141
|
</div>
|
|
142
|
-
<div class="flex flex-wrap items-start gap-x-
|
|
142
|
+
<div class="flex flex-wrap items-start gap-x-2">
|
|
143
143
|
<FdsPhonenumberCountryPicker
|
|
144
144
|
v-model="country"
|
|
145
145
|
:items="countryItems"
|
|
@@ -377,7 +377,7 @@ onBeforeUnmount(() => {
|
|
|
377
377
|
v-if="dropdownOpen && !disabled && filteredCountries.length > 0"
|
|
378
378
|
role="listbox"
|
|
379
379
|
:id="listboxId"
|
|
380
|
-
class="absolute left-0 z-20 mt-
|
|
380
|
+
class="absolute left-0 z-20 mt-2 min-w-[20rem] w-max max-w-[calc(100vw-2rem)] max-h-[280px] overflow-y-auto rounded-md border border-gray-300 bg-white"
|
|
381
381
|
v-bind="listDataAttrs"
|
|
382
382
|
@mouseleave="clearHoverPreview"
|
|
383
383
|
>
|