fds-vue-core 8.2.3 → 8.3.0

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.
Files changed (36) hide show
  1. package/components.d.ts +2 -0
  2. package/dist/components/FdsSearchSelect/FdsSearchSelect.vue.d.ts +3 -5
  3. package/dist/components/FdsSearchSelectPro/FdsSearchSelectPro.vue.d.ts +3 -5
  4. package/dist/components/Form/FdsInput/FdsInput.vue.d.ts +2 -3
  5. package/dist/components/Form/FdsInput/types.d.ts +0 -2
  6. package/dist/components/Form/FdsPhonenumber/FdsPhonenumber.vue.d.ts +0 -3
  7. package/dist/components/Form/FdsPhonenumber/normalizePhoneInput.d.ts +7 -0
  8. package/dist/components/Form/FdsPhonenumber/phoneMask.d.ts +2 -0
  9. package/dist/components/Form/FdsPhonenumber/types.d.ts +1 -1
  10. package/dist/components/Form/FdsSsn/FdsSsn.vue.d.ts +33 -0
  11. package/dist/components/Form/FdsSsn/ssnMask.d.ts +19 -0
  12. package/dist/components/Form/FdsSsn/types.d.ts +24 -0
  13. package/dist/fds-vue-core.cjs.js +937 -448
  14. package/dist/fds-vue-core.cjs.js.map +1 -1
  15. package/dist/fds-vue-core.css +1 -1
  16. package/dist/fds-vue-core.es.js +938 -449
  17. package/dist/fds-vue-core.es.js.map +1 -1
  18. package/dist/helpers/validateSsn.d.ts +16 -0
  19. package/dist/index.d.ts +5 -1
  20. package/dist/tsconfig.build.tsbuildinfo +1 -1
  21. package/package.json +2 -1
  22. package/src/components/Form/FdsInput/FdsInput.stories.ts +1 -7
  23. package/src/components/Form/FdsInput/FdsInput.vue +41 -163
  24. package/src/components/Form/FdsInput/types.ts +0 -2
  25. package/src/components/Form/FdsPhonenumber/FdsPhonenumber.vue +30 -9
  26. package/src/components/Form/FdsPhonenumber/normalizePhoneInput.ts +88 -0
  27. package/src/components/Form/FdsPhonenumber/phoneMask.ts +28 -0
  28. package/src/components/Form/FdsPhonenumber/types.ts +0 -3
  29. package/src/components/Form/FdsSsn/FdsSsn.stories.ts +131 -0
  30. package/src/components/Form/FdsSsn/FdsSsn.vue +133 -0
  31. package/src/components/Form/FdsSsn/ssnMask.ts +20 -0
  32. package/src/components/Form/FdsSsn/types.ts +36 -0
  33. package/src/helpers/validateSsn.ts +63 -0
  34. package/src/index.ts +16 -0
  35. package/src/lang/en.json +2 -0
  36. package/src/lang/sv.json +2 -0
@@ -0,0 +1,131 @@
1
+ import type { Meta, StoryObj } from '@storybook/vue3'
2
+ import { ref } from 'vue'
3
+ import FdsSsn from './FdsSsn.vue'
4
+
5
+ const meta: Meta<typeof FdsSsn> = {
6
+ title: 'FDS/Form/FdsSsn',
7
+ component: FdsSsn,
8
+ tags: ['autodocs'],
9
+ argTypes: {
10
+ label: { control: 'text' },
11
+ meta: { control: 'text' },
12
+ disabled: { control: 'boolean' },
13
+ optional: { control: 'boolean' },
14
+ valid: { control: 'select', options: [undefined, true, false, null] },
15
+ invalidMessage: { control: 'text' },
16
+ allowCoordinationNumber: { control: 'boolean' },
17
+ allowInterimNumber: { control: 'boolean' },
18
+ },
19
+ args: {
20
+ label: 'Personnummer',
21
+ meta: '',
22
+ disabled: false,
23
+ optional: false,
24
+ valid: undefined,
25
+ invalidMessage: 'Ange ett giltigt personnummer',
26
+ allowCoordinationNumber: true,
27
+ allowInterimNumber: true,
28
+ },
29
+ }
30
+
31
+ export default meta
32
+ type Story = StoryObj<typeof meta>
33
+
34
+ export const Default: Story = {
35
+ render: (args) => ({
36
+ components: { FdsSsn },
37
+ setup() {
38
+ const ssn = ref('')
39
+ const ssnValid = ref<boolean | null>(null)
40
+ return { args, ssn, ssnValid }
41
+ },
42
+ template: `
43
+ <div>
44
+ <FdsSsn
45
+ v-bind="args"
46
+ v-model="ssn"
47
+ @valid="ssnValid = $event"
48
+ />
49
+ <p class="mt-4 text-sm">v-model: {{ ssn || '—' }}</p>
50
+ <p class="text-sm">valid: {{ ssnValid }}</p>
51
+ </div>
52
+ `,
53
+ }),
54
+ }
55
+
56
+ export const InvalidValue: Story = {
57
+ args: {
58
+ valid: false,
59
+ },
60
+ render: (args) => ({
61
+ components: { FdsSsn },
62
+ setup() {
63
+ const ssn = ref('198501011234')
64
+ return { args, ssn }
65
+ },
66
+ template: `
67
+ <FdsSsn v-bind="args" v-model="ssn" />
68
+ `,
69
+ }),
70
+ }
71
+
72
+ export const ValidPersonnummer: Story = {
73
+ render: (args) => ({
74
+ components: { FdsSsn },
75
+ setup() {
76
+ const ssn = ref('198507099805')
77
+ return { args, ssn }
78
+ },
79
+ template: `
80
+ <FdsSsn v-bind="args" v-model="ssn" />
81
+ `,
82
+ }),
83
+ }
84
+
85
+ export const CoordinationNumber: Story = {
86
+ args: {
87
+ allowCoordinationNumber: true,
88
+ },
89
+ render: (args) => ({
90
+ components: { FdsSsn },
91
+ setup() {
92
+ const ssn = ref('198506698805')
93
+ const ssnValid = ref<boolean | null>(null)
94
+ return { args, ssn, ssnValid }
95
+ },
96
+ template: `
97
+ <div>
98
+ <FdsSsn
99
+ v-bind="args"
100
+ v-model="ssn"
101
+ @valid="ssnValid = $event"
102
+ />
103
+ <p class="mt-4 text-sm">valid: {{ ssnValid }}</p>
104
+ </div>
105
+ `,
106
+ }),
107
+ }
108
+
109
+ export const InterimNumber: Story = {
110
+ args: {
111
+ allowInterimNumber: true,
112
+ },
113
+ render: (args) => ({
114
+ components: { FdsSsn },
115
+ setup() {
116
+ const ssn = ref('19850709T125')
117
+ const ssnValid = ref<boolean | null>(null)
118
+ return { args, ssn, ssnValid }
119
+ },
120
+ template: `
121
+ <div>
122
+ <FdsSsn
123
+ v-bind="args"
124
+ v-model="ssn"
125
+ @valid="ssnValid = $event"
126
+ />
127
+ <p class="mt-4 text-sm">valid: {{ ssnValid }}</p>
128
+ </div>
129
+ `,
130
+ }),
131
+ }
@@ -0,0 +1,133 @@
1
+ <script setup lang="ts">
2
+ import { computed, ref, useAttrs, watch } from 'vue'
3
+ import { useFdsI18n } from '../../../plugin/useFdsI18n'
4
+ import FdsInput from '../FdsInput/FdsInput.vue'
5
+ import { getSsnMask, getSsnMaskOptions } from './ssnMask'
6
+ import type { FdsSsnEmits, FdsSsnProps } from './types'
7
+ import { getSsnValidationState, stripSsnSeparators, type SsnValidationOptions } from '../../../helpers/validateSsn'
8
+
9
+ defineOptions({
10
+ inheritAttrs: false,
11
+ })
12
+
13
+ const modelValue = defineModel<string>({ default: '' })
14
+ const attrs = useAttrs()
15
+
16
+ const props = withDefaults(defineProps<FdsSsnProps>(), {
17
+ label: undefined,
18
+ meta: undefined,
19
+ optional: false,
20
+ valid: undefined,
21
+ invalidMessage: undefined,
22
+ id: undefined,
23
+ autocomplete: 'off',
24
+ required: false,
25
+ placeholder: undefined,
26
+ name: undefined,
27
+ autofocus: false,
28
+ readonly: false,
29
+ allowCoordinationNumber: true,
30
+ allowInterimNumber: true,
31
+ disabled: false,
32
+ dataTestid: undefined,
33
+ inputClass: undefined,
34
+ })
35
+
36
+ const emit = defineEmits<FdsSsnEmits>()
37
+
38
+ const { t } = useFdsI18n()
39
+
40
+ const inputAttrs = computed(() => {
41
+ const { class: _class, style, ...rest } = attrs
42
+ return {
43
+ ...rest,
44
+ ...(style == null ? {} : { style: style as string | Record<string, unknown> }),
45
+ }
46
+ })
47
+
48
+ const forwardedInputProps = computed(() => ({
49
+ id: props.id,
50
+ autocomplete: props.autocomplete,
51
+ required: props.required,
52
+ placeholder: props.placeholder,
53
+ name: props.name,
54
+ autofocus: props.autofocus,
55
+ readonly: props.readonly,
56
+ inputmode: props.allowInterimNumber ? 'text' : 'numeric',
57
+ ...inputAttrs.value,
58
+ }))
59
+
60
+ const validationOptions = computed(
61
+ (): SsnValidationOptions => ({
62
+ allowCoordinationNumber: props.allowCoordinationNumber,
63
+ allowInterimNumber: props.allowInterimNumber,
64
+ }),
65
+ )
66
+
67
+ const ssnMask = computed(() => getSsnMask(props.allowInterimNumber))
68
+ const ssnMaskOptions = computed(() => getSsnMaskOptions(props.allowInterimNumber))
69
+
70
+ /** Set after blur; validation does not run on input. */
71
+ const committedValid = ref<boolean | null>(null)
72
+
73
+ const resolvedLabel = computed(() => (props.label === undefined ? t('FdsSsn.label') : props.label))
74
+
75
+ const resolvedInvalidMessage = computed(() =>
76
+ props.invalidMessage === undefined ? t('FdsSsn.invalidSsn') : props.invalidMessage,
77
+ )
78
+
79
+ const displayValid = computed((): boolean | null | undefined => {
80
+ if (props.valid === false) return false
81
+ if (props.valid === true) return true
82
+ if (committedValid.value === false) return false
83
+ if (committedValid.value === true) return true
84
+ return props.valid
85
+ })
86
+
87
+ function normalizeValue() {
88
+ const normalized = stripSsnSeparators(modelValue.value ?? '')
89
+ if (normalized !== (modelValue.value ?? '')) {
90
+ modelValue.value = normalized
91
+ }
92
+ }
93
+
94
+ function runValidation() {
95
+ const validationState = getSsnValidationState(modelValue.value ?? '', validationOptions.value)
96
+ committedValid.value = validationState
97
+ emit('valid', validationState)
98
+ }
99
+
100
+ watch([() => props.allowCoordinationNumber, () => props.allowInterimNumber], () => {
101
+ if (committedValid.value === null) {
102
+ return
103
+ }
104
+ runValidation()
105
+ })
106
+
107
+ function handleBlur(ev: FocusEvent) {
108
+ normalizeValue()
109
+ runValidation()
110
+ emit('blur', ev)
111
+ }
112
+ </script>
113
+
114
+ <template>
115
+ <div class="w-full">
116
+ <FdsInput
117
+ v-model="modelValue"
118
+ :mask="ssnMask"
119
+ :mask-options="ssnMaskOptions"
120
+ :label="resolvedLabel"
121
+ :meta="meta"
122
+ :valid="displayValid"
123
+ :disabled="disabled"
124
+ :optional="optional"
125
+ :invalid-message="resolvedInvalidMessage"
126
+ :ariaLabel="resolvedLabel"
127
+ :data-testid="dataTestid"
128
+ :class="inputClass"
129
+ v-bind="forwardedInputProps"
130
+ @blur="handleBlur"
131
+ />
132
+ </div>
133
+ </template>
@@ -0,0 +1,20 @@
1
+ /** IMask pattern for ÅÅÅÅMMDDKKKK (12 digits, no separator). */
2
+ export const SSN_MASK = '000000000000'
3
+
4
+ /** Alphanumeric mask when interim numbers (letters in num part) are allowed. */
5
+ export const SSN_INTERIM_MASK = 'XXXXXXXXXXXX'
6
+
7
+ export const SSN_INTERIM_MASK_OPTIONS = {
8
+ lazy: true,
9
+ definitions: {
10
+ X: /[0-9A-Za-z]/,
11
+ },
12
+ } as const
13
+
14
+ export function getSsnMask(allowInterimNumber: boolean): string {
15
+ return allowInterimNumber ? SSN_INTERIM_MASK : SSN_MASK
16
+ }
17
+
18
+ export function getSsnMaskOptions(allowInterimNumber: boolean) {
19
+ return allowInterimNumber ? SSN_INTERIM_MASK_OPTIONS : { lazy: true }
20
+ }
@@ -0,0 +1,36 @@
1
+ import type { FdsInputProps } from '../FdsInput/types'
2
+
3
+ type FdsSsnInputPassthroughProps = Pick<
4
+ FdsInputProps,
5
+ | 'id'
6
+ | 'autocomplete'
7
+ | 'required'
8
+ | 'placeholder'
9
+ | 'name'
10
+ | 'autofocus'
11
+ | 'readonly'
12
+ | 'inputClass'
13
+ >
14
+
15
+ export interface FdsSsnProps extends FdsSsnInputPassthroughProps {
16
+ label?: string
17
+ meta?: string
18
+ optional?: boolean
19
+ valid?: boolean | null
20
+ invalidMessage?: string
21
+ /** Personnummer as shown in the input (12 characters, no separator). */
22
+ modelValue?: string
23
+ allowCoordinationNumber?: boolean
24
+ allowInterimNumber?: boolean
25
+ disabled?: boolean
26
+ dataTestid?: string
27
+ onValid?: ((value: boolean | null) => void) | Array<(value: boolean | null) => void>
28
+ onBlur?: ((event: FocusEvent) => void) | Array<(event: FocusEvent) => void>
29
+ 'onUpdate:modelValue'?: ((value: string) => void) | Array<(value: string) => void>
30
+ }
31
+
32
+ export interface FdsSsnEmits {
33
+ 'update:modelValue': [value: string]
34
+ valid: [value: boolean | null]
35
+ blur: [ev: FocusEvent]
36
+ }
@@ -0,0 +1,63 @@
1
+ import Personnummer from 'personnummer'
2
+ import { normalizePidSearchValue } from '../composables/useIsPid'
3
+
4
+ export interface SsnValidationOptions {
5
+ allowCoordinationNumber?: boolean
6
+ allowInterimNumber?: boolean
7
+ }
8
+
9
+ export interface SsnValidationResult {
10
+ isValid: boolean
11
+ /** Normalized value without separators; `null` when empty or invalid. */
12
+ digits: string | null
13
+ }
14
+
15
+ const INVALID_RESULT: SsnValidationResult = {
16
+ isValid: false,
17
+ digits: null,
18
+ }
19
+
20
+ const DEFAULT_SSN_VALIDATION_OPTIONS = {
21
+ allowCoordinationNumber: true,
22
+ allowInterimNumber: true,
23
+ } as const
24
+
25
+ function resolveSsnValidationOptions(options?: SsnValidationOptions) {
26
+ return {
27
+ ...DEFAULT_SSN_VALIDATION_OPTIONS,
28
+ ...options,
29
+ }
30
+ }
31
+
32
+ /** Strip separators; keeps alphanumeric characters for interim numbers. */
33
+ export function stripSsnSeparators(value: string): string {
34
+ return normalizePidSearchValue(value)
35
+ }
36
+
37
+ /** Validate a Swedish personal identity number. */
38
+ export function validateSsnNumber(value: string, options?: SsnValidationOptions): SsnValidationResult {
39
+ const trimmed = value.trim()
40
+ if (!trimmed) {
41
+ return INVALID_RESULT
42
+ }
43
+
44
+ const resolvedOptions = resolveSsnValidationOptions(options)
45
+ const isValid = Personnummer.valid(trimmed, resolvedOptions)
46
+
47
+ return {
48
+ isValid,
49
+ digits: isValid ? stripSsnSeparators(trimmed) : null,
50
+ }
51
+ }
52
+
53
+ export function validateSsn(value: string, options?: SsnValidationOptions): boolean {
54
+ return validateSsnNumber(value, options).isValid
55
+ }
56
+
57
+ /** `null` when empty, otherwise whether the value is a valid personnummer. */
58
+ export function getSsnValidationState(value: string, options?: SsnValidationOptions): boolean | null {
59
+ if (!value.trim()) {
60
+ return null
61
+ }
62
+ return validateSsn(value, options)
63
+ }
package/src/index.ts CHANGED
@@ -31,6 +31,7 @@ import FdsWizard from './components/FdsWizard/FdsWizard.vue'
31
31
  import FdsCheckbox from './components/Form/FdsCheckbox/FdsCheckbox.vue'
32
32
  import FdsInput from './components/Form/FdsInput/FdsInput.vue'
33
33
  import FdsPhonenumber from './components/Form/FdsPhonenumber/FdsPhonenumber.vue'
34
+ import FdsSsn from './components/Form/FdsSsn/FdsSsn.vue'
34
35
  import FdsRadio from './components/Form/FdsRadio/FdsRadio.vue'
35
36
  import FdsSelect from './components/Form/FdsSelect/FdsSelect.vue'
36
37
  import FdsTextarea from './components/Form/FdsTextarea/FdsTextarea.vue'
@@ -113,6 +114,7 @@ export {
113
114
  FdsModal,
114
115
  FdsPagination,
115
116
  FdsPhonenumber,
117
+ FdsSsn,
116
118
  FdsPopover,
117
119
  FdsRadio,
118
120
  FdsSearchSelect,
@@ -196,6 +198,7 @@ const FdsVueCorePlugin: Plugin = {
196
198
  app.component('FdsCheckbox', FdsCheckbox)
197
199
  app.component('FdsTextarea', FdsTextarea)
198
200
  app.component('FdsPhonenumber', FdsPhonenumber)
201
+ app.component('FdsSsn', FdsSsn)
199
202
  app.component('FdsSelect', FdsSelect)
200
203
  app.component('FdsTable', FdsTable)
201
204
  app.component('FdsTableHead', FdsTableHead)
@@ -283,6 +286,19 @@ export {
283
286
 
284
287
  export type { FdsPhonenumberEmits, FdsPhonenumberProps } from './components/Form/FdsPhonenumber/types'
285
288
 
289
+ export {
290
+ getSsnValidationState,
291
+ stripSsnSeparators,
292
+ validateSsn,
293
+ validateSsnNumber,
294
+ type SsnValidationOptions,
295
+ type SsnValidationResult,
296
+ } from './helpers/validateSsn'
297
+
298
+ export { getSsnMask, getSsnMaskOptions, SSN_INTERIM_MASK, SSN_MASK } from './components/Form/FdsSsn/ssnMask'
299
+
300
+ export type { FdsSsnEmits, FdsSsnProps } from './components/Form/FdsSsn/types'
301
+
286
302
  // Table component types
287
303
  export type { FdsTableProps } from './components/Table/FdsTable/types'
288
304
 
package/src/lang/en.json CHANGED
@@ -50,6 +50,8 @@
50
50
  "FdsPhonenumber.invalidPhone": "Enter a valid phone number",
51
51
  "FdsPhonenumber.noCountryResults": "No country matches your search",
52
52
  "FdsPhonenumber.phoneNumber": "Phone number",
53
+ "FdsSsn.invalidSsn": "Enter a valid personal identity number",
54
+ "FdsSsn.label": "Personal identity number",
53
55
  "FdsSearchSelectPro.loadingMore": "Loading more...",
54
56
  "FdsSearchSelectPro.showMore": "Show more",
55
57
  "FdsSearchSelectPro.unspecified": "Unspecified",
package/src/lang/sv.json CHANGED
@@ -50,6 +50,8 @@
50
50
  "FdsPhonenumber.invalidPhone": "Ange ett giltigt telefonnummer",
51
51
  "FdsPhonenumber.noCountryResults": "Inget land matchar sökningen",
52
52
  "FdsPhonenumber.phoneNumber": "Telefonnummer",
53
+ "FdsSsn.invalidSsn": "Ange ett giltigt personnummer",
54
+ "FdsSsn.label": "Personnummer",
53
55
  "FdsSearchSelectPro.loadingMore": "Hämtar fler...",
54
56
  "FdsSearchSelectPro.showMore": "Visa fler",
55
57
  "FdsSearchSelectPro.unspecified": "Ospecificerat",