fds-vue-core 8.2.3 → 8.2.4

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fds-vue-core",
3
- "version": "8.2.3",
3
+ "version": "8.2.4",
4
4
  "description": "FDS Vue Core Component Library",
5
5
  "type": "module",
6
6
  "main": "./dist/fds-vue-core.cjs.js",
@@ -61,13 +61,9 @@ 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
- },
68
64
  maskOptions: {
69
65
  control: { type: 'object' },
70
- description: 'Additional IMask options. Overrides displayChar defaults when set.',
66
+ description: 'Additional IMask options (lazy defaults to true).',
71
67
  },
72
68
  maxlength: {
73
69
  control: { type: 'number' },
@@ -87,7 +83,6 @@ const meta: Meta<typeof FdsInput> = {
87
83
  required: false,
88
84
  type: 'text',
89
85
  mask: undefined,
90
- displayChar: undefined,
91
86
  maskOptions: undefined,
92
87
  maxlength: undefined,
93
88
  },
@@ -320,7 +315,6 @@ export const WithMaskPostalCode: Story = {
320
315
  v-model="postalCode"
321
316
  label="Postnummer"
322
317
  mask="000 00"
323
- display-char="N"
324
318
  />
325
319
  <p class="mt-4">Värde: {{ postalCode }}</p>
326
320
  </div>
@@ -27,7 +27,6 @@ const props = withDefaults(defineProps<FdsInputProps>(), {
27
27
  meta: undefined,
28
28
  clearButton: false,
29
29
  mask: undefined,
30
- displayChar: undefined,
31
30
  maskOptions: undefined,
32
31
  class: undefined,
33
32
  pattern: undefined,
@@ -39,7 +38,7 @@ const props = withDefaults(defineProps<FdsInputProps>(), {
39
38
 
40
39
  const emit = defineEmits<{
41
40
  (e: 'input', ev: Event): void
42
- (e: 'change', ev: Event): void
41
+ (e: 'autocomplete', ev: Event): void
43
42
  (e: 'clearInput'): void
44
43
  (e: 'update:value', value: string): void
45
44
  (e: 'keyup', ev: KeyboardEvent): void
@@ -143,108 +142,33 @@ const inputPaddingRight = computed(() => {
143
142
 
144
143
  const externalClass = computed(() => (props.class ?? attrs.class) as HTMLAttributes['class'] | undefined)
145
144
 
146
- const maskDisplayValue = ref('')
147
- const maskPlaceholderChar = ref('')
145
+ const inputPaddingClasses = 'px-3 py-[calc(0.75rem-1px)]'
148
146
 
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,
147
+ const inputClasses = computed(() => [
148
+ 'block rounded-md border border-gray-500',
149
+ maxlength.value ? '' : 'w-full',
150
+ inputPaddingClasses,
174
151
  inputPaddingRight.value,
152
+ 'focus:outline-2 focus:outline-blue-500 -outline-offset-2 focus:border-transparent',
153
+ showDateIcon.value &&
154
+ '[&::-webkit-calendar-picker-indicator]:opacity-0 [&::-webkit-calendar-picker-indicator]:pointer-events-none',
155
+ props.disabled
156
+ ? 'text-gray-800 outline-dashed outline-2 outline-gray-400 cursor-not-allowed border-transparent'
157
+ : 'text-gray-900 bg-white',
158
+ isInvalid.value && 'outline-2 outline-red-600',
159
+ props.inputClass,
175
160
  ])
176
161
 
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
-
226
162
  const inputStyle = computed(() => {
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
-
163
+ if (!maxlength.value) return undefined
238
164
  return {
239
- ...size,
240
- color: 'transparent',
241
- WebkitTextFillColor: 'transparent',
242
- caretColor: props.disabled ? '#1f2937' : '#111827',
165
+ width: `calc(${maxlength.value}ch + 1.5rem + 0.25rem)`,
166
+ maxWidth: '100%',
243
167
  }
244
168
  })
245
169
 
246
170
  const rightIconsContainerClasses = computed(() => [
247
- 'absolute z-10 flex gap-2 top-1/2 -translate-y-1/2 items-center justify-end',
171
+ 'absolute flex gap-2 top-1/2 -translate-y-1/2 items-center justify-end',
248
172
  showClearButton.value ? 'right-2' : 'right-3',
249
173
  ])
250
174
 
@@ -268,7 +192,6 @@ function onClear() {
268
192
  internalValue.value = ''
269
193
  if (maskInstance) {
270
194
  maskInstance.value = ''
271
- nextTick(syncMaskDisplay)
272
195
  }
273
196
  emit('clearInput')
274
197
  }
@@ -310,74 +233,28 @@ const inputRef = ref<HTMLInputElement | null>(null)
310
233
 
311
234
  let maskInstance: any = null
312
235
 
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
-
336
236
  const createMask = () => {
337
- // Destroy existing mask if any
338
237
  if (maskInstance) {
339
238
  maskInstance.destroy()
340
239
  maskInstance = null
341
240
  }
342
241
 
343
- // Create new mask if mask prop is provided and input exists
344
242
  if (props.mask && inputRef.value) {
345
- const maskConfig = {
243
+ maskInstance = IMask(inputRef.value, {
346
244
  mask: props.mask,
347
245
  ...props.maskOptions,
348
- ...(props.displayChar
349
- ? {
350
- placeholderChar: props.displayChar,
351
- lazy: props.maskOptions?.lazy ?? false,
352
- }
353
- : {}),
354
- }
355
-
356
- maskInstance = IMask(inputRef.value, maskConfig as any)
246
+ lazy: props.maskOptions?.lazy ?? true,
247
+ } as any)
357
248
 
358
- // Sync mask value with internalValue
359
249
  if (internalValue.value) {
360
250
  maskInstance.value = internalValue.value
361
251
  }
362
252
 
363
253
  maskInstance.on('accept', () => {
364
254
  if (maskInstance) {
365
- internalValue.value = getMaskEmittedValue(maskInstance)
366
- nextTick(syncMaskDisplay)
255
+ internalValue.value = maskInstance.value
367
256
  }
368
257
  })
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 = ''
381
258
  }
382
259
  }
383
260
 
@@ -412,7 +289,7 @@ const ariaDescribedBy = computed(() => {
412
289
 
413
290
  // Watch for mask-related prop changes and recreate mask
414
291
  watch(
415
- () => [props.mask, props.displayChar, props.maskOptions] as const,
292
+ () => [props.mask, props.maskOptions] as const,
416
293
  () => {
417
294
  nextTick(() => {
418
295
  createMask()
@@ -421,16 +298,25 @@ watch(
421
298
  { deep: true },
422
299
  )
423
300
 
301
+ function shouldSyncMaskFromModel(newValue: string | undefined, currentMaskedValue: string): boolean {
302
+ if (newValue === undefined) {
303
+ return false
304
+ }
305
+ if (document.activeElement !== inputRef.value) {
306
+ return true
307
+ }
308
+ const newDigits = String(newValue).replace(/\D/g, '')
309
+ const currentDigits = String(currentMaskedValue ?? '').replace(/\D/g, '')
310
+ return newDigits !== currentDigits
311
+ }
312
+
424
313
  // Watch for external value changes and update mask
425
- // Only update if the value actually changed and user is not actively editing
426
314
  watch(
427
315
  () => props.value,
428
316
  (newValue) => {
429
- if (maskInstance && newValue !== undefined && newValue !== getMaskEmittedValue(maskInstance)) {
430
- // Only update if input doesn't have focus (user is not editing)
431
- if (document.activeElement !== inputRef.value) {
317
+ if (maskInstance && newValue !== undefined && newValue !== maskInstance.value) {
318
+ if (shouldSyncMaskFromModel(newValue, maskInstance.value)) {
432
319
  maskInstance.value = newValue
433
- nextTick(syncMaskDisplay)
434
320
  }
435
321
  }
436
322
  },
@@ -439,11 +325,9 @@ watch(
439
325
  watch(
440
326
  () => modelValue.value,
441
327
  (newValue) => {
442
- if (maskInstance && newValue !== undefined && newValue !== getMaskEmittedValue(maskInstance)) {
443
- // Only update if input doesn't have focus (user is not editing)
444
- if (document.activeElement !== inputRef.value) {
328
+ if (maskInstance && newValue !== undefined && newValue !== maskInstance.value) {
329
+ if (shouldSyncMaskFromModel(newValue, maskInstance.value)) {
445
330
  maskInstance.value = newValue
446
- nextTick(syncMaskDisplay)
447
331
  }
448
332
  }
449
333
  },
@@ -469,7 +353,7 @@ watch(
469
353
  </FdsMeta>
470
354
  </div>
471
355
  <div :class="{ 'flex-1': props.labelLeft }">
472
- <div class="relative" :class="maskFieldWrapperClasses">
356
+ <div class="relative">
473
357
  <input
474
358
  ref="inputRef"
475
359
  :type="inputType === 'password' ? (showPassword ? 'text' : 'password') : inputType"
@@ -489,6 +373,7 @@ watch(
489
373
  v-bind="inputBindings"
490
374
  @input="handleInputChange"
491
375
  @change="handleInputChange"
376
+ @autocomplete="$emit('autocomplete', $event)"
492
377
  @blur="$emit('blur', $event)"
493
378
  @keyup="
494
379
  (ev) => {
@@ -497,13 +382,6 @@ watch(
497
382
  }
498
383
  "
499
384
  />
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>
507
385
  <div :class="rightIconsContainerClasses">
508
386
  <FdsButtonIcon
509
387
  v-if="showDateIcon"
@@ -9,8 +9,6 @@ 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
14
12
  modelValue?: string
15
13
  value?: string
16
14
  type?: string
@@ -3,6 +3,8 @@ import { computed, ref, useAttrs, watch } from 'vue'
3
3
  import { useFdsI18n } from '../../../plugin/useFdsI18n'
4
4
  import FdsInput from '../FdsInput/FdsInput.vue'
5
5
  import { buildCountryOptions, sortCountryOptionsByName } from './countries'
6
+ import { getPhoneMaskForCountry } from './phoneMask'
7
+ import { normalizeNationalPhoneInput, shouldNormalizePhoneInput } from './normalizePhoneInput'
6
8
  import FdsPhonenumberCountryPicker from './FdsPhonenumberCountryPicker.vue'
7
9
  import type { FdsPhonenumberEmits, FdsPhonenumberProps } from './types'
8
10
  import { getPhoneValidationState, validatePhoneNumber } from './validatePhone'
@@ -11,7 +13,7 @@ defineOptions({
11
13
  inheritAttrs: false,
12
14
  })
13
15
 
14
- const [nationalNumber, modelModifiers] = defineModel<string>({ default: '' })
16
+ const [nationalNumber] = defineModel<string>({ default: '' })
15
17
  const country = defineModel<string>('country', { default: 'SE' })
16
18
  const attrs = useAttrs()
17
19
 
@@ -25,12 +27,9 @@ const props = withDefaults(defineProps<FdsPhonenumberProps>(), {
25
27
  autocomplete: 'tel',
26
28
  required: false,
27
29
  placeholder: undefined,
28
- maxlength: undefined,
29
- minlength: undefined,
30
30
  name: undefined,
31
31
  autofocus: false,
32
32
  readonly: false,
33
- pattern: undefined,
34
33
  defaultCountry: 'SE',
35
34
  countries: undefined,
36
35
  locale: undefined,
@@ -57,12 +56,10 @@ const forwardedInputProps = computed(() => ({
57
56
  autocomplete: props.autocomplete,
58
57
  required: props.required,
59
58
  placeholder: props.placeholder,
60
- maxlength: props.maxlength,
61
- minlength: props.minlength,
62
59
  name: props.name,
63
60
  autofocus: props.autofocus,
64
61
  readonly: props.readonly,
65
- pattern: props.pattern,
62
+ inputmode: 'tel',
66
63
  ...inputAttrs.value,
67
64
  }))
68
65
 
@@ -77,6 +74,12 @@ const countryItems = computed(() => {
77
74
  )
78
75
  })
79
76
 
77
+ const phoneMask = computed(() => getPhoneMaskForCountry(country.value ?? props.defaultCountry))
78
+
79
+ const phoneMaskOptions = computed(() => ({
80
+ prepare: (value: string) => applyPhoneNormalization(value, country.value ?? props.defaultCountry),
81
+ }))
82
+
80
83
  /** Set after the phone number field has blurred; validation does not run on input. */
81
84
  const committedValid = ref<boolean | null>(null)
82
85
 
@@ -109,6 +112,20 @@ function runValidation() {
109
112
  emit('update:e164', result.isValid && result.phoneNumber ? result.phoneNumber : '')
110
113
  }
111
114
 
115
+ function applyPhoneNormalization(value: string, countryCode = country.value ?? props.defaultCountry): string {
116
+ if (!shouldNormalizePhoneInput(value, countryCode)) {
117
+ return value
118
+ }
119
+ return normalizeNationalPhoneInput(value, countryCode)
120
+ }
121
+
122
+ watch(nationalNumber, (value) => {
123
+ const normalized = applyPhoneNormalization(value ?? '')
124
+ if (normalized !== (value ?? '')) {
125
+ nationalNumber.value = normalized
126
+ }
127
+ })
128
+
112
129
  watch(country, () => {
113
130
  if (committedValid.value === null) {
114
131
  return
@@ -117,6 +134,10 @@ watch(country, () => {
117
134
  })
118
135
 
119
136
  function handleBlur(ev: FocusEvent) {
137
+ const normalized = applyPhoneNormalization(nationalNumber.value ?? '')
138
+ if (normalized !== (nationalNumber.value ?? '')) {
139
+ nationalNumber.value = normalized
140
+ }
120
141
  runValidation()
121
142
  emit('blur', ev)
122
143
  }
@@ -152,8 +173,8 @@ function onNoCountryResults(value: boolean) {
152
173
  />
153
174
  <FdsInput
154
175
  v-model="nationalNumber"
155
- :modelModifiers="modelModifiers"
156
- type="tel"
176
+ :mask="phoneMask"
177
+ :mask-options="phoneMaskOptions"
157
178
  :valid="displayValid"
158
179
  :disabled="disabled"
159
180
  :optional="optional"
@@ -0,0 +1,88 @@
1
+ import parsePhoneNumberFromString, { getCountryCallingCode, type CountryCode } from 'libphonenumber-js/mobile/es6'
2
+ import { getPhoneMaskForCountry } from './phoneMask'
3
+
4
+ const nationalDigitsForMask = (parsed: NonNullable<ReturnType<typeof parsePhoneNumberFromString>>): string =>
5
+ parsed.formatNational().replace(/\D/g, '')
6
+
7
+ const tryParseForCountry = (candidate: string, country: CountryCode): ReturnType<typeof parsePhoneNumberFromString> => {
8
+ const parsed = parsePhoneNumberFromString(candidate, country) ?? parsePhoneNumberFromString(candidate)
9
+ if (!parsed?.isValid() || parsed.country !== country) {
10
+ return undefined
11
+ }
12
+ return parsed
13
+ }
14
+
15
+ /** Whether the value likely contains a country calling code or international formatting. */
16
+ export const shouldNormalizePhoneInput = (value: string, countryIso2: string): boolean => {
17
+ const trimmed = value.trim()
18
+ if (!trimmed) {
19
+ return false
20
+ }
21
+ if (trimmed.includes('+') || trimmed.startsWith('00')) {
22
+ return true
23
+ }
24
+
25
+ const digits = trimmed.replace(/\D/g, '')
26
+ const mask = getPhoneMaskForCountry(countryIso2)
27
+ if (!mask) {
28
+ return false
29
+ }
30
+
31
+ const maxDigits = (mask.match(/0/g) ?? []).length
32
+ if (digits.length > maxDigits) {
33
+ return true
34
+ }
35
+
36
+ try {
37
+ const callingCode = getCountryCallingCode(countryIso2 as CountryCode)
38
+ if (digits.startsWith(callingCode) && digits.length > maxDigits - 1) {
39
+ return true
40
+ }
41
+ } catch {
42
+ return false
43
+ }
44
+
45
+ return false
46
+ }
47
+
48
+ /**
49
+ * Converts international/autocomplete values to national digits for the IMask field.
50
+ * IMask reapplies spacing from the country mask.
51
+ */
52
+ export const normalizeNationalPhoneInput = (value: string, countryIso2: string): string => {
53
+ const trimmed = value.trim()
54
+ if (!trimmed) {
55
+ return ''
56
+ }
57
+
58
+ const country = countryIso2.trim().toUpperCase() as CountryCode
59
+ const digits = trimmed.replace(/\D/g, '')
60
+
61
+ const candidates = new Set<string>([trimmed])
62
+ if (digits) {
63
+ candidates.add(digits)
64
+ candidates.add(`+${digits}`)
65
+ }
66
+
67
+ for (const candidate of candidates) {
68
+ const parsed = tryParseForCountry(candidate, country)
69
+ if (parsed) {
70
+ return nationalDigitsForMask(parsed)
71
+ }
72
+ }
73
+
74
+ try {
75
+ const callingCode = getCountryCallingCode(country)
76
+ if (digits.startsWith(callingCode) && digits.length > callingCode.length) {
77
+ const withoutCode = digits.slice(callingCode.length)
78
+ const parsed = tryParseForCountry(withoutCode, country) ?? tryParseForCountry(`0${withoutCode}`, country)
79
+ if (parsed) {
80
+ return nationalDigitsForMask(parsed)
81
+ }
82
+ }
83
+ } catch {
84
+ return trimmed
85
+ }
86
+
87
+ return trimmed
88
+ }
@@ -0,0 +1,28 @@
1
+ import { getExampleNumber, type CountryCode } from 'libphonenumber-js/mobile/es6'
2
+ import examples from 'libphonenumber-js/mobile/examples'
3
+
4
+ const maskCache = new Map<string, string>()
5
+
6
+ /** IMask pattern with space grouping derived from libphonenumber mobile example numbers. */
7
+ export const getPhoneMaskForCountry = (countryIso2: string): string | undefined => {
8
+ const country = countryIso2.trim().toUpperCase() as CountryCode
9
+ const cached = maskCache.get(country)
10
+ if (cached) {
11
+ return cached
12
+ }
13
+
14
+ const example = getExampleNumber(country, examples)
15
+ if (!example) {
16
+ return undefined
17
+ }
18
+
19
+ const groups = example
20
+ .formatNational()
21
+ .split(/\D+/)
22
+ .filter(Boolean)
23
+ .map((part) => part.length)
24
+
25
+ const mask = groups.map((size) => '0'.repeat(size)).join(' ')
26
+ maskCache.set(country, mask)
27
+ return mask
28
+ }
@@ -7,12 +7,9 @@ type FdsPhonenumberInputPassthroughProps = Pick<
7
7
  | 'autocomplete'
8
8
  | 'required'
9
9
  | 'placeholder'
10
- | 'maxlength'
11
- | 'minlength'
12
10
  | 'name'
13
11
  | 'autofocus'
14
12
  | 'readonly'
15
- | 'pattern'
16
13
  >
17
14
 
18
15
  export interface FdsPhonenumberProps extends FdsPhonenumberInputPassthroughProps {