fds-vue-core 7.2.1 β 7.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/Form/FdsPhonenumber/FdsPhonenumber.vue.d.ts +2 -0
- package/dist/components/Form/FdsPhonenumber/FdsPhonenumberCountryPicker.vue.d.ts +2 -3
- package/dist/components/Form/FdsPhonenumber/countries.d.ts +5 -3
- package/dist/components/Form/FdsPhonenumber/types.d.ts +1 -0
- package/dist/fds-vue-core.cjs.js +302 -130
- 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 +302 -130
- package/dist/fds-vue-core.es.js.map +1 -1
- package/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +18 -18
- package/src/components/FdsWizard/FdsWizard.vue +1 -1
- package/src/components/Form/FdsPhonenumber/FdsPhonenumber.stories.ts +1 -1
- package/src/components/Form/FdsPhonenumber/FdsPhonenumber.vue +16 -4
- package/src/components/Form/FdsPhonenumber/FdsPhonenumberCountryPicker.vue +256 -73
- package/src/components/Form/FdsPhonenumber/countries.ts +19 -3
- package/src/components/Form/FdsPhonenumber/types.ts +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fds-vue-core",
|
|
3
|
-
"version": "7.2.
|
|
3
|
+
"version": "7.2.3",
|
|
4
4
|
"description": "FDS Vue Core Component Library",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/fds-vue-core.cjs.js",
|
|
@@ -48,31 +48,31 @@
|
|
|
48
48
|
"vue": "^3.5.25"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"axios": "1.16.
|
|
52
|
-
"date-fns": "4.1
|
|
51
|
+
"axios": "1.16.1",
|
|
52
|
+
"date-fns": "4.2.1",
|
|
53
53
|
"imask": "7.6.1",
|
|
54
54
|
"libphonenumber-js": "1.12.36",
|
|
55
|
-
"tailwindcss": "4.
|
|
56
|
-
"vue-i18n": "11.
|
|
55
|
+
"tailwindcss": "4.3.0",
|
|
56
|
+
"vue-i18n": "11.4.4"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@chromatic-com/storybook": "5.
|
|
60
|
-
"@intlify/unplugin-vue-i18n": "11.
|
|
61
|
-
"@storybook/addon-a11y": "10.
|
|
62
|
-
"@storybook/addon-docs": "10.
|
|
63
|
-
"@storybook/addon-vitest": "10.
|
|
64
|
-
"@storybook/vue3": "10.
|
|
65
|
-
"@storybook/vue3-vite": "10.
|
|
66
|
-
"@tailwindcss/vite": "4.
|
|
59
|
+
"@chromatic-com/storybook": "5.2.1",
|
|
60
|
+
"@intlify/unplugin-vue-i18n": "11.2.3",
|
|
61
|
+
"@storybook/addon-a11y": "10.4.0",
|
|
62
|
+
"@storybook/addon-docs": "10.4.0",
|
|
63
|
+
"@storybook/addon-vitest": "10.4.0",
|
|
64
|
+
"@storybook/vue3": "10.4.0",
|
|
65
|
+
"@storybook/vue3-vite": "10.4.0",
|
|
66
|
+
"@tailwindcss/vite": "4.3.0",
|
|
67
67
|
"@types/node": "22.16.5",
|
|
68
|
-
"@vitejs/plugin-vue": "6.0.
|
|
68
|
+
"@vitejs/plugin-vue": "6.0.7",
|
|
69
69
|
"@vitest/browser": "3.2.4",
|
|
70
|
-
"fg-devkit": "1.8.
|
|
71
|
-
"storybook": "10.
|
|
72
|
-
"tsx": "4.
|
|
70
|
+
"fg-devkit": "1.8.5",
|
|
71
|
+
"storybook": "10.4.0",
|
|
72
|
+
"tsx": "4.22.3",
|
|
73
73
|
"vite": "7.3.2",
|
|
74
74
|
"vite-plugin-dts": "4.5.4",
|
|
75
|
-
"vite-plugin-vue-devtools": "8.1.
|
|
75
|
+
"vite-plugin-vue-devtools": "8.1.2",
|
|
76
76
|
"vitest": "3.2.4",
|
|
77
77
|
"vue": "3.5.34"
|
|
78
78
|
},
|
|
@@ -503,7 +503,7 @@ defineSlots<{
|
|
|
503
503
|
</span>
|
|
504
504
|
<span
|
|
505
505
|
:ref="(el) => setStepLabelRef(el, visibleIndex)"
|
|
506
|
-
class="absolute bottom-0 text-blue-600 font-bold text-sm"
|
|
506
|
+
class="absolute bottom-0 text-blue-600 font-bold text-sm whitespace-nowrap"
|
|
507
507
|
:class="{ invisible: !showStepLabels }"
|
|
508
508
|
>
|
|
509
509
|
{{ entry.route.meta.wizard.name }}
|
|
@@ -37,10 +37,13 @@ const { t, locale: fdsLocale } = useFdsI18n()
|
|
|
37
37
|
const resolvedLocale = computed(() => props.locale ?? fdsLocale.value)
|
|
38
38
|
|
|
39
39
|
const countryItems = computed(() => {
|
|
40
|
-
const options = props.countries?.length
|
|
40
|
+
const options = props.countries?.length
|
|
41
|
+
? props.countries
|
|
42
|
+
: buildCountryOptions(resolvedLocale.value, props.defaultCountry)
|
|
41
43
|
return sortCountryOptionsByName(
|
|
42
44
|
options.map((option) => ({ ...option })),
|
|
43
45
|
resolvedLocale.value,
|
|
46
|
+
props.defaultCountry,
|
|
44
47
|
)
|
|
45
48
|
})
|
|
46
49
|
|
|
@@ -61,6 +64,8 @@ const showInvalidMessage = computed(
|
|
|
61
64
|
() => displayValid.value === false && !props.optional && props.invalidMessage && !props.disabled,
|
|
62
65
|
)
|
|
63
66
|
|
|
67
|
+
const noCountryResults = ref(false)
|
|
68
|
+
|
|
64
69
|
function runValidation() {
|
|
65
70
|
const validationState = getPhoneValidationState(
|
|
66
71
|
nationalNumber.value ?? '',
|
|
@@ -85,6 +90,11 @@ function handleBlur(ev: FocusEvent) {
|
|
|
85
90
|
runValidation()
|
|
86
91
|
emit('blur', ev)
|
|
87
92
|
}
|
|
93
|
+
|
|
94
|
+
function onNoCountryResults(value: boolean) {
|
|
95
|
+
noCountryResults.value = value
|
|
96
|
+
emit('noCountryResults', value)
|
|
97
|
+
}
|
|
88
98
|
</script>
|
|
89
99
|
|
|
90
100
|
<template>
|
|
@@ -95,16 +105,16 @@ function handleBlur(ev: FocusEvent) {
|
|
|
95
105
|
<div v-if="meta" class="font-thin mb-1">
|
|
96
106
|
{{ meta }}
|
|
97
107
|
</div>
|
|
98
|
-
<div class="flex flex-wrap items-start">
|
|
108
|
+
<div class="flex flex-wrap items-start gap-x-1">
|
|
99
109
|
<FdsPhonenumberCountryPicker
|
|
100
110
|
v-model="country"
|
|
101
111
|
:items="countryItems"
|
|
102
112
|
:valid="displayValid"
|
|
103
113
|
:disabled="disabled"
|
|
104
114
|
:ariaLabel="t('FdsPhonenumber.countryCode')"
|
|
105
|
-
:no-results-label="t('FdsPhonenumber.noCountryResults')"
|
|
106
115
|
:data-testid="dataTestid ? `${dataTestid}-country` : undefined"
|
|
107
116
|
:class="['mb-0! shrink-0', selectClass ?? '']"
|
|
117
|
+
@no-country-results="onNoCountryResults"
|
|
108
118
|
/>
|
|
109
119
|
<FdsInput
|
|
110
120
|
v-model="nationalNumber"
|
|
@@ -116,9 +126,11 @@ function handleBlur(ev: FocusEvent) {
|
|
|
116
126
|
:data-testid="dataTestid ? `${dataTestid}-number` : undefined"
|
|
117
127
|
:class="['mb-0! min-w-0 flex-1', inputClass ?? '']"
|
|
118
128
|
@blur="handleBlur"
|
|
119
|
-
:inputClass="[' rounded-l-none']"
|
|
120
129
|
/>
|
|
121
130
|
</div>
|
|
131
|
+
<div v-if="noCountryResults" class="text-red-700 font-bold mt-1">
|
|
132
|
+
{{ t('FdsPhonenumber.noCountryResults') }}
|
|
133
|
+
</div>
|
|
122
134
|
<div v-if="showInvalidMessage" class="text-red-700 font-bold mt-1">
|
|
123
135
|
{{ invalidMessage }}
|
|
124
136
|
</div>
|
|
@@ -1,20 +1,23 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import { computed,
|
|
2
|
+
import { computed, onBeforeUnmount, onMounted, ref, watch, type HTMLAttributes } from 'vue'
|
|
3
3
|
import { useFdsI18n } from '../../../plugin/useFdsI18n'
|
|
4
4
|
import FdsIcon from '../../FdsIcon/FdsIcon.vue'
|
|
5
|
-
import { countryCodeToFlag, filterCountryOptions, type CountryPhoneOption } from './countries'
|
|
5
|
+
import { countryCodeToFlag, filterCountryOptions, isPrioritizedCountry, type CountryPhoneOption } from './countries'
|
|
6
6
|
|
|
7
7
|
const { t } = useFdsI18n()
|
|
8
8
|
|
|
9
9
|
const country = defineModel<string>({ required: true })
|
|
10
10
|
|
|
11
|
+
const emit = defineEmits<{
|
|
12
|
+
noCountryResults: [value: boolean]
|
|
13
|
+
}>()
|
|
14
|
+
|
|
11
15
|
const props = withDefaults(
|
|
12
16
|
defineProps<{
|
|
13
17
|
items: CountryPhoneOption[]
|
|
14
18
|
valid?: boolean | null
|
|
15
19
|
disabled?: boolean
|
|
16
20
|
ariaLabel?: string
|
|
17
|
-
noResultsLabel?: string
|
|
18
21
|
dataTestid?: string
|
|
19
22
|
class?: HTMLAttributes['class']
|
|
20
23
|
}>(),
|
|
@@ -22,53 +25,104 @@ const props = withDefaults(
|
|
|
22
25
|
valid: undefined,
|
|
23
26
|
disabled: false,
|
|
24
27
|
ariaLabel: undefined,
|
|
25
|
-
noResultsLabel: '',
|
|
26
28
|
dataTestid: undefined,
|
|
27
29
|
class: undefined,
|
|
28
30
|
},
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
const rootRef = ref<HTMLElement | null>(null)
|
|
32
|
-
const listRef = ref<HTMLUListElement | null>(null)
|
|
33
34
|
const inputRef = ref<HTMLInputElement | null>(null)
|
|
34
35
|
const searchQuery = ref('')
|
|
35
36
|
const dropdownOpen = ref(false)
|
|
36
37
|
const isFocused = ref(false)
|
|
38
|
+
const hoveredCountry = ref<CountryPhoneOption | null>(null)
|
|
39
|
+
const activeIndex = ref(-1)
|
|
40
|
+
|
|
41
|
+
const listboxId = `fds-phone-country-listbox-${Math.random().toString(36).slice(2, 9)}`
|
|
37
42
|
|
|
38
43
|
const selectedCountry = computed(() => props.items.find((item) => item.value === country.value))
|
|
39
44
|
|
|
40
45
|
const filteredCountries = computed(() => filterCountryOptions(props.items, searchQuery.value))
|
|
41
46
|
|
|
47
|
+
const lastPrioritizedFilteredIndex = computed(() =>
|
|
48
|
+
filteredCountries.value.reduce(
|
|
49
|
+
(lastIndex, option, index) => (isPrioritizedCountry(option.value) ? index : lastIndex),
|
|
50
|
+
-1,
|
|
51
|
+
),
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
const hasNoCountryResults = computed(
|
|
55
|
+
() =>
|
|
56
|
+
dropdownOpen.value &&
|
|
57
|
+
!props.disabled &&
|
|
58
|
+
searchQuery.value.trim().length > 0 &&
|
|
59
|
+
filteredCountries.value.length === 0,
|
|
60
|
+
)
|
|
61
|
+
|
|
42
62
|
const isInvalid = computed(() => props.valid === false && !props.disabled)
|
|
43
63
|
|
|
44
|
-
const
|
|
64
|
+
const isSearching = computed(() => searchQuery.value.length > 0)
|
|
65
|
+
|
|
66
|
+
const isHoverPreview = computed(() => !!hoveredCountry.value && dropdownOpen.value)
|
|
67
|
+
|
|
68
|
+
const activeCountry = computed(() => {
|
|
69
|
+
if (activeIndex.value < 0) {
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
return filteredCountries.value[activeIndex.value] ?? null
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const showSelectedDialPreview = computed(() => !!selectedCountry.value && !isSearching.value && !isHoverPreview.value)
|
|
76
|
+
|
|
77
|
+
const showHoverDialPreview = computed(() => isHoverPreview.value && !!hoveredCountry.value)
|
|
78
|
+
|
|
79
|
+
const showActiveDialPreview = computed(
|
|
80
|
+
() => !showHoverDialPreview.value && !!activeCountry.value && dropdownOpen.value && !isSearching.value,
|
|
81
|
+
)
|
|
45
82
|
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
return
|
|
83
|
+
const dialPreviewCountry = computed(() => {
|
|
84
|
+
if (showHoverDialPreview.value && hoveredCountry.value) {
|
|
85
|
+
return hoveredCountry.value
|
|
49
86
|
}
|
|
50
|
-
if (
|
|
51
|
-
return
|
|
87
|
+
if (showActiveDialPreview.value && activeCountry.value) {
|
|
88
|
+
return activeCountry.value
|
|
52
89
|
}
|
|
53
|
-
|
|
90
|
+
if (showSelectedDialPreview.value && selectedCountry.value) {
|
|
91
|
+
return selectedCountry.value
|
|
92
|
+
}
|
|
93
|
+
return null
|
|
54
94
|
})
|
|
55
95
|
|
|
96
|
+
const activeDescendantId = computed(() =>
|
|
97
|
+
activeIndex.value >= 0 ? `fds-phone-country-option-${activeIndex.value}` : undefined,
|
|
98
|
+
)
|
|
99
|
+
|
|
56
100
|
const inputDataAttrs = computed(() => (props.dataTestid ? { 'data-testid': props.dataTestid } : {}))
|
|
57
101
|
|
|
58
102
|
const listDataAttrs = computed(() => (props.dataTestid ? { 'data-testid': `${props.dataTestid}-list` } : {}))
|
|
59
103
|
|
|
104
|
+
const dialPreviewClasses = computed(() => [
|
|
105
|
+
'pointer-events-none absolute inset-0 flex items-center pl-10 pr-10 text-base leading-6 tabular-nums',
|
|
106
|
+
showHoverDialPreview.value ? 'text-gray-900/50' : 'text-gray-900',
|
|
107
|
+
])
|
|
108
|
+
|
|
60
109
|
const inputClasses = computed(() => [
|
|
61
|
-
'block w-full h-12 rounded-md border border-gray-500 py-2 text-base leading-6 tabular-nums',
|
|
62
|
-
showSelectedPreview.value ? 'pl-10 pr-10' : 'px-3 pr-10',
|
|
110
|
+
'block w-full h-12 rounded-md border border-gray-500 py-2 text-base leading-6 tabular-nums px-3 pr-10',
|
|
63
111
|
'focus:outline-2 focus:outline-blue-500 -outline-offset-2 focus:border-transparent',
|
|
64
112
|
props.disabled
|
|
65
113
|
? 'text-gray-800 outline-dashed outline-2 outline-gray-400 cursor-not-allowed border-transparent bg-gray-50'
|
|
66
|
-
: 'bg-white text-gray-900',
|
|
114
|
+
: 'cursor-text bg-white text-gray-900',
|
|
67
115
|
isInvalid.value && 'outline-2 -outline-offset-2 outline-red-600',
|
|
116
|
+
hasNoCountryResults.value && 'outline-red-600!',
|
|
117
|
+
])
|
|
118
|
+
|
|
119
|
+
const arrowButtonClasses = computed(() => [
|
|
120
|
+
'absolute right-0 top-0 z-10 flex h-12 w-10 items-center justify-center border-0 bg-transparent p-0',
|
|
121
|
+
props.disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
|
68
122
|
])
|
|
69
123
|
|
|
70
124
|
const arrowClasses = computed(() => [
|
|
71
|
-
'transition-transform duration-200 ease-in-out',
|
|
125
|
+
'pointer-events-none transition-transform duration-200 ease-in-out',
|
|
72
126
|
{
|
|
73
127
|
'fill-gray-500': props.disabled,
|
|
74
128
|
'fill-red-600': isInvalid.value && !props.disabled,
|
|
@@ -77,48 +131,76 @@ const arrowClasses = computed(() => [
|
|
|
77
131
|
},
|
|
78
132
|
])
|
|
79
133
|
|
|
80
|
-
|
|
81
|
-
if (
|
|
82
|
-
return
|
|
83
|
-
}
|
|
84
|
-
await nextTick()
|
|
85
|
-
const list = listRef.value
|
|
86
|
-
if (!list) {
|
|
87
|
-
return
|
|
88
|
-
}
|
|
89
|
-
const selected = list.querySelector<HTMLElement>(`#fds-country-${country.value}`)
|
|
90
|
-
if (!selected) {
|
|
134
|
+
const openDropdown = () => {
|
|
135
|
+
if (props.disabled) {
|
|
91
136
|
return
|
|
92
137
|
}
|
|
138
|
+
dropdownOpen.value = true
|
|
139
|
+
isFocused.value = true
|
|
140
|
+
searchQuery.value = ''
|
|
141
|
+
activeIndex.value = -1
|
|
142
|
+
}
|
|
93
143
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const itemRect = selected.getBoundingClientRect()
|
|
97
|
-
const relativeTop = itemRect.top - listRect.top + list.scrollTop
|
|
98
|
-
list.scrollTop = relativeTop - (list.clientHeight - selected.offsetHeight) / 2
|
|
144
|
+
const clearHoverPreview = () => {
|
|
145
|
+
hoveredCountry.value = null
|
|
99
146
|
}
|
|
100
147
|
|
|
101
|
-
|
|
102
|
-
|
|
148
|
+
const clearActiveOption = () => {
|
|
149
|
+
activeIndex.value = -1
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const setActiveIndex = (index: number) => {
|
|
153
|
+
if (!filteredCountries.value.length) {
|
|
154
|
+
clearActiveOption()
|
|
103
155
|
return
|
|
104
156
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
157
|
+
activeIndex.value = Math.max(0, Math.min(index, filteredCountries.value.length - 1))
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const setActiveIndexToSelectedOrFirst = () => {
|
|
161
|
+
const selectedIndex = filteredCountries.value.findIndex((option) => option.value === country.value)
|
|
162
|
+
setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const setActiveIndexToSelectedOrLast = () => {
|
|
166
|
+
const selectedIndex = filteredCountries.value.findIndex((option) => option.value === country.value)
|
|
167
|
+
setActiveIndex(selectedIndex >= 0 ? selectedIndex : filteredCountries.value.length - 1)
|
|
109
168
|
}
|
|
110
169
|
|
|
111
|
-
|
|
170
|
+
const onOptionMouseEnter = (option: CountryPhoneOption) => {
|
|
171
|
+
hoveredCountry.value = option
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const onOptionMouseLeave = () => {
|
|
175
|
+
clearHoverPreview()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const closeDropdown = () => {
|
|
112
179
|
dropdownOpen.value = false
|
|
113
180
|
isFocused.value = false
|
|
114
181
|
searchQuery.value = ''
|
|
182
|
+
clearHoverPreview()
|
|
183
|
+
clearActiveOption()
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const onInputFocus = () => {
|
|
187
|
+
openDropdown()
|
|
115
188
|
}
|
|
116
189
|
|
|
117
|
-
|
|
190
|
+
const onArrowPointerDown = () => {
|
|
191
|
+
if (props.disabled) {
|
|
192
|
+
return
|
|
193
|
+
}
|
|
194
|
+
if (dropdownOpen.value) {
|
|
195
|
+
closeDropdown()
|
|
196
|
+
inputRef.value?.blur()
|
|
197
|
+
return
|
|
198
|
+
}
|
|
118
199
|
openDropdown()
|
|
200
|
+
inputRef.value?.focus()
|
|
119
201
|
}
|
|
120
202
|
|
|
121
|
-
|
|
203
|
+
const onInputBlur = () => {
|
|
122
204
|
isFocused.value = false
|
|
123
205
|
window.setTimeout(() => {
|
|
124
206
|
if (!dropdownOpen.value) {
|
|
@@ -128,21 +210,86 @@ function onInputBlur() {
|
|
|
128
210
|
}, 120)
|
|
129
211
|
}
|
|
130
212
|
|
|
131
|
-
|
|
213
|
+
const onSearchInput = (event: Event) => {
|
|
132
214
|
const target = event.target as HTMLInputElement
|
|
133
215
|
searchQuery.value = target.value
|
|
216
|
+
clearActiveOption()
|
|
134
217
|
if (!dropdownOpen.value) {
|
|
135
218
|
dropdownOpen.value = true
|
|
136
219
|
}
|
|
137
220
|
}
|
|
138
221
|
|
|
139
|
-
|
|
222
|
+
const onInputKeyDown = (event: KeyboardEvent) => {
|
|
223
|
+
if (props.disabled) {
|
|
224
|
+
return
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (event.key === 'ArrowDown' && filteredCountries.value.length > 0) {
|
|
228
|
+
event.preventDefault()
|
|
229
|
+
clearHoverPreview()
|
|
230
|
+
if (!dropdownOpen.value) {
|
|
231
|
+
openDropdown()
|
|
232
|
+
setActiveIndexToSelectedOrFirst()
|
|
233
|
+
return
|
|
234
|
+
}
|
|
235
|
+
if (activeIndex.value < 0) {
|
|
236
|
+
setActiveIndexToSelectedOrFirst()
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
setActiveIndex(activeIndex.value + 1)
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (event.key === 'ArrowUp' && filteredCountries.value.length > 0) {
|
|
244
|
+
event.preventDefault()
|
|
245
|
+
clearHoverPreview()
|
|
246
|
+
if (!dropdownOpen.value) {
|
|
247
|
+
openDropdown()
|
|
248
|
+
setActiveIndexToSelectedOrLast()
|
|
249
|
+
return
|
|
250
|
+
}
|
|
251
|
+
if (activeIndex.value < 0) {
|
|
252
|
+
setActiveIndexToSelectedOrLast()
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
setActiveIndex(activeIndex.value - 1)
|
|
256
|
+
return
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (event.key === 'Enter') {
|
|
260
|
+
if (!dropdownOpen.value && filteredCountries.value.length > 0) {
|
|
261
|
+
event.preventDefault()
|
|
262
|
+
openDropdown()
|
|
263
|
+
setActiveIndexToSelectedOrFirst()
|
|
264
|
+
return
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (dropdownOpen.value && activeCountry.value) {
|
|
268
|
+
event.preventDefault()
|
|
269
|
+
selectCountry(activeCountry.value)
|
|
270
|
+
}
|
|
271
|
+
return
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (event.key === 'Escape' && dropdownOpen.value) {
|
|
275
|
+
event.preventDefault()
|
|
276
|
+
closeDropdown()
|
|
277
|
+
inputRef.value?.focus()
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (event.key === 'Tab' && dropdownOpen.value) {
|
|
282
|
+
closeDropdown()
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const selectCountry = (option: CountryPhoneOption) => {
|
|
140
287
|
country.value = option.value
|
|
141
288
|
closeDropdown()
|
|
142
289
|
inputRef.value?.blur()
|
|
143
290
|
}
|
|
144
291
|
|
|
145
|
-
|
|
292
|
+
const onClickOutside = (event: MouseEvent) => {
|
|
146
293
|
if (!dropdownOpen.value || !rootRef.value) {
|
|
147
294
|
return
|
|
148
295
|
}
|
|
@@ -160,6 +307,24 @@ watch(
|
|
|
160
307
|
},
|
|
161
308
|
)
|
|
162
309
|
|
|
310
|
+
watch(filteredCountries, (options) => {
|
|
311
|
+
if (!options.length) {
|
|
312
|
+
clearActiveOption()
|
|
313
|
+
return
|
|
314
|
+
}
|
|
315
|
+
if (activeIndex.value >= options.length) {
|
|
316
|
+
setActiveIndex(options.length - 1)
|
|
317
|
+
}
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
watch(
|
|
321
|
+
hasNoCountryResults,
|
|
322
|
+
(value) => {
|
|
323
|
+
emit('noCountryResults', value)
|
|
324
|
+
},
|
|
325
|
+
{ immediate: true },
|
|
326
|
+
)
|
|
327
|
+
|
|
163
328
|
onMounted(() => {
|
|
164
329
|
window.addEventListener('mousedown', onClickOutside)
|
|
165
330
|
})
|
|
@@ -172,13 +337,12 @@ onBeforeUnmount(() => {
|
|
|
172
337
|
<template>
|
|
173
338
|
<div ref="rootRef" class="relative w-32" :class="props.class">
|
|
174
339
|
<div class="relative w-full">
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
</span>
|
|
340
|
+
<div v-if="!dropdownOpen" :class="dialPreviewClasses" aria-hidden="true">
|
|
341
|
+
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-xl leading-none">
|
|
342
|
+
{{ countryCodeToFlag(dialPreviewCountry?.value ?? '') }}
|
|
343
|
+
</span>
|
|
344
|
+
{{ t('common.plus') }}{{ dialPreviewCountry?.countryCode ?? '' }}
|
|
345
|
+
</div>
|
|
182
346
|
<input
|
|
183
347
|
ref="inputRef"
|
|
184
348
|
type="search"
|
|
@@ -186,51 +350,70 @@ onBeforeUnmount(() => {
|
|
|
186
350
|
role="combobox"
|
|
187
351
|
:aria-expanded="dropdownOpen"
|
|
188
352
|
aria-autocomplete="list"
|
|
353
|
+
aria-haspopup="listbox"
|
|
189
354
|
:aria-label="ariaLabel"
|
|
190
|
-
:
|
|
355
|
+
:aria-controls="dropdownOpen ? listboxId : undefined"
|
|
356
|
+
:aria-activedescendant="activeDescendantId"
|
|
357
|
+
:value="searchQuery"
|
|
191
358
|
:disabled="disabled"
|
|
192
359
|
:class="inputClasses"
|
|
193
360
|
v-bind="inputDataAttrs"
|
|
194
361
|
@focus="onInputFocus"
|
|
195
362
|
@blur="onInputBlur"
|
|
363
|
+
@keydown="onInputKeyDown"
|
|
196
364
|
@input="onSearchInput"
|
|
197
|
-
class="border-r-0 rounded-r-none"
|
|
198
365
|
/>
|
|
199
|
-
<
|
|
366
|
+
<button
|
|
367
|
+
type="button"
|
|
368
|
+
tabindex="-1"
|
|
369
|
+
:disabled="disabled"
|
|
370
|
+
:class="arrowButtonClasses"
|
|
371
|
+
aria-hidden="true"
|
|
372
|
+
@mousedown.prevent="onArrowPointerDown"
|
|
373
|
+
>
|
|
200
374
|
<FdsIcon name="arrowDown" :size="24" :class="arrowClasses" />
|
|
201
|
-
</
|
|
375
|
+
</button>
|
|
202
376
|
</div>
|
|
203
|
-
|
|
204
377
|
<ul
|
|
205
|
-
v-if="dropdownOpen && !disabled"
|
|
206
|
-
ref="listRef"
|
|
378
|
+
v-if="dropdownOpen && !disabled && filteredCountries.length > 0"
|
|
207
379
|
role="listbox"
|
|
208
|
-
|
|
380
|
+
:id="listboxId"
|
|
381
|
+
class="absolute left-0 z-20 mt-1 min-w-[20rem] w-max max-w-[calc(100vw-2rem)] max-h-[280px] overflow-y-auto rounded-md border border-gray-300 bg-white"
|
|
209
382
|
v-bind="listDataAttrs"
|
|
383
|
+
@mouseleave="clearHoverPreview"
|
|
210
384
|
>
|
|
211
|
-
<li v-if="filteredCountries.length === 0" class="px-3 text-sm text-gray-700">
|
|
212
|
-
{{ noResultsLabel }}
|
|
213
|
-
</li>
|
|
214
385
|
<li
|
|
215
|
-
v-for="option in filteredCountries"
|
|
386
|
+
v-for="(option, index) in filteredCountries"
|
|
216
387
|
:key="option.value"
|
|
388
|
+
:id="`fds-phone-country-option-${index}`"
|
|
217
389
|
role="option"
|
|
218
|
-
:id="`fds-country-${option.value}`"
|
|
219
390
|
:aria-selected="option.value === country"
|
|
220
|
-
class="mb-0 border-b border-gray-100"
|
|
391
|
+
class="mb-0 border-b border-gray-100 hover:bg-blue_t-100 active:bg-blue_t-200 last:border-b-0!"
|
|
392
|
+
:class="{
|
|
393
|
+
'border-gray-300': index === lastPrioritizedFilteredIndex,
|
|
394
|
+
'bg-blue_t-100': activeIndex === index,
|
|
395
|
+
}"
|
|
221
396
|
>
|
|
222
397
|
<button
|
|
223
398
|
type="button"
|
|
224
|
-
|
|
225
|
-
|
|
399
|
+
tabindex="-1"
|
|
400
|
+
class="flex w-full items-center justify-between gap-2 py-4 px-3 text-left cursor-pointer"
|
|
401
|
+
@mouseenter="onOptionMouseEnter(option)"
|
|
402
|
+
@mouseleave="onOptionMouseLeave"
|
|
403
|
+
@mousedown.prevent
|
|
404
|
+
@click="selectCountry(option)"
|
|
226
405
|
>
|
|
227
|
-
<span class="
|
|
228
|
-
|
|
229
|
-
|
|
406
|
+
<span class="flex items-center gap-2">
|
|
407
|
+
<span class="flex items-center gap-3">
|
|
408
|
+
<span class="shrink-0 text-xl leading-none" aria-hidden="true">
|
|
409
|
+
{{ countryCodeToFlag(option.value) }}
|
|
410
|
+
</span>
|
|
411
|
+
{{ option.countryName }}
|
|
412
|
+
</span>
|
|
413
|
+
<span class="text-gray-600">({{ t('common.plus') }}{{ option.countryCode }})</span>
|
|
230
414
|
</span>
|
|
231
|
-
<span
|
|
232
|
-
|
|
233
|
-
<FdsIcon v-if="option.value === country" name="check" :size="20" class="fill-green-600" />
|
|
415
|
+
<span aria-hidden="true">
|
|
416
|
+
<FdsIcon v-if="option.value === country" name="check" :size="24" class="fill-green-600" />
|
|
234
417
|
</span>
|
|
235
418
|
</button>
|
|
236
419
|
</li>
|
|
@@ -36,20 +36,36 @@ function getLocalizedCountryName(iso2: string, locale: string): string {
|
|
|
36
36
|
return displayNames?.of(iso2) ?? iso2
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
export const PRIORITIZED_COUNTRIES = ['SE', 'NO', 'DK', 'FI'] as const
|
|
40
|
+
|
|
41
|
+
export function isPrioritizedCountry(iso2: string): boolean {
|
|
42
|
+
return PRIORITIZED_COUNTRIES.includes(iso2 as (typeof PRIORITIZED_COUNTRIES)[number])
|
|
43
|
+
}
|
|
44
|
+
|
|
39
45
|
export function sortCountryOptionsByName(
|
|
40
46
|
options: CountryPhoneOption[],
|
|
41
47
|
locale = 'sv-SE',
|
|
48
|
+
_preferredCountry = 'SE',
|
|
42
49
|
): CountryPhoneOption[] {
|
|
43
50
|
const sortLocale = resolveDisplayLocale(locale).split(/[-_]/)[0] || 'sv'
|
|
44
51
|
return [...options].sort((a, b) => {
|
|
52
|
+
const priorityA = PRIORITIZED_COUNTRIES.indexOf(a.value as (typeof PRIORITIZED_COUNTRIES)[number])
|
|
53
|
+
const priorityB = PRIORITIZED_COUNTRIES.indexOf(b.value as (typeof PRIORITIZED_COUNTRIES)[number])
|
|
54
|
+
|
|
55
|
+
if (priorityA !== -1 || priorityB !== -1) {
|
|
56
|
+
if (priorityA === -1) return 1
|
|
57
|
+
if (priorityB === -1) return -1
|
|
58
|
+
if (priorityA !== priorityB) return priorityA - priorityB
|
|
59
|
+
}
|
|
60
|
+
|
|
45
61
|
const nameA = a.countryName || a.label
|
|
46
62
|
const nameB = b.countryName || b.label
|
|
47
63
|
return nameA.localeCompare(nameB, sortLocale)
|
|
48
64
|
})
|
|
49
65
|
}
|
|
50
66
|
|
|
51
|
-
/** Country options for phone fields,
|
|
52
|
-
export function buildCountryOptions(locale = 'sv-SE'): CountryPhoneOption[] {
|
|
67
|
+
/** Country options for phone fields; SE/NO/DK/FI first, then alphabetical by name. */
|
|
68
|
+
export function buildCountryOptions(locale = 'sv-SE', preferredCountry = 'SE'): CountryPhoneOption[] {
|
|
53
69
|
const options: CountryPhoneOption[] = getCountries().map((iso2) => {
|
|
54
70
|
const countryName = getLocalizedCountryName(iso2, locale)
|
|
55
71
|
const countryCode = getCountryCallingCode(iso2)
|
|
@@ -61,7 +77,7 @@ export function buildCountryOptions(locale = 'sv-SE'): CountryPhoneOption[] {
|
|
|
61
77
|
}
|
|
62
78
|
})
|
|
63
79
|
|
|
64
|
-
return sortCountryOptionsByName(options, locale)
|
|
80
|
+
return sortCountryOptionsByName(options, locale, preferredCountry)
|
|
65
81
|
}
|
|
66
82
|
|
|
67
83
|
/** ISO 3166-1 alpha-2 to regional indicator flag emoji, e.g. `SE` β πΈπͺ */
|