fds-vue-core 7.2.1 β 7.2.2
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 +303 -128
- 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 +303 -128
- 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 +259 -72
- 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.2",
|
|
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,108 @@ 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)
|
|
45
67
|
|
|
46
|
-
const
|
|
47
|
-
if (
|
|
48
|
-
return
|
|
68
|
+
const activeCountry = computed(() => {
|
|
69
|
+
if (activeIndex.value < 0) {
|
|
70
|
+
return null
|
|
49
71
|
}
|
|
50
|
-
|
|
51
|
-
|
|
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
|
+
)
|
|
82
|
+
|
|
83
|
+
const showDialPreviewPadding = computed(
|
|
84
|
+
() => showSelectedDialPreview.value || showHoverDialPreview.value || showActiveDialPreview.value,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
const dialPreviewCountry = computed(() => {
|
|
88
|
+
if (showHoverDialPreview.value && hoveredCountry.value) {
|
|
89
|
+
return hoveredCountry.value
|
|
90
|
+
}
|
|
91
|
+
if (showActiveDialPreview.value && activeCountry.value) {
|
|
92
|
+
return activeCountry.value
|
|
52
93
|
}
|
|
53
|
-
|
|
94
|
+
if (showSelectedDialPreview.value && selectedCountry.value) {
|
|
95
|
+
return selectedCountry.value
|
|
96
|
+
}
|
|
97
|
+
return null
|
|
54
98
|
})
|
|
55
99
|
|
|
100
|
+
const activeDescendantId = computed(() =>
|
|
101
|
+
activeIndex.value >= 0 ? `fds-phone-country-option-${activeIndex.value}` : undefined,
|
|
102
|
+
)
|
|
103
|
+
|
|
56
104
|
const inputDataAttrs = computed(() => (props.dataTestid ? { 'data-testid': props.dataTestid } : {}))
|
|
57
105
|
|
|
58
106
|
const listDataAttrs = computed(() => (props.dataTestid ? { 'data-testid': `${props.dataTestid}-list` } : {}))
|
|
59
107
|
|
|
108
|
+
const dialPreviewClasses = computed(() => [
|
|
109
|
+
'pointer-events-none absolute inset-0 flex items-center pl-10 pr-10 text-base leading-6 tabular-nums',
|
|
110
|
+
showHoverDialPreview.value ? 'text-gray-900/50' : 'text-gray-900',
|
|
111
|
+
])
|
|
112
|
+
|
|
60
113
|
const inputClasses = computed(() => [
|
|
61
114
|
'block w-full h-12 rounded-md border border-gray-500 py-2 text-base leading-6 tabular-nums',
|
|
62
|
-
|
|
115
|
+
showDialPreviewPadding.value ? 'pl-10 pr-10' : 'px-3 pr-10',
|
|
63
116
|
'focus:outline-2 focus:outline-blue-500 -outline-offset-2 focus:border-transparent',
|
|
64
117
|
props.disabled
|
|
65
118
|
? 'text-gray-800 outline-dashed outline-2 outline-gray-400 cursor-not-allowed border-transparent bg-gray-50'
|
|
66
|
-
: 'bg-white text-gray-900',
|
|
119
|
+
: 'cursor-text bg-white text-gray-900',
|
|
67
120
|
isInvalid.value && 'outline-2 -outline-offset-2 outline-red-600',
|
|
68
121
|
])
|
|
69
122
|
|
|
123
|
+
const arrowButtonClasses = computed(() => [
|
|
124
|
+
'absolute right-0 top-0 z-10 flex h-12 w-10 items-center justify-center border-0 bg-transparent p-0',
|
|
125
|
+
props.disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
|
126
|
+
])
|
|
127
|
+
|
|
70
128
|
const arrowClasses = computed(() => [
|
|
71
|
-
'transition-transform duration-200 ease-in-out',
|
|
129
|
+
'pointer-events-none transition-transform duration-200 ease-in-out',
|
|
72
130
|
{
|
|
73
131
|
'fill-gray-500': props.disabled,
|
|
74
132
|
'fill-red-600': isInvalid.value && !props.disabled,
|
|
@@ -77,48 +135,76 @@ const arrowClasses = computed(() => [
|
|
|
77
135
|
},
|
|
78
136
|
])
|
|
79
137
|
|
|
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) {
|
|
138
|
+
const openDropdown = () => {
|
|
139
|
+
if (props.disabled) {
|
|
91
140
|
return
|
|
92
141
|
}
|
|
142
|
+
dropdownOpen.value = true
|
|
143
|
+
isFocused.value = true
|
|
144
|
+
searchQuery.value = ''
|
|
145
|
+
activeIndex.value = -1
|
|
146
|
+
}
|
|
93
147
|
|
|
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
|
|
148
|
+
const clearHoverPreview = () => {
|
|
149
|
+
hoveredCountry.value = null
|
|
99
150
|
}
|
|
100
151
|
|
|
101
|
-
|
|
102
|
-
|
|
152
|
+
const clearActiveOption = () => {
|
|
153
|
+
activeIndex.value = -1
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const setActiveIndex = (index: number) => {
|
|
157
|
+
if (!filteredCountries.value.length) {
|
|
158
|
+
clearActiveOption()
|
|
103
159
|
return
|
|
104
160
|
}
|
|
105
|
-
|
|
106
|
-
isFocused.value = true
|
|
107
|
-
searchQuery.value = ''
|
|
108
|
-
await scrollToSelectedCountry()
|
|
161
|
+
activeIndex.value = Math.max(0, Math.min(index, filteredCountries.value.length - 1))
|
|
109
162
|
}
|
|
110
163
|
|
|
111
|
-
|
|
164
|
+
const setActiveIndexToSelectedOrFirst = () => {
|
|
165
|
+
const selectedIndex = filteredCountries.value.findIndex((option) => option.value === country.value)
|
|
166
|
+
setActiveIndex(selectedIndex >= 0 ? selectedIndex : 0)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const setActiveIndexToSelectedOrLast = () => {
|
|
170
|
+
const selectedIndex = filteredCountries.value.findIndex((option) => option.value === country.value)
|
|
171
|
+
setActiveIndex(selectedIndex >= 0 ? selectedIndex : filteredCountries.value.length - 1)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const onOptionMouseEnter = (option: CountryPhoneOption) => {
|
|
175
|
+
hoveredCountry.value = option
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const onOptionMouseLeave = () => {
|
|
179
|
+
clearHoverPreview()
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const closeDropdown = () => {
|
|
112
183
|
dropdownOpen.value = false
|
|
113
184
|
isFocused.value = false
|
|
114
185
|
searchQuery.value = ''
|
|
186
|
+
clearHoverPreview()
|
|
187
|
+
clearActiveOption()
|
|
115
188
|
}
|
|
116
189
|
|
|
117
|
-
|
|
190
|
+
const onInputFocus = () => {
|
|
191
|
+
openDropdown()
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const onArrowPointerDown = () => {
|
|
195
|
+
if (props.disabled) {
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
if (dropdownOpen.value) {
|
|
199
|
+
closeDropdown()
|
|
200
|
+
inputRef.value?.blur()
|
|
201
|
+
return
|
|
202
|
+
}
|
|
118
203
|
openDropdown()
|
|
204
|
+
inputRef.value?.focus()
|
|
119
205
|
}
|
|
120
206
|
|
|
121
|
-
|
|
207
|
+
const onInputBlur = () => {
|
|
122
208
|
isFocused.value = false
|
|
123
209
|
window.setTimeout(() => {
|
|
124
210
|
if (!dropdownOpen.value) {
|
|
@@ -128,21 +214,86 @@ function onInputBlur() {
|
|
|
128
214
|
}, 120)
|
|
129
215
|
}
|
|
130
216
|
|
|
131
|
-
|
|
217
|
+
const onSearchInput = (event: Event) => {
|
|
132
218
|
const target = event.target as HTMLInputElement
|
|
133
219
|
searchQuery.value = target.value
|
|
220
|
+
clearActiveOption()
|
|
134
221
|
if (!dropdownOpen.value) {
|
|
135
222
|
dropdownOpen.value = true
|
|
136
223
|
}
|
|
137
224
|
}
|
|
138
225
|
|
|
139
|
-
|
|
226
|
+
const onInputKeyDown = (event: KeyboardEvent) => {
|
|
227
|
+
if (props.disabled) {
|
|
228
|
+
return
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (event.key === 'ArrowDown' && filteredCountries.value.length > 0) {
|
|
232
|
+
event.preventDefault()
|
|
233
|
+
clearHoverPreview()
|
|
234
|
+
if (!dropdownOpen.value) {
|
|
235
|
+
openDropdown()
|
|
236
|
+
setActiveIndexToSelectedOrFirst()
|
|
237
|
+
return
|
|
238
|
+
}
|
|
239
|
+
if (activeIndex.value < 0) {
|
|
240
|
+
setActiveIndexToSelectedOrFirst()
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
setActiveIndex(activeIndex.value + 1)
|
|
244
|
+
return
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (event.key === 'ArrowUp' && filteredCountries.value.length > 0) {
|
|
248
|
+
event.preventDefault()
|
|
249
|
+
clearHoverPreview()
|
|
250
|
+
if (!dropdownOpen.value) {
|
|
251
|
+
openDropdown()
|
|
252
|
+
setActiveIndexToSelectedOrLast()
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
if (activeIndex.value < 0) {
|
|
256
|
+
setActiveIndexToSelectedOrLast()
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
setActiveIndex(activeIndex.value - 1)
|
|
260
|
+
return
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (event.key === 'Enter') {
|
|
264
|
+
if (!dropdownOpen.value && filteredCountries.value.length > 0) {
|
|
265
|
+
event.preventDefault()
|
|
266
|
+
openDropdown()
|
|
267
|
+
setActiveIndexToSelectedOrFirst()
|
|
268
|
+
return
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (dropdownOpen.value && activeCountry.value) {
|
|
272
|
+
event.preventDefault()
|
|
273
|
+
selectCountry(activeCountry.value)
|
|
274
|
+
}
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (event.key === 'Escape' && dropdownOpen.value) {
|
|
279
|
+
event.preventDefault()
|
|
280
|
+
closeDropdown()
|
|
281
|
+
inputRef.value?.focus()
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (event.key === 'Tab' && dropdownOpen.value) {
|
|
286
|
+
closeDropdown()
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const selectCountry = (option: CountryPhoneOption) => {
|
|
140
291
|
country.value = option.value
|
|
141
292
|
closeDropdown()
|
|
142
293
|
inputRef.value?.blur()
|
|
143
294
|
}
|
|
144
295
|
|
|
145
|
-
|
|
296
|
+
const onClickOutside = (event: MouseEvent) => {
|
|
146
297
|
if (!dropdownOpen.value || !rootRef.value) {
|
|
147
298
|
return
|
|
148
299
|
}
|
|
@@ -160,6 +311,24 @@ watch(
|
|
|
160
311
|
},
|
|
161
312
|
)
|
|
162
313
|
|
|
314
|
+
watch(filteredCountries, (options) => {
|
|
315
|
+
if (!options.length) {
|
|
316
|
+
clearActiveOption()
|
|
317
|
+
return
|
|
318
|
+
}
|
|
319
|
+
if (activeIndex.value >= options.length) {
|
|
320
|
+
setActiveIndex(options.length - 1)
|
|
321
|
+
}
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
watch(
|
|
325
|
+
hasNoCountryResults,
|
|
326
|
+
(value) => {
|
|
327
|
+
emit('noCountryResults', value)
|
|
328
|
+
},
|
|
329
|
+
{ immediate: true },
|
|
330
|
+
)
|
|
331
|
+
|
|
163
332
|
onMounted(() => {
|
|
164
333
|
window.addEventListener('mousedown', onClickOutside)
|
|
165
334
|
})
|
|
@@ -172,13 +341,12 @@ onBeforeUnmount(() => {
|
|
|
172
341
|
<template>
|
|
173
342
|
<div ref="rootRef" class="relative w-32" :class="props.class">
|
|
174
343
|
<div class="relative w-full">
|
|
175
|
-
<
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
</span>
|
|
344
|
+
<div v-if="dialPreviewCountry" :class="dialPreviewClasses" aria-hidden="true">
|
|
345
|
+
<span class="absolute left-3 top-1/2 -translate-y-1/2 text-xl leading-none">
|
|
346
|
+
{{ countryCodeToFlag(dialPreviewCountry.value) }}
|
|
347
|
+
</span>
|
|
348
|
+
{{ t('common.plus') }}{{ dialPreviewCountry.countryCode }}
|
|
349
|
+
</div>
|
|
182
350
|
<input
|
|
183
351
|
ref="inputRef"
|
|
184
352
|
type="search"
|
|
@@ -186,51 +354,70 @@ onBeforeUnmount(() => {
|
|
|
186
354
|
role="combobox"
|
|
187
355
|
:aria-expanded="dropdownOpen"
|
|
188
356
|
aria-autocomplete="list"
|
|
357
|
+
aria-haspopup="listbox"
|
|
189
358
|
:aria-label="ariaLabel"
|
|
190
|
-
:
|
|
359
|
+
:aria-controls="dropdownOpen ? listboxId : undefined"
|
|
360
|
+
:aria-activedescendant="activeDescendantId"
|
|
361
|
+
:value="searchQuery"
|
|
191
362
|
:disabled="disabled"
|
|
192
363
|
:class="inputClasses"
|
|
193
364
|
v-bind="inputDataAttrs"
|
|
194
365
|
@focus="onInputFocus"
|
|
195
366
|
@blur="onInputBlur"
|
|
367
|
+
@keydown="onInputKeyDown"
|
|
196
368
|
@input="onSearchInput"
|
|
197
|
-
class="border-r-0 rounded-r-none"
|
|
198
369
|
/>
|
|
199
|
-
<
|
|
370
|
+
<button
|
|
371
|
+
type="button"
|
|
372
|
+
tabindex="-1"
|
|
373
|
+
:disabled="disabled"
|
|
374
|
+
:class="arrowButtonClasses"
|
|
375
|
+
aria-hidden="true"
|
|
376
|
+
@mousedown.prevent="onArrowPointerDown"
|
|
377
|
+
>
|
|
200
378
|
<FdsIcon name="arrowDown" :size="24" :class="arrowClasses" />
|
|
201
|
-
</
|
|
379
|
+
</button>
|
|
202
380
|
</div>
|
|
203
|
-
|
|
204
381
|
<ul
|
|
205
|
-
v-if="dropdownOpen && !disabled"
|
|
206
|
-
ref="listRef"
|
|
382
|
+
v-if="dropdownOpen && !disabled && filteredCountries.length > 0"
|
|
207
383
|
role="listbox"
|
|
208
|
-
|
|
384
|
+
:id="listboxId"
|
|
385
|
+
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
386
|
v-bind="listDataAttrs"
|
|
387
|
+
@mouseleave="clearHoverPreview"
|
|
210
388
|
>
|
|
211
|
-
<li v-if="filteredCountries.length === 0" class="px-3 text-sm text-gray-700">
|
|
212
|
-
{{ noResultsLabel }}
|
|
213
|
-
</li>
|
|
214
389
|
<li
|
|
215
|
-
v-for="option in filteredCountries"
|
|
390
|
+
v-for="(option, index) in filteredCountries"
|
|
216
391
|
:key="option.value"
|
|
392
|
+
:id="`fds-phone-country-option-${index}`"
|
|
217
393
|
role="option"
|
|
218
|
-
:id="`fds-country-${option.value}`"
|
|
219
394
|
:aria-selected="option.value === country"
|
|
220
|
-
class="mb-0 border-b border-gray-100"
|
|
395
|
+
class="mb-0 border-b border-gray-100 hover:bg-blue_t-100 active:bg-blue_t-200 last:border-b-0!"
|
|
396
|
+
:class="{
|
|
397
|
+
'border-gray-300': index === lastPrioritizedFilteredIndex,
|
|
398
|
+
'bg-blue_t-100': activeIndex === index,
|
|
399
|
+
}"
|
|
221
400
|
>
|
|
222
401
|
<button
|
|
223
402
|
type="button"
|
|
224
|
-
|
|
225
|
-
|
|
403
|
+
tabindex="-1"
|
|
404
|
+
class="flex w-full items-center justify-between gap-2 py-4 px-3 text-left cursor-pointer"
|
|
405
|
+
@mouseenter="onOptionMouseEnter(option)"
|
|
406
|
+
@mouseleave="onOptionMouseLeave"
|
|
407
|
+
@mousedown.prevent
|
|
408
|
+
@click="selectCountry(option)"
|
|
226
409
|
>
|
|
227
|
-
<span class="
|
|
228
|
-
|
|
229
|
-
|
|
410
|
+
<span class="flex items-center gap-2">
|
|
411
|
+
<span class="flex items-center gap-3">
|
|
412
|
+
<span class="shrink-0 text-xl leading-none" aria-hidden="true">
|
|
413
|
+
{{ countryCodeToFlag(option.value) }}
|
|
414
|
+
</span>
|
|
415
|
+
{{ option.countryName }}
|
|
416
|
+
</span>
|
|
417
|
+
<span class="text-gray-600">({{ t('common.plus') }}{{ option.countryCode }})</span>
|
|
230
418
|
</span>
|
|
231
|
-
<span
|
|
232
|
-
|
|
233
|
-
<FdsIcon v-if="option.value === country" name="check" :size="20" class="fill-green-600" />
|
|
419
|
+
<span aria-hidden="true">
|
|
420
|
+
<FdsIcon v-if="option.value === country" name="check" :size="24" class="fill-green-600" />
|
|
234
421
|
</span>
|
|
235
422
|
</button>
|
|
236
423
|
</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` β πΈπͺ */
|