@marianmeres/stuic 3.84.0 → 3.85.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.
- package/dist/components/Checkout/CheckoutAddressForm.svelte +43 -4
- package/dist/components/Checkout/CheckoutAddressForm.svelte.d.ts +28 -3
- package/dist/components/Checkout/_internal/checkout-i18n-defaults.js +2 -0
- package/dist/components/Input/FieldCountry.svelte +326 -0
- package/dist/components/Input/FieldCountry.svelte.d.ts +59 -0
- package/dist/components/Input/index.css +27 -0
- package/dist/components/Input/index.d.ts +2 -1
- package/dist/components/Input/index.js +2 -1
- package/package.json +1 -1
|
@@ -7,9 +7,17 @@
|
|
|
7
7
|
CheckoutValidationError,
|
|
8
8
|
} from "./_internal/checkout-types.js";
|
|
9
9
|
import type { Props as FieldPhoneNumberProps } from "../Input/FieldPhoneNumber.svelte";
|
|
10
|
+
import type { Props as FieldCountryProps } from "../Input/FieldCountry.svelte";
|
|
11
|
+
import type { Country } from "../Input/_internal/countries.js";
|
|
10
12
|
|
|
11
13
|
export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "children"> {
|
|
12
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Bindable address data. Default: createEmptyAddress().
|
|
16
|
+
*
|
|
17
|
+
* Note: `address.country` is expected to be an ISO alpha-2 code (e.g. "SK")
|
|
18
|
+
* when used with the built-in country selector. Pre-existing free-text values
|
|
19
|
+
* won't match a country and will render as unselected.
|
|
20
|
+
*/
|
|
13
21
|
address?: CheckoutAddressData;
|
|
14
22
|
|
|
15
23
|
/**
|
|
@@ -42,12 +50,13 @@
|
|
|
42
50
|
|
|
43
51
|
/**
|
|
44
52
|
* Override the country field with a custom selector.
|
|
45
|
-
* When provided, replaces the default
|
|
53
|
+
* When provided, replaces the default searchable country picker.
|
|
54
|
+
* `value` is an ISO alpha-2 code.
|
|
46
55
|
*/
|
|
47
56
|
countryField?: Snippet<
|
|
48
57
|
[
|
|
49
58
|
{
|
|
50
|
-
/** Current country value */
|
|
59
|
+
/** Current country value (ISO alpha-2 code) */
|
|
51
60
|
value: string;
|
|
52
61
|
/** Called when country changes */
|
|
53
62
|
onchange: (value: string) => void;
|
|
@@ -61,9 +70,29 @@
|
|
|
61
70
|
]
|
|
62
71
|
>;
|
|
63
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Restrict the country selector to a specific list. Accepts ISO codes
|
|
75
|
+
* or already-resolved Country objects. Default: all countries.
|
|
76
|
+
*/
|
|
77
|
+
countryList?: Country[] | string[];
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* ISO codes pinned at the top of the country dropdown, above a divider.
|
|
81
|
+
*/
|
|
82
|
+
preferredCountries?: string[];
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Override displayed country names. Keys are ISO alpha-2 codes,
|
|
86
|
+
* values are the localized name. Missing keys fall back to English.
|
|
87
|
+
*/
|
|
88
|
+
countryNames?: Record<string, string>;
|
|
89
|
+
|
|
64
90
|
/** Extra props forwarded to the internal FieldPhoneNumber component. */
|
|
65
91
|
phoneFieldProps?: Partial<FieldPhoneNumberProps>;
|
|
66
92
|
|
|
93
|
+
/** Extra props forwarded to the internal FieldCountry component. */
|
|
94
|
+
countryFieldProps?: Partial<FieldCountryProps>;
|
|
95
|
+
|
|
67
96
|
t?: TranslateFn;
|
|
68
97
|
unstyled?: boolean;
|
|
69
98
|
class?: string;
|
|
@@ -77,6 +106,7 @@
|
|
|
77
106
|
import { createEmptyAddress } from "./_internal/checkout-utils.js";
|
|
78
107
|
import FieldInput from "../Input/FieldInput.svelte";
|
|
79
108
|
import FieldPhoneNumber from "../Input/FieldPhoneNumber.svelte";
|
|
109
|
+
import FieldCountry from "../Input/FieldCountry.svelte";
|
|
80
110
|
import { validatePhoneNumber } from "../Input/phone-validation.js";
|
|
81
111
|
|
|
82
112
|
const DEFAULT_REQUIRED = ["name", "street", "city", "postal_code", "country"];
|
|
@@ -88,7 +118,11 @@
|
|
|
88
118
|
fields,
|
|
89
119
|
requiredFields = DEFAULT_REQUIRED,
|
|
90
120
|
countryField,
|
|
121
|
+
countryList,
|
|
122
|
+
preferredCountries,
|
|
123
|
+
countryNames,
|
|
91
124
|
phoneFieldProps,
|
|
125
|
+
countryFieldProps,
|
|
92
126
|
t: tProp,
|
|
93
127
|
unstyled = false,
|
|
94
128
|
class: classProp,
|
|
@@ -235,7 +269,7 @@
|
|
|
235
269
|
})}
|
|
236
270
|
{:else}
|
|
237
271
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
238
|
-
<
|
|
272
|
+
<FieldCountry
|
|
239
273
|
bind:value={address.country}
|
|
240
274
|
label={t("checkout.address.country_label")}
|
|
241
275
|
placeholder={t("checkout.address.country_placeholder")}
|
|
@@ -243,11 +277,16 @@
|
|
|
243
277
|
name="{label}-country"
|
|
244
278
|
id="{label}-country"
|
|
245
279
|
labelLeftBreakpoint={0}
|
|
280
|
+
{countryList}
|
|
281
|
+
{preferredCountries}
|
|
282
|
+
{countryNames}
|
|
283
|
+
t={tProp}
|
|
246
284
|
validate={{
|
|
247
285
|
customValidator(val) {
|
|
248
286
|
return fieldError("country") || "";
|
|
249
287
|
},
|
|
250
288
|
}}
|
|
289
|
+
{...countryFieldProps}
|
|
251
290
|
/>
|
|
252
291
|
{/if}
|
|
253
292
|
{/if}
|
|
@@ -3,8 +3,16 @@ import type { HTMLAttributes } from "svelte/elements";
|
|
|
3
3
|
import type { TranslateFn } from "../../types.js";
|
|
4
4
|
import type { CheckoutAddressData, CheckoutValidationError } from "./_internal/checkout-types.js";
|
|
5
5
|
import type { Props as FieldPhoneNumberProps } from "../Input/FieldPhoneNumber.svelte";
|
|
6
|
+
import type { Props as FieldCountryProps } from "../Input/FieldCountry.svelte";
|
|
7
|
+
import type { Country } from "../Input/_internal/countries.js";
|
|
6
8
|
export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "children"> {
|
|
7
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Bindable address data. Default: createEmptyAddress().
|
|
11
|
+
*
|
|
12
|
+
* Note: `address.country` is expected to be an ISO alpha-2 code (e.g. "SK")
|
|
13
|
+
* when used with the built-in country selector. Pre-existing free-text values
|
|
14
|
+
* won't match a country and will render as unselected.
|
|
15
|
+
*/
|
|
8
16
|
address?: CheckoutAddressData;
|
|
9
17
|
/**
|
|
10
18
|
* Label prefix used for:
|
|
@@ -32,11 +40,12 @@ export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "childr
|
|
|
32
40
|
requiredFields?: string[];
|
|
33
41
|
/**
|
|
34
42
|
* Override the country field with a custom selector.
|
|
35
|
-
* When provided, replaces the default
|
|
43
|
+
* When provided, replaces the default searchable country picker.
|
|
44
|
+
* `value` is an ISO alpha-2 code.
|
|
36
45
|
*/
|
|
37
46
|
countryField?: Snippet<[
|
|
38
47
|
{
|
|
39
|
-
/** Current country value */
|
|
48
|
+
/** Current country value (ISO alpha-2 code) */
|
|
40
49
|
value: string;
|
|
41
50
|
/** Called when country changes */
|
|
42
51
|
onchange: (value: string) => void;
|
|
@@ -48,8 +57,24 @@ export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "childr
|
|
|
48
57
|
id: string;
|
|
49
58
|
}
|
|
50
59
|
]>;
|
|
60
|
+
/**
|
|
61
|
+
* Restrict the country selector to a specific list. Accepts ISO codes
|
|
62
|
+
* or already-resolved Country objects. Default: all countries.
|
|
63
|
+
*/
|
|
64
|
+
countryList?: Country[] | string[];
|
|
65
|
+
/**
|
|
66
|
+
* ISO codes pinned at the top of the country dropdown, above a divider.
|
|
67
|
+
*/
|
|
68
|
+
preferredCountries?: string[];
|
|
69
|
+
/**
|
|
70
|
+
* Override displayed country names. Keys are ISO alpha-2 codes,
|
|
71
|
+
* values are the localized name. Missing keys fall back to English.
|
|
72
|
+
*/
|
|
73
|
+
countryNames?: Record<string, string>;
|
|
51
74
|
/** Extra props forwarded to the internal FieldPhoneNumber component. */
|
|
52
75
|
phoneFieldProps?: Partial<FieldPhoneNumberProps>;
|
|
76
|
+
/** Extra props forwarded to the internal FieldCountry component. */
|
|
77
|
+
countryFieldProps?: Partial<FieldCountryProps>;
|
|
53
78
|
t?: TranslateFn;
|
|
54
79
|
unstyled?: boolean;
|
|
55
80
|
class?: string;
|
|
@@ -70,6 +70,8 @@ const DEFAULTS = {
|
|
|
70
70
|
"checkout.address.postal_code_placeholder": "",
|
|
71
71
|
"checkout.address.country_label": "Country",
|
|
72
72
|
"checkout.address.country_placeholder": "",
|
|
73
|
+
"checkout.address.country_search_placeholder": "Search country...",
|
|
74
|
+
"checkout.address.country_no_results": "No country found",
|
|
73
75
|
"checkout.address.phone_label": "Phone",
|
|
74
76
|
"checkout.address.phone_placeholder": "",
|
|
75
77
|
"checkout.address.required_marker": "*",
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
<script lang="ts" module>
|
|
2
|
+
import type { Snippet } from "svelte";
|
|
3
|
+
import type { ValidateOptions } from "../../actions/validate.svelte.js";
|
|
4
|
+
import type { TranslateFn } from "../../types.js";
|
|
5
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
6
|
+
import type { Country } from "./_internal/countries.js";
|
|
7
|
+
import type { InputWrapClassProps } from "./types.js";
|
|
8
|
+
|
|
9
|
+
type SnippetWithId = Snippet<[{ id: string }]>;
|
|
10
|
+
|
|
11
|
+
export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
12
|
+
/** Selected country ISO alpha-2 code (e.g. "SK"). Bindable. Empty = unselected. */
|
|
13
|
+
value?: string;
|
|
14
|
+
|
|
15
|
+
/** Called whenever the selection changes. */
|
|
16
|
+
onChange?: (iso: string) => void;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Restrict the list to specific countries. Accepts either ISO codes or
|
|
20
|
+
* already-resolved Country objects. Default: all COUNTRIES.
|
|
21
|
+
*/
|
|
22
|
+
countryList?: Country[] | string[];
|
|
23
|
+
|
|
24
|
+
/** ISO codes to pin at the top of the dropdown, above a divider. */
|
|
25
|
+
preferredCountries?: string[];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Override displayed country names. Keys are ISO alpha-2 codes,
|
|
29
|
+
* values are the localized name. Missing keys fall back to the English
|
|
30
|
+
* name from countries.ts.
|
|
31
|
+
*/
|
|
32
|
+
countryNames?: Record<string, string>;
|
|
33
|
+
|
|
34
|
+
/** Show country flag emoji in dropdown items. Default: true. */
|
|
35
|
+
flags?: boolean;
|
|
36
|
+
|
|
37
|
+
/** Hidden input name (enables form submission + validation). */
|
|
38
|
+
name?: string;
|
|
39
|
+
id?: string;
|
|
40
|
+
tabindex?: number;
|
|
41
|
+
placeholder?: string;
|
|
42
|
+
required?: boolean;
|
|
43
|
+
disabled?: boolean;
|
|
44
|
+
|
|
45
|
+
label?: SnippetWithId | THC;
|
|
46
|
+
description?: SnippetWithId | THC;
|
|
47
|
+
class?: string;
|
|
48
|
+
renderSize?: "sm" | "md" | "lg" | string;
|
|
49
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
50
|
+
|
|
51
|
+
labelAfter?: SnippetWithId | THC;
|
|
52
|
+
inputBefore?: SnippetWithId | THC;
|
|
53
|
+
inputAfter?: SnippetWithId | THC;
|
|
54
|
+
inputBelow?: SnippetWithId | THC;
|
|
55
|
+
below?: SnippetWithId | THC;
|
|
56
|
+
labelLeft?: boolean;
|
|
57
|
+
labelLeftWidth?: "normal" | "wide";
|
|
58
|
+
labelLeftBreakpoint?: number;
|
|
59
|
+
|
|
60
|
+
/** Classes for the underlying trigger <button> element. */
|
|
61
|
+
classInput?: string;
|
|
62
|
+
/** Classes for the dropdown popover. */
|
|
63
|
+
classDropdown?: string;
|
|
64
|
+
style?: string;
|
|
65
|
+
|
|
66
|
+
t?: TranslateFn;
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<script lang="ts">
|
|
71
|
+
import {
|
|
72
|
+
validate as validateAction,
|
|
73
|
+
type ValidationResult,
|
|
74
|
+
} from "../../actions/validate.svelte.js";
|
|
75
|
+
import { getId } from "../../utils/get-id.js";
|
|
76
|
+
import { twMerge } from "../../utils/tw-merge.js";
|
|
77
|
+
import { iconChevronDown } from "../../icons/index.js";
|
|
78
|
+
import DropdownMenu, {
|
|
79
|
+
type DropdownMenuActionItem,
|
|
80
|
+
type DropdownMenuItem,
|
|
81
|
+
type DropdownMenuSearchConfig,
|
|
82
|
+
} from "../DropdownMenu/DropdownMenu.svelte";
|
|
83
|
+
import InputWrap from "./_internal/InputWrap.svelte";
|
|
84
|
+
import { COUNTRIES, ISO_MAP } from "./_internal/countries.js";
|
|
85
|
+
|
|
86
|
+
let {
|
|
87
|
+
value = $bindable(""),
|
|
88
|
+
onChange,
|
|
89
|
+
countryList: countryListProp,
|
|
90
|
+
preferredCountries,
|
|
91
|
+
countryNames,
|
|
92
|
+
flags = true,
|
|
93
|
+
//
|
|
94
|
+
name,
|
|
95
|
+
id = getId(),
|
|
96
|
+
tabindex = 0,
|
|
97
|
+
placeholder,
|
|
98
|
+
required = false,
|
|
99
|
+
disabled = false,
|
|
100
|
+
//
|
|
101
|
+
label,
|
|
102
|
+
description,
|
|
103
|
+
class: classProp,
|
|
104
|
+
renderSize = "md",
|
|
105
|
+
validate,
|
|
106
|
+
//
|
|
107
|
+
labelAfter,
|
|
108
|
+
inputBefore,
|
|
109
|
+
inputAfter,
|
|
110
|
+
inputBelow,
|
|
111
|
+
below,
|
|
112
|
+
labelLeft = false,
|
|
113
|
+
labelLeftWidth = "normal",
|
|
114
|
+
labelLeftBreakpoint = 480,
|
|
115
|
+
//
|
|
116
|
+
classInput,
|
|
117
|
+
classDropdown,
|
|
118
|
+
classLabel,
|
|
119
|
+
classLabelBox,
|
|
120
|
+
classInputBox,
|
|
121
|
+
classInputBoxWrap,
|
|
122
|
+
classInputBoxWrapInvalid,
|
|
123
|
+
classDescBox,
|
|
124
|
+
classDescBoxToggle,
|
|
125
|
+
classBelowBox,
|
|
126
|
+
classValidationBox,
|
|
127
|
+
style,
|
|
128
|
+
//
|
|
129
|
+
t,
|
|
130
|
+
...rest
|
|
131
|
+
}: Props = $props();
|
|
132
|
+
|
|
133
|
+
let isOpen = $state(false);
|
|
134
|
+
let hiddenInputEl: HTMLInputElement | undefined = $state();
|
|
135
|
+
let validation: ValidationResult | undefined = $state();
|
|
136
|
+
const setValidationResult = (res: ValidationResult) => (validation = res);
|
|
137
|
+
|
|
138
|
+
function localizedName(c: Country): string {
|
|
139
|
+
return countryNames?.[c.iso] ?? c.name;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// Resolve the working country list (accept ISO codes or Country objects).
|
|
143
|
+
let resolvedCountries: Country[] = $derived.by(() => {
|
|
144
|
+
if (!countryListProp) return COUNTRIES;
|
|
145
|
+
if (countryListProp.length === 0) return [];
|
|
146
|
+
if (typeof countryListProp[0] === "string") {
|
|
147
|
+
const set = new Set(
|
|
148
|
+
(countryListProp as string[]).map((c) => c.toUpperCase())
|
|
149
|
+
);
|
|
150
|
+
return COUNTRIES.filter((c) => set.has(c.iso));
|
|
151
|
+
}
|
|
152
|
+
return countryListProp as Country[];
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// Sort resolved list alphabetically by displayed name (locale-aware).
|
|
156
|
+
let sortedCountries: Country[] = $derived(
|
|
157
|
+
[...resolvedCountries].sort((a, b) =>
|
|
158
|
+
localizedName(a).localeCompare(localizedName(b))
|
|
159
|
+
)
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
let selectedCountry: Country | undefined = $derived(
|
|
163
|
+
value ? ISO_MAP.get(value.toUpperCase()) : undefined
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
function countryToItem(c: Country): DropdownMenuActionItem {
|
|
167
|
+
const name = localizedName(c);
|
|
168
|
+
const prefix = flags ? `${c.flag} ` : "";
|
|
169
|
+
return {
|
|
170
|
+
type: "action",
|
|
171
|
+
id: c.iso,
|
|
172
|
+
label: `${prefix}${name}`,
|
|
173
|
+
onSelect: () => {
|
|
174
|
+
value = c.iso;
|
|
175
|
+
onChange?.(c.iso);
|
|
176
|
+
// Trigger change on hidden input so validation runs.
|
|
177
|
+
hiddenInputEl?.dispatchEvent(new Event("change", { bubbles: true }));
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
let items: DropdownMenuItem[] = $derived.by(() => {
|
|
183
|
+
const result: DropdownMenuItem[] = [];
|
|
184
|
+
const preferredSet = new Set(
|
|
185
|
+
preferredCountries?.map((c) => c.toUpperCase()) ?? []
|
|
186
|
+
);
|
|
187
|
+
|
|
188
|
+
if (preferredSet.size > 0) {
|
|
189
|
+
// Preserve the order given in `preferredCountries`.
|
|
190
|
+
const order = preferredCountries!.map((c) => c.toUpperCase());
|
|
191
|
+
const preferred = order
|
|
192
|
+
.map((iso) => resolvedCountries.find((c) => c.iso === iso))
|
|
193
|
+
.filter((c): c is Country => !!c);
|
|
194
|
+
preferred.forEach((c) => result.push(countryToItem(c)));
|
|
195
|
+
if (preferred.length > 0) {
|
|
196
|
+
result.push({ type: "divider", id: "__preferred-divider" });
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const rest =
|
|
201
|
+
preferredSet.size > 0
|
|
202
|
+
? sortedCountries.filter((c) => !preferredSet.has(c.iso))
|
|
203
|
+
: sortedCountries;
|
|
204
|
+
rest.forEach((c) => result.push(countryToItem(c)));
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
let searchConfig: DropdownMenuSearchConfig = $derived({
|
|
210
|
+
placeholder:
|
|
211
|
+
t?.("checkout.address.country_search_placeholder") || "Search country...",
|
|
212
|
+
strategy: "prefix",
|
|
213
|
+
getContent: (item) => {
|
|
214
|
+
const c = ISO_MAP.get(String(item.id));
|
|
215
|
+
if (!c) return String(item.id);
|
|
216
|
+
const localized = localizedName(c);
|
|
217
|
+
// Search against localized + English + ISO so typing works in either lang.
|
|
218
|
+
return `${localized} ${c.name} ${c.iso}`;
|
|
219
|
+
},
|
|
220
|
+
autoFocus: true,
|
|
221
|
+
noResultsMessage:
|
|
222
|
+
t?.("checkout.address.country_no_results") || "No country found",
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
let triggerText = $derived.by(() => {
|
|
226
|
+
if (selectedCountry) return localizedName(selectedCountry);
|
|
227
|
+
return placeholder ?? "";
|
|
228
|
+
});
|
|
229
|
+
</script>
|
|
230
|
+
|
|
231
|
+
<InputWrap
|
|
232
|
+
{id}
|
|
233
|
+
{label}
|
|
234
|
+
{description}
|
|
235
|
+
{labelAfter}
|
|
236
|
+
{inputBefore}
|
|
237
|
+
{inputAfter}
|
|
238
|
+
{inputBelow}
|
|
239
|
+
{below}
|
|
240
|
+
{required}
|
|
241
|
+
{disabled}
|
|
242
|
+
size={renderSize}
|
|
243
|
+
class={classProp}
|
|
244
|
+
{labelLeft}
|
|
245
|
+
{labelLeftWidth}
|
|
246
|
+
{labelLeftBreakpoint}
|
|
247
|
+
{classLabel}
|
|
248
|
+
{classLabelBox}
|
|
249
|
+
{classInputBox}
|
|
250
|
+
{classInputBoxWrap}
|
|
251
|
+
{classInputBoxWrapInvalid}
|
|
252
|
+
{classDescBox}
|
|
253
|
+
{classDescBoxToggle}
|
|
254
|
+
{classBelowBox}
|
|
255
|
+
{classValidationBox}
|
|
256
|
+
{validation}
|
|
257
|
+
{style}
|
|
258
|
+
>
|
|
259
|
+
<DropdownMenu
|
|
260
|
+
{items}
|
|
261
|
+
bind:isOpen
|
|
262
|
+
position="bottom-span-right"
|
|
263
|
+
search={searchConfig}
|
|
264
|
+
maxHeight="300px"
|
|
265
|
+
closeOnSelect
|
|
266
|
+
class="stuic-field-country"
|
|
267
|
+
classDropdown={twMerge("w-72 max-w-[calc(100vw-1rem)]", classDropdown)}
|
|
268
|
+
>
|
|
269
|
+
{#snippet trigger({ toggle, triggerProps })}
|
|
270
|
+
<!--
|
|
271
|
+
Spread triggerProps (ARIA wiring from DropdownMenu) first, then override
|
|
272
|
+
its `id` with our InputWrap id so the <label for={id}> activates the button
|
|
273
|
+
when clicked. DropdownMenu's internal `aria-labelledby` reference is
|
|
274
|
+
incidentally broken by this swap, but `aria-controls` + `aria-expanded`
|
|
275
|
+
on the trigger and `role="menu"` on the popover keep the menu accessible.
|
|
276
|
+
-->
|
|
277
|
+
<button
|
|
278
|
+
type="button"
|
|
279
|
+
class={twMerge(
|
|
280
|
+
"stuic-field-country-trigger",
|
|
281
|
+
"flex items-center justify-between w-full text-left cursor-pointer",
|
|
282
|
+
"px-3 py-2.5",
|
|
283
|
+
!selectedCountry && "stuic-field-country-placeholder",
|
|
284
|
+
classInput
|
|
285
|
+
)}
|
|
286
|
+
onclick={toggle}
|
|
287
|
+
{disabled}
|
|
288
|
+
{tabindex}
|
|
289
|
+
{...triggerProps}
|
|
290
|
+
{id}
|
|
291
|
+
{...rest}
|
|
292
|
+
>
|
|
293
|
+
<span class="flex-1 min-w-0 truncate">{triggerText || " "}</span>
|
|
294
|
+
<span
|
|
295
|
+
class={twMerge(
|
|
296
|
+
"transition-transform duration-150 shrink-0 ml-2 opacity-60",
|
|
297
|
+
isOpen && "rotate-180"
|
|
298
|
+
)}
|
|
299
|
+
aria-hidden="true"
|
|
300
|
+
>
|
|
301
|
+
{@html iconChevronDown({ size: 16 })}
|
|
302
|
+
</span>
|
|
303
|
+
</button>
|
|
304
|
+
{/snippet}
|
|
305
|
+
</DropdownMenu>
|
|
306
|
+
</InputWrap>
|
|
307
|
+
|
|
308
|
+
<!-- Hidden input for form submission + validation -->
|
|
309
|
+
{#if name}
|
|
310
|
+
<input
|
|
311
|
+
type="hidden"
|
|
312
|
+
{name}
|
|
313
|
+
value={value ?? ""}
|
|
314
|
+
bind:this={hiddenInputEl}
|
|
315
|
+
use:validateAction={() => {
|
|
316
|
+
const customOpts = typeof validate === "object" && validate ? validate : {};
|
|
317
|
+
return {
|
|
318
|
+
enabled: validate !== false,
|
|
319
|
+
...customOpts,
|
|
320
|
+
setValidationResult,
|
|
321
|
+
};
|
|
322
|
+
}}
|
|
323
|
+
{required}
|
|
324
|
+
{disabled}
|
|
325
|
+
/>
|
|
326
|
+
{/if}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Snippet } from "svelte";
|
|
2
|
+
import type { ValidateOptions } from "../../actions/validate.svelte.js";
|
|
3
|
+
import type { TranslateFn } from "../../types.js";
|
|
4
|
+
import type { THC } from "../Thc/Thc.svelte";
|
|
5
|
+
import type { Country } from "./_internal/countries.js";
|
|
6
|
+
import type { InputWrapClassProps } from "./types.js";
|
|
7
|
+
type SnippetWithId = Snippet<[{
|
|
8
|
+
id: string;
|
|
9
|
+
}]>;
|
|
10
|
+
export interface Props extends InputWrapClassProps, Record<string, any> {
|
|
11
|
+
/** Selected country ISO alpha-2 code (e.g. "SK"). Bindable. Empty = unselected. */
|
|
12
|
+
value?: string;
|
|
13
|
+
/** Called whenever the selection changes. */
|
|
14
|
+
onChange?: (iso: string) => void;
|
|
15
|
+
/**
|
|
16
|
+
* Restrict the list to specific countries. Accepts either ISO codes or
|
|
17
|
+
* already-resolved Country objects. Default: all COUNTRIES.
|
|
18
|
+
*/
|
|
19
|
+
countryList?: Country[] | string[];
|
|
20
|
+
/** ISO codes to pin at the top of the dropdown, above a divider. */
|
|
21
|
+
preferredCountries?: string[];
|
|
22
|
+
/**
|
|
23
|
+
* Override displayed country names. Keys are ISO alpha-2 codes,
|
|
24
|
+
* values are the localized name. Missing keys fall back to the English
|
|
25
|
+
* name from countries.ts.
|
|
26
|
+
*/
|
|
27
|
+
countryNames?: Record<string, string>;
|
|
28
|
+
/** Show country flag emoji in dropdown items. Default: true. */
|
|
29
|
+
flags?: boolean;
|
|
30
|
+
/** Hidden input name (enables form submission + validation). */
|
|
31
|
+
name?: string;
|
|
32
|
+
id?: string;
|
|
33
|
+
tabindex?: number;
|
|
34
|
+
placeholder?: string;
|
|
35
|
+
required?: boolean;
|
|
36
|
+
disabled?: boolean;
|
|
37
|
+
label?: SnippetWithId | THC;
|
|
38
|
+
description?: SnippetWithId | THC;
|
|
39
|
+
class?: string;
|
|
40
|
+
renderSize?: "sm" | "md" | "lg" | string;
|
|
41
|
+
validate?: boolean | Omit<ValidateOptions, "setValidationResult">;
|
|
42
|
+
labelAfter?: SnippetWithId | THC;
|
|
43
|
+
inputBefore?: SnippetWithId | THC;
|
|
44
|
+
inputAfter?: SnippetWithId | THC;
|
|
45
|
+
inputBelow?: SnippetWithId | THC;
|
|
46
|
+
below?: SnippetWithId | THC;
|
|
47
|
+
labelLeft?: boolean;
|
|
48
|
+
labelLeftWidth?: "normal" | "wide";
|
|
49
|
+
labelLeftBreakpoint?: number;
|
|
50
|
+
/** Classes for the underlying trigger <button> element. */
|
|
51
|
+
classInput?: string;
|
|
52
|
+
/** Classes for the dropdown popover. */
|
|
53
|
+
classDropdown?: string;
|
|
54
|
+
style?: string;
|
|
55
|
+
t?: TranslateFn;
|
|
56
|
+
}
|
|
57
|
+
declare const FieldCountry: import("svelte").Component<Props, {}, "value">;
|
|
58
|
+
type FieldCountry = ReturnType<typeof FieldCountry>;
|
|
59
|
+
export default FieldCountry;
|
|
@@ -625,4 +625,31 @@
|
|
|
625
625
|
padding-top: 0;
|
|
626
626
|
padding-bottom: 0;
|
|
627
627
|
}
|
|
628
|
+
|
|
629
|
+
/* ============================================================================
|
|
630
|
+
FIELD COUNTRY
|
|
631
|
+
============================================================================ */
|
|
632
|
+
|
|
633
|
+
/*
|
|
634
|
+
* DropdownMenu defaults to `display: inline-block` (shrink-to-fit), which
|
|
635
|
+
* makes the trigger button's `w-full` only as wide as its content — so the
|
|
636
|
+
* chevron snaps to the text instead of the right edge. Stretch the wrapper
|
|
637
|
+
* inside the InputWrap's flex container.
|
|
638
|
+
*/
|
|
639
|
+
.stuic-field-country.stuic-dropdown-menu {
|
|
640
|
+
display: block;
|
|
641
|
+
flex: 1;
|
|
642
|
+
min-width: 0;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/*
|
|
646
|
+
* Reset field-input padding leaking into the popover's search bar
|
|
647
|
+
* (mirrors the phone-prefix-picker fix — the input itself is correctly
|
|
648
|
+
* sized, but its container also gets `.stuic-dropdown-menu-search`
|
|
649
|
+
* padding which stacks on top, making the row look oversized).
|
|
650
|
+
*/
|
|
651
|
+
.stuic-field-country .stuic-dropdown-menu-search {
|
|
652
|
+
padding-top: 0;
|
|
653
|
+
padding-bottom: 0;
|
|
654
|
+
}
|
|
628
655
|
}
|
|
@@ -14,5 +14,6 @@ export { default as FieldInputLocalized, type Props as FieldInputLocalizedProps,
|
|
|
14
14
|
export { default as FieldKeyValues, type Props as FieldKeyValuesProps, type KeyValueEntry, } from "./FieldKeyValues.svelte";
|
|
15
15
|
export { default as FieldObject, type Props as FieldObjectProps, } from "./FieldObject.svelte";
|
|
16
16
|
export { default as FieldPhoneNumber, type Props as FieldPhoneNumberProps, } from "./FieldPhoneNumber.svelte";
|
|
17
|
+
export { default as FieldCountry, type Props as FieldCountryProps, } from "./FieldCountry.svelte";
|
|
17
18
|
export { validatePhoneNumber } from "./phone-validation.js";
|
|
18
|
-
export { type Country } from "./_internal/countries.js";
|
|
19
|
+
export { type Country, COUNTRIES, ISO_MAP } from "./_internal/countries.js";
|
|
@@ -14,5 +14,6 @@ export { default as FieldInputLocalized, } from "./FieldInputLocalized.svelte";
|
|
|
14
14
|
export { default as FieldKeyValues, } from "./FieldKeyValues.svelte";
|
|
15
15
|
export { default as FieldObject, } from "./FieldObject.svelte";
|
|
16
16
|
export { default as FieldPhoneNumber, } from "./FieldPhoneNumber.svelte";
|
|
17
|
+
export { default as FieldCountry, } from "./FieldCountry.svelte";
|
|
17
18
|
export { validatePhoneNumber } from "./phone-validation.js";
|
|
18
|
-
export {} from "./_internal/countries.js";
|
|
19
|
+
export { COUNTRIES, ISO_MAP } from "./_internal/countries.js";
|