@marianmeres/stuic 3.142.0 → 3.143.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 +185 -25
- package/dist/components/Checkout/CheckoutAddressForm.svelte.d.ts +61 -1
- package/dist/components/Checkout/CheckoutShippingStep.svelte +1 -0
- package/dist/components/Checkout/CheckoutShippingStep.svelte.d.ts +1 -0
- package/dist/components/Checkout/README.md +51 -0
- package/dist/components/Checkout/_internal/checkout-i18n-defaults.js +1 -0
- package/dist/components/Checkout/_internal/checkout-types.d.ts +11 -0
- package/dist/components/Checkout/index.d.ts +1 -1
- package/dist/components/Input/FieldAssets.svelte +4 -1
- package/package.json +1 -1
|
@@ -5,8 +5,10 @@
|
|
|
5
5
|
import type { TranslateFn } from "../../types.js";
|
|
6
6
|
import type { Props as FieldCountryProps } from "../Input/FieldCountry.svelte";
|
|
7
7
|
import type { Props as FieldPhoneNumberProps } from "../Input/FieldPhoneNumber.svelte";
|
|
8
|
+
import type { Props as FieldSelectProps } from "../Input/FieldSelect.svelte";
|
|
8
9
|
import type {
|
|
9
10
|
CheckoutAddressData,
|
|
11
|
+
CheckoutSubdivisionOption,
|
|
10
12
|
CheckoutValidationError,
|
|
11
13
|
} from "./_internal/checkout-types.js";
|
|
12
14
|
|
|
@@ -87,6 +89,71 @@
|
|
|
87
89
|
*/
|
|
88
90
|
countryNames?: Record<string, string>;
|
|
89
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Subdivision (state/province/region) lists keyed by UPPERCASE ISO alpha-2
|
|
94
|
+
* country code. When the currently selected `address.country` has a
|
|
95
|
+
* non-empty entry, the `state_or_region` field renders as a fixed select
|
|
96
|
+
* over these options (storing the option `code`); for every other country
|
|
97
|
+
* it stays the default free-text input, whose value is never touched.
|
|
98
|
+
* stuic ships no subdivision data — pass exactly the countries your
|
|
99
|
+
* checkout logic keys on (tax, shipping). Default: undefined (free text
|
|
100
|
+
* everywhere, current behavior).
|
|
101
|
+
*
|
|
102
|
+
* When the built-in select is active, a stored value is reconciled
|
|
103
|
+
* against the list: an exact `code` match (case-insensitive) is
|
|
104
|
+
* normalized to the canonical code, an exact `name` match ("Michigan")
|
|
105
|
+
* is rewritten to its `code` ("MI"), and anything else is left
|
|
106
|
+
* untouched and renders as unselected — surfaced by required-validation
|
|
107
|
+
* rather than destroyed. Reconciliation is skipped when the `stateField`
|
|
108
|
+
* snippet replaces the field — a custom control owns its value.
|
|
109
|
+
*
|
|
110
|
+
* Country edits made through this form flip the mode live even for a
|
|
111
|
+
* plain (non-`$state`) `address` object; external mutations of a plain
|
|
112
|
+
* object are not observable — pass a `$state` object for those.
|
|
113
|
+
*/
|
|
114
|
+
subdivisions?: Record<string, CheckoutSubdivisionOption[]>;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Whether `state_or_region` is required while the subdivision select is
|
|
118
|
+
* active (i.e. the current country has a `subdivisions` entry). Also
|
|
119
|
+
* accepts a per-country predicate receiving the UPPERCASE ISO code.
|
|
120
|
+
* Countries without a list keep the plain `requiredFields` behavior
|
|
121
|
+
* (as does a `requiredFields` entry for "state_or_region", which wins
|
|
122
|
+
* regardless). Default: true.
|
|
123
|
+
*/
|
|
124
|
+
subdivisionRequired?: boolean | ((countryIso: string) => boolean);
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Override the state/region field with a custom control (parity with
|
|
128
|
+
* `countryField`). When provided, replaces the built-in field in both
|
|
129
|
+
* modes; `options` is the active country's subdivision list, or null
|
|
130
|
+
* when the free-text input would apply.
|
|
131
|
+
*/
|
|
132
|
+
stateField?: Snippet<
|
|
133
|
+
[
|
|
134
|
+
{
|
|
135
|
+
/** Current state/region value */
|
|
136
|
+
value: string;
|
|
137
|
+
/** Called when the value changes */
|
|
138
|
+
onchange: (value: string) => void;
|
|
139
|
+
/** Error message for this field (if any) */
|
|
140
|
+
error?: string;
|
|
141
|
+
/** Field label text */
|
|
142
|
+
label: string;
|
|
143
|
+
/** HTML id attribute for the input */
|
|
144
|
+
id: string;
|
|
145
|
+
/** Active country's subdivision list, or null (free-text mode) */
|
|
146
|
+
options: CheckoutSubdivisionOption[] | null;
|
|
147
|
+
},
|
|
148
|
+
]
|
|
149
|
+
>;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Extra props forwarded to the internal FieldSelect when the subdivision
|
|
153
|
+
* select is active (parity with `countryFieldProps`).
|
|
154
|
+
*/
|
|
155
|
+
stateFieldProps?: Partial<FieldSelectProps>;
|
|
156
|
+
|
|
90
157
|
/** Extra props forwarded to the internal FieldPhoneNumber component. */
|
|
91
158
|
phoneFieldProps?: Partial<FieldPhoneNumberProps>;
|
|
92
159
|
|
|
@@ -109,6 +176,7 @@
|
|
|
109
176
|
import FieldCountry from "../Input/FieldCountry.svelte";
|
|
110
177
|
import FieldInput from "../Input/FieldInput.svelte";
|
|
111
178
|
import FieldPhoneNumber from "../Input/FieldPhoneNumber.svelte";
|
|
179
|
+
import FieldSelect from "../Input/FieldSelect.svelte";
|
|
112
180
|
import { validatePhoneNumber } from "../Input/phone-validation.js";
|
|
113
181
|
import { t_default } from "./_internal/checkout-i18n-defaults.js";
|
|
114
182
|
import { createEmptyAddress } from "./_internal/checkout-utils.js";
|
|
@@ -125,6 +193,10 @@
|
|
|
125
193
|
countryList,
|
|
126
194
|
preferredCountries,
|
|
127
195
|
countryNames,
|
|
196
|
+
subdivisions,
|
|
197
|
+
subdivisionRequired = true,
|
|
198
|
+
stateField,
|
|
199
|
+
stateFieldProps,
|
|
128
200
|
phoneFieldProps,
|
|
129
201
|
countryFieldProps,
|
|
130
202
|
t: tProp,
|
|
@@ -147,6 +219,56 @@
|
|
|
147
219
|
let containerWidth = $state(0);
|
|
148
220
|
let isSmall = $derived(containerWidth > 0 && containerWidth < 480);
|
|
149
221
|
|
|
222
|
+
// Subdivision select mode (see the `subdivisions` prop) --------------------
|
|
223
|
+
// Local mirror of `address.country` (writable derived). The select/input
|
|
224
|
+
// mode must react to country edits even when the consumer passed a plain
|
|
225
|
+
// (non-$state) address object, whose deep mutations Svelte cannot observe —
|
|
226
|
+
// so every country write that goes through this form also overrides the
|
|
227
|
+
// mirror directly, while reactive addresses and prop replacement resync it
|
|
228
|
+
// through the derived expression. External mutations of a plain object stay
|
|
229
|
+
// unobservable (true for every field).
|
|
230
|
+
let _country = $derived(address.country ?? "");
|
|
231
|
+
|
|
232
|
+
function setCountry(v: string) {
|
|
233
|
+
address.country = v;
|
|
234
|
+
_country = v;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let _countryCC = $derived(_country.trim().toUpperCase());
|
|
238
|
+
|
|
239
|
+
let _subdivisionList = $derived.by(() => {
|
|
240
|
+
const list = _countryCC ? subdivisions?.[_countryCC] : undefined;
|
|
241
|
+
return list?.length ? list : null;
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
let _subdivisionIsRequired = $derived(
|
|
245
|
+
isRequired("state_or_region") ||
|
|
246
|
+
(typeof subdivisionRequired === "function"
|
|
247
|
+
? subdivisionRequired(_countryCC)
|
|
248
|
+
: subdivisionRequired)
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
// Reconcile a pre-existing value whenever the built-in select becomes
|
|
252
|
+
// active: legacy free-text rows ("Michigan", "mi") self-heal to the
|
|
253
|
+
// canonical code ("MI"); anything unrecognized is left untouched (renders
|
|
254
|
+
// unselected, caught by required-validation) — never destroyed. Converges:
|
|
255
|
+
// the write triggers one re-run which then matches the code branch with
|
|
256
|
+
// nothing to change. Skipped entirely under the `stateField` snippet — a
|
|
257
|
+
// custom control owns its value (a controlled combobox writing in-progress
|
|
258
|
+
// text through `onchange` must not have it rewritten mid-typing).
|
|
259
|
+
$effect(() => {
|
|
260
|
+
if (stateField) return;
|
|
261
|
+
const list = _subdivisionList;
|
|
262
|
+
if (!list) return;
|
|
263
|
+
const raw = address.state_or_region ?? "";
|
|
264
|
+
const needle = raw.trim().toLowerCase();
|
|
265
|
+
if (!needle) return;
|
|
266
|
+
const hit =
|
|
267
|
+
list.find((o) => o.code.toLowerCase() === needle) ??
|
|
268
|
+
list.find((o) => o.name.trim().toLowerCase() === needle);
|
|
269
|
+
if (hit && raw !== hit.code) address.state_or_region = hit.code;
|
|
270
|
+
});
|
|
271
|
+
|
|
150
272
|
let _class = $derived(
|
|
151
273
|
unstyled ? classProp : twMerge("stuic-checkout-address", classProp)
|
|
152
274
|
);
|
|
@@ -154,12 +276,12 @@
|
|
|
154
276
|
// Imperative API ----------------------------------------------------------
|
|
155
277
|
// Field refs collected during render so consumers can trigger validation
|
|
156
278
|
// without waiting for native form submission. Refs stay undefined for
|
|
157
|
-
// fields hidden via the `fields` prop or replaced by the `countryField
|
|
158
|
-
//
|
|
279
|
+
// fields hidden via the `fields` prop or replaced by the `countryField`/
|
|
280
|
+
// `stateField` snippets — `validateAllFields` skips nullish entries.
|
|
159
281
|
let nameField = $state<FieldInput>();
|
|
160
282
|
let streetField = $state<FieldInput>();
|
|
161
283
|
let cityField = $state<FieldInput>();
|
|
162
|
-
let
|
|
284
|
+
let stateFieldRef = $state<FieldInput | FieldSelect>();
|
|
163
285
|
let postalCodeField = $state<FieldInput>();
|
|
164
286
|
let countryFieldRef = $state<FieldCountry>();
|
|
165
287
|
let phoneField = $state<FieldPhoneNumber>();
|
|
@@ -170,7 +292,7 @@
|
|
|
170
292
|
nameField,
|
|
171
293
|
streetField,
|
|
172
294
|
cityField,
|
|
173
|
-
|
|
295
|
+
stateFieldRef,
|
|
174
296
|
postalCodeField,
|
|
175
297
|
countryFieldRef,
|
|
176
298
|
phoneField,
|
|
@@ -276,22 +398,63 @@
|
|
|
276
398
|
/>
|
|
277
399
|
{/if}
|
|
278
400
|
{#if fields?.state_or_region !== false}
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
labelLeftBreakpoint={0}
|
|
285
|
-
placeholder={t("checkout.address.state_or_region_placeholder")}
|
|
286
|
-
required={isRequired("state_or_region")}
|
|
287
|
-
name="{label}-state_or_region"
|
|
288
|
-
id="{label}-state_or_region"
|
|
289
|
-
validate={{
|
|
290
|
-
customValidator(val) {
|
|
291
|
-
return fieldError("state_or_region") || "";
|
|
401
|
+
{#if stateField}
|
|
402
|
+
{@render stateField({
|
|
403
|
+
value: address.state_or_region ?? "",
|
|
404
|
+
onchange: (v) => {
|
|
405
|
+
address.state_or_region = v;
|
|
292
406
|
},
|
|
293
|
-
|
|
294
|
-
|
|
407
|
+
error: fieldError("state_or_region"),
|
|
408
|
+
label: t("checkout.address.state_or_region_label"),
|
|
409
|
+
id: `${label}-state_or_region`,
|
|
410
|
+
options: _subdivisionList,
|
|
411
|
+
})}
|
|
412
|
+
{:else if _subdivisionList}
|
|
413
|
+
<FieldSelect
|
|
414
|
+
bind:this={stateFieldRef}
|
|
415
|
+
bind:value={
|
|
416
|
+
() => address.state_or_region ?? "",
|
|
417
|
+
(v) => {
|
|
418
|
+
address.state_or_region = String(v ?? "");
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
options={[
|
|
422
|
+
{
|
|
423
|
+
label: t("checkout.address.state_or_region_select_placeholder"),
|
|
424
|
+
value: "",
|
|
425
|
+
},
|
|
426
|
+
..._subdivisionList.map((o) => ({ label: o.name, value: o.code })),
|
|
427
|
+
]}
|
|
428
|
+
label={t("checkout.address.state_or_region_label")}
|
|
429
|
+
labelLeftBreakpoint={0}
|
|
430
|
+
required={_subdivisionIsRequired}
|
|
431
|
+
name="{label}-state_or_region"
|
|
432
|
+
id="{label}-state_or_region"
|
|
433
|
+
validate={{
|
|
434
|
+
customValidator(val) {
|
|
435
|
+
return fieldError("state_or_region") || "";
|
|
436
|
+
},
|
|
437
|
+
}}
|
|
438
|
+
{...stateFieldProps}
|
|
439
|
+
/>
|
|
440
|
+
{:else}
|
|
441
|
+
<!-- svelte-ignore binding_property_non_reactive -->
|
|
442
|
+
<FieldInput
|
|
443
|
+
bind:this={stateFieldRef}
|
|
444
|
+
bind:value={address.state_or_region}
|
|
445
|
+
label={t("checkout.address.state_or_region_label")}
|
|
446
|
+
labelLeftBreakpoint={0}
|
|
447
|
+
placeholder={t("checkout.address.state_or_region_placeholder")}
|
|
448
|
+
required={isRequired("state_or_region")}
|
|
449
|
+
name="{label}-state_or_region"
|
|
450
|
+
id="{label}-state_or_region"
|
|
451
|
+
validate={{
|
|
452
|
+
customValidator(val) {
|
|
453
|
+
return fieldError("state_or_region") || "";
|
|
454
|
+
},
|
|
455
|
+
}}
|
|
456
|
+
/>
|
|
457
|
+
{/if}
|
|
295
458
|
{/if}
|
|
296
459
|
{#if fields?.postal_code !== false}
|
|
297
460
|
<!-- svelte-ignore binding_property_non_reactive -->
|
|
@@ -318,19 +481,16 @@
|
|
|
318
481
|
{#if fields?.country !== false}
|
|
319
482
|
{#if countryField}
|
|
320
483
|
{@render countryField({
|
|
321
|
-
value:
|
|
322
|
-
onchange:
|
|
323
|
-
address.country = v;
|
|
324
|
-
},
|
|
484
|
+
value: _country,
|
|
485
|
+
onchange: setCountry,
|
|
325
486
|
error: fieldError("country"),
|
|
326
487
|
label: t("checkout.address.country_label"),
|
|
327
488
|
id: `${label}-country`,
|
|
328
489
|
})}
|
|
329
490
|
{:else}
|
|
330
|
-
<!-- svelte-ignore binding_property_non_reactive -->
|
|
331
491
|
<FieldCountry
|
|
332
492
|
bind:this={countryFieldRef}
|
|
333
|
-
bind:value={
|
|
493
|
+
bind:value={() => _country, setCountry}
|
|
334
494
|
label={t("checkout.address.country_label")}
|
|
335
495
|
placeholder={t("checkout.address.country_placeholder")}
|
|
336
496
|
required={isRequired("country")}
|
|
@@ -4,7 +4,8 @@ import type { HTMLAttributes } from "svelte/elements";
|
|
|
4
4
|
import type { TranslateFn } from "../../types.js";
|
|
5
5
|
import type { Props as FieldCountryProps } from "../Input/FieldCountry.svelte";
|
|
6
6
|
import type { Props as FieldPhoneNumberProps } from "../Input/FieldPhoneNumber.svelte";
|
|
7
|
-
import type {
|
|
7
|
+
import type { Props as FieldSelectProps } from "../Input/FieldSelect.svelte";
|
|
8
|
+
import type { CheckoutAddressData, CheckoutSubdivisionOption, CheckoutValidationError } from "./_internal/checkout-types.js";
|
|
8
9
|
export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "children"> {
|
|
9
10
|
/**
|
|
10
11
|
* Bindable address data. Default: createEmptyAddress().
|
|
@@ -71,6 +72,65 @@ export interface Props extends Omit<HTMLAttributes<HTMLFieldSetElement>, "childr
|
|
|
71
72
|
* values are the localized name. Missing keys fall back to English.
|
|
72
73
|
*/
|
|
73
74
|
countryNames?: Record<string, string>;
|
|
75
|
+
/**
|
|
76
|
+
* Subdivision (state/province/region) lists keyed by UPPERCASE ISO alpha-2
|
|
77
|
+
* country code. When the currently selected `address.country` has a
|
|
78
|
+
* non-empty entry, the `state_or_region` field renders as a fixed select
|
|
79
|
+
* over these options (storing the option `code`); for every other country
|
|
80
|
+
* it stays the default free-text input, whose value is never touched.
|
|
81
|
+
* stuic ships no subdivision data — pass exactly the countries your
|
|
82
|
+
* checkout logic keys on (tax, shipping). Default: undefined (free text
|
|
83
|
+
* everywhere, current behavior).
|
|
84
|
+
*
|
|
85
|
+
* When the built-in select is active, a stored value is reconciled
|
|
86
|
+
* against the list: an exact `code` match (case-insensitive) is
|
|
87
|
+
* normalized to the canonical code, an exact `name` match ("Michigan")
|
|
88
|
+
* is rewritten to its `code` ("MI"), and anything else is left
|
|
89
|
+
* untouched and renders as unselected — surfaced by required-validation
|
|
90
|
+
* rather than destroyed. Reconciliation is skipped when the `stateField`
|
|
91
|
+
* snippet replaces the field — a custom control owns its value.
|
|
92
|
+
*
|
|
93
|
+
* Country edits made through this form flip the mode live even for a
|
|
94
|
+
* plain (non-`$state`) `address` object; external mutations of a plain
|
|
95
|
+
* object are not observable — pass a `$state` object for those.
|
|
96
|
+
*/
|
|
97
|
+
subdivisions?: Record<string, CheckoutSubdivisionOption[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Whether `state_or_region` is required while the subdivision select is
|
|
100
|
+
* active (i.e. the current country has a `subdivisions` entry). Also
|
|
101
|
+
* accepts a per-country predicate receiving the UPPERCASE ISO code.
|
|
102
|
+
* Countries without a list keep the plain `requiredFields` behavior
|
|
103
|
+
* (as does a `requiredFields` entry for "state_or_region", which wins
|
|
104
|
+
* regardless). Default: true.
|
|
105
|
+
*/
|
|
106
|
+
subdivisionRequired?: boolean | ((countryIso: string) => boolean);
|
|
107
|
+
/**
|
|
108
|
+
* Override the state/region field with a custom control (parity with
|
|
109
|
+
* `countryField`). When provided, replaces the built-in field in both
|
|
110
|
+
* modes; `options` is the active country's subdivision list, or null
|
|
111
|
+
* when the free-text input would apply.
|
|
112
|
+
*/
|
|
113
|
+
stateField?: Snippet<[
|
|
114
|
+
{
|
|
115
|
+
/** Current state/region value */
|
|
116
|
+
value: string;
|
|
117
|
+
/** Called when the value changes */
|
|
118
|
+
onchange: (value: string) => void;
|
|
119
|
+
/** Error message for this field (if any) */
|
|
120
|
+
error?: string;
|
|
121
|
+
/** Field label text */
|
|
122
|
+
label: string;
|
|
123
|
+
/** HTML id attribute for the input */
|
|
124
|
+
id: string;
|
|
125
|
+
/** Active country's subdivision list, or null (free-text mode) */
|
|
126
|
+
options: CheckoutSubdivisionOption[] | null;
|
|
127
|
+
}
|
|
128
|
+
]>;
|
|
129
|
+
/**
|
|
130
|
+
* Extra props forwarded to the internal FieldSelect when the subdivision
|
|
131
|
+
* select is active (parity with `countryFieldProps`).
|
|
132
|
+
*/
|
|
133
|
+
stateFieldProps?: Partial<FieldSelectProps>;
|
|
74
134
|
/** Extra props forwarded to the internal FieldPhoneNumber component. */
|
|
75
135
|
phoneFieldProps?: Partial<FieldPhoneNumberProps>;
|
|
76
136
|
/** Extra props forwarded to the internal FieldCountry component. */
|
|
@@ -99,6 +99,7 @@
|
|
|
99
99
|
* Props forwarded to both inner `CheckoutAddressForm` instances (shipping
|
|
100
100
|
* and, when visible, billing). Use this to configure `phoneFieldProps`,
|
|
101
101
|
* `countryFieldProps`, `preferredCountries`, `countryList`, `countryNames`,
|
|
102
|
+
* `subdivisions`, `subdivisionRequired`, `stateFieldProps`,
|
|
102
103
|
* `requiredFields`, or `fields` without replacing the entire left column
|
|
103
104
|
* via the `leftColumn` snippet.
|
|
104
105
|
*
|
|
@@ -55,6 +55,7 @@ export interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "children">
|
|
|
55
55
|
* Props forwarded to both inner `CheckoutAddressForm` instances (shipping
|
|
56
56
|
* and, when visible, billing). Use this to configure `phoneFieldProps`,
|
|
57
57
|
* `countryFieldProps`, `preferredCountries`, `countryList`, `countryNames`,
|
|
58
|
+
* `subdivisions`, `subdivisionRequired`, `stateFieldProps`,
|
|
58
59
|
* `requiredFields`, or `fields` without replacing the entire left column
|
|
59
60
|
* via the `leftColumn` snippet.
|
|
60
61
|
*
|
|
@@ -126,6 +126,57 @@ interface CheckoutValidationError {
|
|
|
126
126
|
|
|
127
127
|
The step component **does not auto-advance**. It calls `onContinue` when the user clicks "Continue"; the consumer decides whether to actually advance, retry, or show errors.
|
|
128
128
|
|
|
129
|
+
## Country-aware state/region select (`subdivisions`)
|
|
130
|
+
|
|
131
|
+
By default `CheckoutAddressForm` renders `state_or_region` as free text. For
|
|
132
|
+
countries where downstream logic keys on an exact subdivision code (US
|
|
133
|
+
sales-tax tables, shipping zones), pass `subdivisions` — lists keyed by
|
|
134
|
+
UPPERCASE ISO alpha-2 country code. When the selected country has an entry,
|
|
135
|
+
the field swaps to a fixed select storing the canonical `code`; every other
|
|
136
|
+
country keeps the free-text input, verbatim. stuic ships **no** subdivision
|
|
137
|
+
data — pass exactly what your app needs:
|
|
138
|
+
|
|
139
|
+
```svelte
|
|
140
|
+
<CheckoutAddressForm
|
|
141
|
+
bind:address
|
|
142
|
+
subdivisions={{
|
|
143
|
+
US: [
|
|
144
|
+
{ code: "AL", name: "Alabama" },
|
|
145
|
+
{ code: "AK", name: "Alaska" },
|
|
146
|
+
// ... full USPS list
|
|
147
|
+
],
|
|
148
|
+
}}
|
|
149
|
+
/>
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Behavior details:
|
|
153
|
+
|
|
154
|
+
- **Stored value is always the `code`** ("MI") — same wire shape as free text,
|
|
155
|
+
no server contract change.
|
|
156
|
+
- **Prefill reconciliation:** entering select mode with a legacy value
|
|
157
|
+
self-heals it — `"mi"` → `"MI"`, `"Michigan"` → `"MI"` (written back into
|
|
158
|
+
the bound `address`). Unrecognized values are left untouched and render as
|
|
159
|
+
unselected, surfaced by validation rather than destroyed. Applies only to
|
|
160
|
+
the built-in select — the `stateField` snippet owns its value entirely.
|
|
161
|
+
- **Country switching never clears the field** — a US → CA → US round-trip
|
|
162
|
+
restores "MI".
|
|
163
|
+
- **Required:** while the select is active the field is required by default
|
|
164
|
+
(`subdivisionRequired`, also accepts a per-country predicate
|
|
165
|
+
`(countryIso) => boolean`). Countries without a list keep the plain
|
|
166
|
+
`requiredFields` behavior.
|
|
167
|
+
- `stateFieldProps` forwards extras to the internal `FieldSelect`; the
|
|
168
|
+
`stateField` snippet replaces the field entirely (receives the active
|
|
169
|
+
`options`, or `null` in free-text mode) — parity with
|
|
170
|
+
`countryField`/`countryFieldProps`.
|
|
171
|
+
- i18n: the select's empty option uses
|
|
172
|
+
`checkout.address.state_or_region_select_placeholder` (default `"Select…"`).
|
|
173
|
+
- Forward through composite steps via
|
|
174
|
+
`CheckoutShippingStep.addressFormProps`.
|
|
175
|
+
|
|
176
|
+
Note: the `validateAddress()` utility has no access to the `subdivisions`
|
|
177
|
+
config, so it does not enforce subdivision-required — use the component's
|
|
178
|
+
imperative `validate()` (step containers already do).
|
|
179
|
+
|
|
129
180
|
## Price arithmetic
|
|
130
181
|
|
|
131
182
|
**All monetary values are integers in the smallest currency unit (cents).** This applies to `CheckoutOrderLineItem.price`, `CheckoutDeliveryOption.price`, `CheckoutDeliveryOption.free_above`, and every field in `CheckoutOrderTotals`.
|
|
@@ -67,6 +67,7 @@ const DEFAULTS = {
|
|
|
67
67
|
"checkout.address.city_placeholder": "",
|
|
68
68
|
"checkout.address.state_or_region_label": "State / Region",
|
|
69
69
|
"checkout.address.state_or_region_placeholder": "",
|
|
70
|
+
"checkout.address.state_or_region_select_placeholder": "Select…",
|
|
70
71
|
"checkout.address.postal_code_label": "Postal Code",
|
|
71
72
|
"checkout.address.postal_code_placeholder": "",
|
|
72
73
|
"checkout.address.country_label": "Country",
|
|
@@ -10,6 +10,17 @@ export interface CheckoutStep {
|
|
|
10
10
|
/** Whether this step can be navigated to (clicked). Default: true for past steps */
|
|
11
11
|
navigable?: boolean;
|
|
12
12
|
}
|
|
13
|
+
/**
|
|
14
|
+
* One selectable subdivision (state/province/region) for the
|
|
15
|
+
* `CheckoutAddressForm` `subdivisions` prop. stuic ships no subdivision
|
|
16
|
+
* data — consumers pass the lists for the countries they care about.
|
|
17
|
+
*/
|
|
18
|
+
export interface CheckoutSubdivisionOption {
|
|
19
|
+
/** Canonical stored value — ISO 3166-2 subdivision suffix / USPS code (e.g. "MI"). */
|
|
20
|
+
code: string;
|
|
21
|
+
/** Display name (e.g. "Michigan"). */
|
|
22
|
+
name: string;
|
|
23
|
+
}
|
|
13
24
|
export interface CheckoutAddressData {
|
|
14
25
|
name: string;
|
|
15
26
|
street: string;
|
|
@@ -12,5 +12,5 @@ export { default as CheckoutReviewStep, type Props as CheckoutReviewStepProps, }
|
|
|
12
12
|
export { default as CheckoutShippingStep, type Props as CheckoutShippingStepProps, } from "./CheckoutShippingStep.svelte";
|
|
13
13
|
export { default as CheckoutConfirmStep, type Props as CheckoutConfirmStepProps, } from "./CheckoutConfirmStep.svelte";
|
|
14
14
|
export { default as CheckoutCompleteStep, type Props as CheckoutCompleteStepProps, } from "./CheckoutCompleteStep.svelte";
|
|
15
|
-
export type { CheckoutStep, CheckoutAddressData, CheckoutCustomerFormData, CheckoutLoginFormData, CheckoutOrderLineItem, CheckoutOrderTotals, CheckoutDeliveryOption, CheckoutDeliverySnapshot, CheckoutOrderData, CheckoutValidationError, } from "./_internal/checkout-types.js";
|
|
15
|
+
export type { CheckoutStep, CheckoutAddressData, CheckoutSubdivisionOption, CheckoutCustomerFormData, CheckoutLoginFormData, CheckoutOrderLineItem, CheckoutOrderTotals, CheckoutDeliveryOption, CheckoutDeliverySnapshot, CheckoutOrderData, CheckoutValidationError, } from "./_internal/checkout-types.js";
|
|
16
16
|
export { defaultFormatPrice, validateEmail, validateAddress, validateCustomerForm, validateLoginForm, createEmptyAddress, createEmptyCustomerFormData, createEmptyLoginFormData, addressesEqual, } from "./_internal/checkout-utils.js";
|
|
@@ -203,7 +203,10 @@
|
|
|
203
203
|
// routing would be ambiguous, so focus (a click on the field) must decide.
|
|
204
204
|
if (paste_targets.size === 1 && !is_text_entry(active)) {
|
|
205
205
|
const [t] = paste_targets;
|
|
206
|
-
if (
|
|
206
|
+
if (
|
|
207
|
+
is_visible(t.el) &&
|
|
208
|
+
(is_unclaimed_focus(active) || shares_modal(t.el, active))
|
|
209
|
+
) {
|
|
207
210
|
t.handle(e);
|
|
208
211
|
}
|
|
209
212
|
}
|