@vtex/faststore 0.0.5 → 0.0.6
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/CHANGELOG.md +45 -0
- package/dist/actions.json +181 -0
- package/dist/catalog.json +1543 -495
- package/dist/client.d.ts +2 -1
- package/dist/client.js +2 -1
- package/dist/components.d.ts +28 -7
- package/dist/components.js +31 -12
- package/dist/docs/access-code-sign-in.md +1 -1
- package/dist/docs/address-summary.md +24 -3
- package/dist/docs/alert-dialog.md +204 -0
- package/dist/docs/billing-address.md +284 -0
- package/dist/docs/card-entry.md +250 -0
- package/dist/docs/cardholder-document.md +142 -0
- package/dist/docs/client-preferences-fieldset.md +148 -0
- package/dist/docs/client-preferences.md +2 -1
- package/dist/docs/client-profile-fieldset.md +184 -0
- package/dist/docs/client-profile.md +1 -1
- package/dist/docs/document-field.md +58 -4
- package/dist/docs/field.md +317 -0
- package/dist/docs/form.md +307 -0
- package/dist/docs/identity-edit.md +6 -6
- package/dist/docs/identity-summary.md +5 -5
- package/dist/docs/installments.md +248 -0
- package/dist/docs/my-account-profile.md +1 -0
- package/dist/docs/order-placed-payment-summary.md +138 -0
- package/dist/docs/payment-method-badges.md +232 -0
- package/dist/docs/payment-methods.md +1 -0
- package/dist/docs/payment.md +218 -0
- package/dist/docs/phone-field.md +58 -4
- package/dist/docs/place-order.md +157 -0
- package/dist/docs/save-card.md +186 -0
- package/dist/docs/saved-cards.md +218 -0
- package/dist/docs/select.md +465 -0
- package/dist/docs/shipping-address.md +110 -258
- package/dist/index.d.ts +4 -1
- package/dist/internal/client.d.ts +3 -1
- package/dist/internal/client.js +3 -1
- package/dist/matter/access-code-sign-in/access-code-sign-in.js +9 -8
- package/dist/matter/access-code-sign-in/access-code-sign-in.js.map +1 -1
- package/dist/matter/billing-address/billing-address.d.ts +165 -0
- package/dist/matter/billing-address/billing-address.js +408 -0
- package/dist/matter/billing-address/billing-address.js.map +1 -0
- package/dist/matter/card-entry/card-capture.d.ts +25 -0
- package/dist/matter/card-entry/card-capture.js +25 -0
- package/dist/matter/card-entry/card-capture.js.map +1 -0
- package/dist/matter/card-entry/card-entry.d.ts +120 -0
- package/dist/matter/card-entry/card-entry.js +832 -0
- package/dist/matter/card-entry/card-entry.js.map +1 -0
- package/dist/matter/cardholder-document/cardholder-document.d.ts +45 -0
- package/dist/matter/cardholder-document/cardholder-document.js +70 -0
- package/dist/matter/cardholder-document/cardholder-document.js.map +1 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.d.ts +77 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.js +299 -0
- package/dist/matter/client-preferences/client-preferences-fieldset.js.map +1 -0
- package/dist/matter/client-preferences/client-preferences.d.ts +3 -5
- package/dist/matter/client-preferences/client-preferences.js +6 -6
- package/dist/matter/client-preferences/client-preferences.js.map +1 -1
- package/dist/matter/client-profile/client-profile.d.ts +4 -6
- package/dist/matter/client-profile/client-profile.js +9 -6
- package/dist/matter/client-profile/client-profile.js.map +1 -1
- package/dist/matter/client-profile-form/client-profile-fieldset.d.ts +119 -0
- package/dist/matter/client-profile-form/client-profile-fieldset.js +488 -0
- package/dist/matter/client-profile-form/client-profile-fieldset.js.map +1 -0
- package/dist/matter/identity-edit/identity-edit.js +2 -2
- package/dist/matter/identity-edit/identity-edit.js.map +1 -1
- package/dist/matter/identity-summary/identity-summary.js +2 -2
- package/dist/matter/identity-summary/identity-summary.js.map +1 -1
- package/dist/matter/installments/installments-select.d.ts +42 -0
- package/dist/matter/installments/installments-select.js +167 -0
- package/dist/matter/installments/installments-select.js.map +1 -0
- package/dist/matter/installments/installments.d.ts +58 -0
- package/dist/matter/installments/installments.js +176 -0
- package/dist/matter/installments/installments.js.map +1 -0
- package/dist/matter/installments/use-installments-selection.d.ts +29 -0
- package/dist/matter/installments/use-installments-selection.js +95 -0
- package/dist/matter/installments/use-installments-selection.js.map +1 -0
- package/dist/matter/my-account-profile/my-account-profile.js +4 -4
- package/dist/matter/my-account-profile/my-account-profile.js.map +1 -1
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.d.ts +63 -0
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js +305 -0
- package/dist/matter/order-placed-payment-summary/order-placed-payment-summary.js.map +1 -0
- package/dist/matter/payment/payment-card-capture.d.ts +19 -0
- package/dist/matter/payment/payment-card-capture.js +129 -0
- package/dist/matter/payment/payment-card-capture.js.map +1 -0
- package/dist/matter/payment/payment-controller.d.ts +24 -0
- package/dist/matter/payment/payment-controller.js +107 -0
- package/dist/matter/payment/payment-controller.js.map +1 -0
- package/dist/matter/payment/payment-method-badges.d.ts +57 -0
- package/dist/matter/payment/payment-method-badges.js +192 -0
- package/dist/matter/payment/payment-method-badges.js.map +1 -0
- package/dist/matter/payment/payment.d.ts +89 -0
- package/dist/matter/payment/payment.js +263 -0
- package/dist/matter/payment/payment.js.map +1 -0
- package/dist/matter/payment/use-payment-method-selection.d.ts +302 -0
- package/dist/matter/payment/use-payment-method-selection.js +458 -0
- package/dist/matter/payment/use-payment-method-selection.js.map +1 -0
- package/dist/matter/payment/use-payment-submit-gate.d.ts +111 -0
- package/dist/matter/payment/use-payment-submit-gate.js +144 -0
- package/dist/matter/payment/use-payment-submit-gate.js.map +1 -0
- package/dist/matter/place-order/place-order.d.ts +74 -0
- package/dist/matter/place-order/place-order.js +379 -0
- package/dist/matter/place-order/place-order.js.map +1 -0
- package/dist/matter/product-list/product-list.d.ts +1 -0
- package/dist/matter/product-list/product-list.js.map +1 -1
- package/dist/matter/product-shelf/product-shelf.d.ts +1 -0
- package/dist/matter/product-shelf/product-shelf.js +2 -0
- package/dist/matter/product-shelf/product-shelf.js.map +1 -1
- package/dist/matter/region/region.js +1 -1
- package/dist/matter/region/region.js.map +1 -1
- package/dist/matter/save-card/save-card.d.ts +49 -0
- package/dist/matter/save-card/save-card.js +188 -0
- package/dist/matter/save-card/save-card.js.map +1 -0
- package/dist/matter/saved-cards/saved-cards.d.ts +79 -0
- package/dist/matter/saved-cards/saved-cards.js +387 -0
- package/dist/matter/saved-cards/saved-cards.js.map +1 -0
- package/dist/matter/shipping-address/country-descriptors.js +20 -10
- package/dist/matter/shipping-address/country-descriptors.js.map +1 -1
- package/dist/matter/shipping-address/shipping-address.d.ts +113 -174
- package/dist/matter/shipping-address/shipping-address.js +480 -791
- package/dist/matter/shipping-address/shipping-address.js.map +1 -1
- package/dist/matter/variant-selector/variant-selector.js +1 -1
- package/dist/particles/address-summary/address-summary.d.ts +24 -0
- package/dist/particles/address-summary/address-summary.js +83 -2
- package/dist/particles/address-summary/address-summary.js.map +1 -1
- package/dist/particles/alert-dialog/alert-dialog.d.ts +68 -0
- package/dist/particles/alert-dialog/alert-dialog.js +187 -0
- package/dist/particles/alert-dialog/alert-dialog.js.map +1 -0
- package/dist/particles/checkbox/checkbox.d.ts +7 -3
- package/dist/particles/checkbox/checkbox.js +4 -1
- package/dist/particles/checkbox/checkbox.js.map +1 -1
- package/dist/particles/document-field/document-field.d.ts +48 -5
- package/dist/particles/document-field/document-field.js +189 -32
- package/dist/particles/document-field/document-field.js.map +1 -1
- package/dist/particles/field/bound-field-registry.js +52 -0
- package/dist/particles/field/bound-field-registry.js.map +1 -0
- package/dist/particles/field/field.d.ts +116 -0
- package/dist/particles/field/field.js +497 -6
- package/dist/particles/field/field.js.map +1 -1
- package/dist/particles/fieldset/fieldset.js +2 -2
- package/dist/particles/fieldset/fieldset.js.map +1 -1
- package/dist/particles/form/form.d.ts +104 -0
- package/dist/particles/form/form.js +485 -0
- package/dist/particles/form/form.js.map +1 -0
- package/dist/particles/phone-field/phone-field.d.ts +38 -10
- package/dist/particles/phone-field/phone-field.js +183 -39
- package/dist/particles/phone-field/phone-field.js.map +1 -1
- package/dist/particles/postal-code-field/postal-code-field.js +2 -2
- package/dist/particles/postal-code-field/postal-code-field.js.map +1 -1
- package/dist/particles/select/field-select.d.ts +43 -0
- package/dist/particles/select/field-select.js +150 -0
- package/dist/particles/select/field-select.js.map +1 -0
- package/dist/particles/select/select.d.ts +2 -0
- package/dist/particles/select/select.js.map +1 -1
- package/dist/particles/shared/compose-refs.js +21 -0
- package/dist/particles/shared/compose-refs.js.map +1 -0
- package/dist/schema.graphql +701 -2
- package/dist/sdk/actions/analytics-action-provider.d.ts +15 -0
- package/dist/sdk/actions/analytics-action-provider.js +19 -0
- package/dist/sdk/actions/analytics-action-provider.js.map +1 -0
- package/dist/sdk/actions/index.d.ts +3 -1
- package/dist/sdk/actions/index.js +3 -1
- package/dist/sdk/actions/payment-action-provider.d.ts +22 -0
- package/dist/sdk/actions/payment-action-provider.js +26 -0
- package/dist/sdk/actions/payment-action-provider.js.map +1 -0
- package/dist/sdk/actions/types.d.ts +6 -2
- package/dist/sdk/analytics/analytics-destination.d.ts +43 -0
- package/dist/sdk/analytics/analytics-destination.js +90 -0
- package/dist/sdk/analytics/analytics-destination.js.map +1 -0
- package/dist/sdk/analytics/use-analytics-actions.d.ts +57 -0
- package/dist/sdk/analytics/use-analytics-actions.js +108 -0
- package/dist/sdk/analytics/use-analytics-actions.js.map +1 -0
- package/dist/sdk/cart/use-cart-actions.js +1 -1
- package/dist/sdk/data/entity-resolution-context.js +31 -0
- package/dist/sdk/data/entity-resolution-context.js.map +1 -0
- package/dist/sdk/data/entity-subscriber.js +18 -8
- package/dist/sdk/data/entity-subscriber.js.map +1 -1
- package/dist/sdk/data/index.js +1 -0
- package/dist/sdk/data/page-reactive-data-root.d.ts +36 -0
- package/dist/sdk/data/page-reactive-data-root.js +4 -2
- package/dist/sdk/data/page-reactive-data-root.js.map +1 -1
- package/dist/sdk/data/reactive-data-provider.js +7 -3
- package/dist/sdk/data/reactive-data-provider.js.map +1 -1
- package/dist/sdk/data/reactive-list-item-data-provider.js +12 -4
- package/dist/sdk/data/reactive-list-item-data-provider.js.map +1 -1
- package/dist/sdk/data/use-reactive-data-settled.js +39 -0
- package/dist/sdk/data/use-reactive-data-settled.js.map +1 -0
- package/dist/sdk/data/use-reactive-entity.js +5 -1
- package/dist/sdk/data/use-reactive-entity.js.map +1 -1
- package/dist/sdk/generated/fragment-types.js +2 -0
- package/dist/sdk/generated/fragment-types.js.map +1 -1
- package/dist/sdk/generated/graphql.js.map +1 -1
- package/dist/sdk/i18n/messages/en-US.js +3 -0
- package/dist/sdk/i18n/messages/en-US.json +3 -0
- package/dist/sdk/i18n/messages/en-US.json.d.ts +3 -0
- package/dist/sdk/i18n/messages/es-ES.js +3 -0
- package/dist/sdk/i18n/messages/es-ES.json +3 -0
- package/dist/sdk/i18n/messages/pt-BR.js +3 -0
- package/dist/sdk/i18n/messages/pt-BR.json +3 -0
- package/dist/sdk/identity/use-identity-actions.js +3 -2
- package/dist/sdk/identity/use-identity-actions.js.map +1 -1
- package/dist/sdk/payment/gateway-origin.js +35 -0
- package/dist/sdk/payment/gateway-origin.js.map +1 -0
- package/dist/sdk/payment/gateway.d.ts +85 -0
- package/dist/sdk/payment/gateway.js +183 -0
- package/dist/sdk/payment/gateway.js.map +1 -0
- package/dist/sdk/payment/operations.d.ts +52 -0
- package/dist/sdk/payment/operations.js +223 -0
- package/dist/sdk/payment/operations.js.map +1 -0
- package/dist/sdk/payment/security-code-mask.js +71 -0
- package/dist/sdk/payment/security-code-mask.js.map +1 -0
- package/dist/sdk/payment/submission-registry.d.ts +33 -0
- package/dist/sdk/payment/submission-registry.js +58 -0
- package/dist/sdk/payment/submission-registry.js.map +1 -0
- package/dist/sdk/payment/use-payment-actions.d.ts +78 -0
- package/dist/sdk/payment/use-payment-actions.js +276 -0
- package/dist/sdk/payment/use-payment-actions.js.map +1 -0
- package/dist/sdk/payment/use-place-order.d.ts +65 -0
- package/dist/sdk/payment/use-place-order.js +640 -0
- package/dist/sdk/payment/use-place-order.js.map +1 -0
- package/dist/sdk/shipping/use-shipping-actions.js +1 -1
- package/dist/utils/loggable-error-name.js +49 -0
- package/dist/utils/loggable-error-name.js.map +1 -0
- package/dist/utils/resolve-authored-copy.js +18 -0
- package/dist/utils/resolve-authored-copy.js.map +1 -0
- package/dist/utils/substitute-template.js +33 -0
- package/dist/utils/substitute-template.js.map +1 -0
- package/package.json +2 -1
- package/src/styles/tailwind.css +34 -0
- package/dist/docs/client-profile-form.md +0 -192
- package/dist/matter/client-profile-form/client-profile-form.d.ts +0 -126
- package/dist/matter/client-profile-form/client-profile-form.js +0 -662
- package/dist/matter/client-profile-form/client-profile-form.js.map +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ComponentDefinition } from "../../core/dist/define-component.js";
|
|
2
2
|
import "../../core/dist/index.js";
|
|
3
|
+
import { ReactNode, Ref } from "react";
|
|
3
4
|
//#region src/particles/phone-field/phone-field.d.ts
|
|
4
5
|
/**
|
|
5
6
|
* Country codes supported by PhoneField.
|
|
@@ -7,21 +8,24 @@ import "../../core/dist/index.js";
|
|
|
7
8
|
* format (Gap-06 in the PhoneField spec).
|
|
8
9
|
*/
|
|
9
10
|
export type PhoneCountry = 'US' | 'BR';
|
|
10
|
-
type PhoneFieldRequirement = 'mandatory' | 'optional'
|
|
11
|
+
type PhoneFieldRequirement = 'mandatory' | 'optional';
|
|
11
12
|
export type PhoneFieldProps = {
|
|
12
|
-
/** Controlled raw (unmasked) phone value. */
|
|
13
|
+
/** Controlled raw (unmasked) phone value. Unused on the bound path — react-hook-form owns the value. */
|
|
13
14
|
value?: string;
|
|
14
|
-
/** Uncontrolled initial raw value. */
|
|
15
|
+
/** Uncontrolled initial raw value. Unused on the bound path — the field contract's `defaultValue` seeds it instead. */
|
|
15
16
|
defaultValue?: string;
|
|
16
17
|
/** Called with the raw (unmasked) value on every change. */
|
|
17
18
|
onValueChange?: (value: string) => void;
|
|
18
19
|
/** Selects the phone mask and validation rule. */
|
|
19
20
|
country?: PhoneCountry;
|
|
20
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Visible field label. No signature default (Decision 5 of the wrapped-field-particles spec) — an
|
|
23
|
+
* omitted label falls back to the field contract's `label` when bound, and to `'Phone'` when not.
|
|
24
|
+
*/
|
|
21
25
|
label?: string;
|
|
22
|
-
/** Disables the field when true. */
|
|
26
|
+
/** Disables the field when true. Falls back to the field contract's `disabled` when bound. */
|
|
23
27
|
isDisabled?: boolean;
|
|
24
|
-
/** Input placeholder text. */
|
|
28
|
+
/** Input placeholder text. Falls back to the field contract's `placeholder` when bound. */
|
|
25
29
|
placeholder?: string;
|
|
26
30
|
/**
|
|
27
31
|
* Externally-controlled invalid state. When provided, overrides the
|
|
@@ -38,18 +42,31 @@ export type PhoneFieldProps = {
|
|
|
38
42
|
/** Error string shown when the mandatory field is left empty. */
|
|
39
43
|
errorRequired?: string;
|
|
40
44
|
/**
|
|
41
|
-
*
|
|
45
|
+
* Raises the field to required. It only ever raises: bound to a fieldset whose rule already says
|
|
46
|
+
* required, `'optional'` does not lower it.
|
|
42
47
|
*
|
|
43
|
-
* - `'mandatory'
|
|
48
|
+
* - `'mandatory'`: empty input blocks submission.
|
|
44
49
|
* - `'optional'`: empty input is allowed; format is still checked when a
|
|
45
50
|
* value is present.
|
|
46
|
-
* -
|
|
51
|
+
* - unset: no opinion, so a phone with no baseline is optional.
|
|
47
52
|
*/
|
|
48
53
|
requirement?: PhoneFieldRequirement;
|
|
49
54
|
/** Called after the field loses focus (post-validation). */
|
|
50
55
|
onBlur?: () => void;
|
|
51
56
|
/** Additional class applied to the root element. */
|
|
52
57
|
className?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Lands on the masked `<input type="tel">` the particle renders. Composed with react-hook-form's own
|
|
60
|
+
* ref on the bound path — a caller ref and react-hook-form's ref are both honoured.
|
|
61
|
+
*/
|
|
62
|
+
ref?: Ref<HTMLInputElement>;
|
|
63
|
+
/**
|
|
64
|
+
* Given, replaces the default `Field.Label` + `PhoneFieldInput` + `Field.Error` anatomy — any order,
|
|
65
|
+
* any subset. `PhoneFieldInput` is the one part that carries the mask, the composed ref and the
|
|
66
|
+
* particle's own value/handlers; a composed generic `Field.Control` would call `useController` a
|
|
67
|
+
* second time for the same name (RFC's named, accepted double-binder hole).
|
|
68
|
+
*/
|
|
69
|
+
children?: ReactNode;
|
|
53
70
|
};
|
|
54
71
|
export declare const PhoneFieldDefinition: ComponentDefinition<PhoneFieldProps>;
|
|
55
72
|
/**
|
|
@@ -61,7 +78,18 @@ export declare const PhoneFieldDefinition: ComponentDefinition<PhoneFieldProps>;
|
|
|
61
78
|
* US/BR masking, blur validation, and the `data-fs-phone-field*` attributes (plus the country variant
|
|
62
79
|
* attribute) are kept; error strings remain props (a documented i18n divergence — the country tables
|
|
63
80
|
* are internal, the copy is owner-supplied).
|
|
81
|
+
*
|
|
82
|
+
* Reads the field contract through `useField()` and `useFormContext()` and dispatches to a bound or an
|
|
83
|
+
* unbound sub-component — "bound" means both are present. Every react-hook-form hook lives in
|
|
84
|
+
* `BoundPhoneField`, so hook order never changes under a mounted component. Both sub-components render
|
|
85
|
+
* the same `PhoneFieldView`, so the two paths cannot drift into different DOM.
|
|
64
86
|
*/
|
|
65
|
-
export declare function PhoneField({
|
|
87
|
+
export declare function PhoneField({ requirement, ...props }: PhoneFieldProps): ReactNode;
|
|
88
|
+
export type PhoneFieldInputProps = {
|
|
89
|
+
className?: string;
|
|
90
|
+
};
|
|
91
|
+
export declare const PhoneFieldInputDefinition: ComponentDefinition<PhoneFieldInputProps>;
|
|
92
|
+
/** Renders the masked `<input>`, reading its value/handlers/ref from the enclosing `PhoneField`. */
|
|
93
|
+
export declare function PhoneFieldInput({ className }: PhoneFieldInputProps): ReactNode;
|
|
66
94
|
//#endregion
|
|
67
95
|
//# sourceMappingURL=phone-field.d.ts.map
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { defineComponent } from "../../core/dist/define-component.js";
|
|
3
3
|
import "../../core/dist/index.js";
|
|
4
|
-
import {
|
|
4
|
+
import { FieldBinding, TEXT_FIELD_VALUE, mergeFieldValidator, useBoundField, useField } from "../field/field.js";
|
|
5
|
+
import { useComposedRef } from "../shared/compose-refs.js";
|
|
5
6
|
import { Input } from "../input/input.js";
|
|
6
|
-
import {
|
|
7
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { createContext, useContext, useMemo, useState } from "react";
|
|
8
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
9
|
+
import { Field } from "@base-ui/react/field";
|
|
10
|
+
import { useFormContext, useFormState } from "react-hook-form";
|
|
8
11
|
//#region src/particles/phone-field/phone-field.tsx
|
|
9
12
|
/**
|
|
10
13
|
* Returns true when `value` starts with the E.164 or bare country-code prefix
|
|
@@ -121,8 +124,8 @@ function validatePhoneNumber(input, country) {
|
|
|
121
124
|
* Normalizes a valid raw phone value to E.164 for persistence (legacy checkout parity): the
|
|
122
125
|
* particle emits local digits ("2015551234") or a "+"-prefixed string; the profile stores
|
|
123
126
|
* "+12015551234". Only call this on values `validatePhoneNumber` has already accepted — shared by
|
|
124
|
-
* every binding that persists a phone value (`ClientProfilePhoneControl`, `
|
|
125
|
-
* there is exactly one normalization implementation.
|
|
127
|
+
* every binding that persists a phone value (`ClientProfilePhoneControl`, `ClientProfileFieldset`)
|
|
128
|
+
* so there is exactly one normalization implementation.
|
|
126
129
|
*/
|
|
127
130
|
function toE164(raw, country) {
|
|
128
131
|
const unmasked = removePhoneMask(raw, country);
|
|
@@ -135,6 +138,7 @@ const PhoneFieldDefinition = defineComponent({
|
|
|
135
138
|
name: "PhoneField",
|
|
136
139
|
importPath: "@vtex/faststore/components",
|
|
137
140
|
component: PhoneField,
|
|
141
|
+
providesContext: "phone-field",
|
|
138
142
|
editor: {
|
|
139
143
|
label: "Phone Field",
|
|
140
144
|
category: "action"
|
|
@@ -151,8 +155,7 @@ const PhoneFieldDefinition = defineComponent({
|
|
|
151
155
|
label: "Label"
|
|
152
156
|
},
|
|
153
157
|
requirement: {
|
|
154
|
-
type: "
|
|
155
|
-
default: "mandatory",
|
|
158
|
+
type: ["mandatory", "optional"],
|
|
156
159
|
label: "Requirement"
|
|
157
160
|
},
|
|
158
161
|
errorInvalid: {
|
|
@@ -166,7 +169,7 @@ const PhoneFieldDefinition = defineComponent({
|
|
|
166
169
|
label: "Required error message"
|
|
167
170
|
}
|
|
168
171
|
},
|
|
169
|
-
guideline: "Locale-aware, business-agnostic phone number input composed from the field + input particles. Applies country masking, validates on blur, and exposes its value via onValueChange. Bind it to a data source (e.g. ClientProfilePhoneControl for checkout) to persist the value."
|
|
172
|
+
guideline: "Locale-aware, business-agnostic phone number input composed from the field + input particles. Applies country masking, validates on blur, and exposes its value via onValueChange. Bind it to a data source (e.g. ClientProfilePhoneControl for checkout) to persist the value. Recomposable (RFC:503): given children, they replace the default Field.Label + PhoneFieldInput + Field.Error anatomy, in any order."
|
|
170
173
|
});
|
|
171
174
|
/**
|
|
172
175
|
* Presentational, business-agnostic phone field. Composes the `field` and `input` particles: the
|
|
@@ -177,20 +180,115 @@ const PhoneFieldDefinition = defineComponent({
|
|
|
177
180
|
* US/BR masking, blur validation, and the `data-fs-phone-field*` attributes (plus the country variant
|
|
178
181
|
* attribute) are kept; error strings remain props (a documented i18n divergence — the country tables
|
|
179
182
|
* are internal, the copy is owner-supplied).
|
|
183
|
+
*
|
|
184
|
+
* Reads the field contract through `useField()` and `useFormContext()` and dispatches to a bound or an
|
|
185
|
+
* unbound sub-component — "bound" means both are present. Every react-hook-form hook lives in
|
|
186
|
+
* `BoundPhoneField`, so hook order never changes under a mounted component. Both sub-components render
|
|
187
|
+
* the same `PhoneFieldView`, so the two paths cannot drift into different DOM.
|
|
180
188
|
*/
|
|
181
|
-
function PhoneField({
|
|
189
|
+
function PhoneField({ requirement, ...props }) {
|
|
190
|
+
const contract = useField();
|
|
191
|
+
const formContext = useFormContext();
|
|
192
|
+
if (contract && formContext) return /* @__PURE__ */ jsx(BoundPhoneField, {
|
|
193
|
+
...props,
|
|
194
|
+
requirement,
|
|
195
|
+
contract
|
|
196
|
+
});
|
|
197
|
+
return /* @__PURE__ */ jsx(UnboundPhoneField, {
|
|
198
|
+
...props,
|
|
199
|
+
requirement
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
const PhoneFieldInputContext = createContext(null);
|
|
203
|
+
function usePhoneFieldInputContext() {
|
|
204
|
+
const context = useContext(PhoneFieldInputContext);
|
|
205
|
+
if (!context) throw new Error("PhoneFieldInput must be a descendant of PhoneField.");
|
|
206
|
+
return context;
|
|
207
|
+
}
|
|
208
|
+
const PhoneFieldInputDefinition = defineComponent({
|
|
209
|
+
name: "PhoneFieldInput",
|
|
210
|
+
importPath: "@vtex/faststore/components",
|
|
211
|
+
component: PhoneFieldInput,
|
|
212
|
+
requiresContext: "phone-field",
|
|
213
|
+
editor: {
|
|
214
|
+
label: "Phone Field Input",
|
|
215
|
+
category: "action"
|
|
216
|
+
},
|
|
217
|
+
schema: {},
|
|
218
|
+
guideline: "The masked <input type=\"tel\"> PhoneField owns. Must sit inside PhoneField, in either the default anatomy or an author's composed children. Carries the country mask, the composed ref, and PhoneField's own onChange/onBlur — a composed Field.Control here instead re-registers the field with react-hook-form and loses the mask (unsupported, RFC's named double-binder hole)."
|
|
219
|
+
});
|
|
220
|
+
/** Renders the masked `<input>`, reading its value/handlers/ref from the enclosing `PhoneField`. */
|
|
221
|
+
function PhoneFieldInput({ className }) {
|
|
222
|
+
const { ref, value, placeholder, required, onChange, onBlur } = usePhoneFieldInputContext();
|
|
223
|
+
return /* @__PURE__ */ jsx(Input, {
|
|
224
|
+
ref,
|
|
225
|
+
"data-fs-phone-field-input": "",
|
|
226
|
+
type: "tel",
|
|
227
|
+
inputMode: "numeric",
|
|
228
|
+
"aria-required": required || void 0,
|
|
229
|
+
value,
|
|
230
|
+
onChange,
|
|
231
|
+
onBlur,
|
|
232
|
+
placeholder,
|
|
233
|
+
className
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
function PhoneFieldView({ ref, country, className, label, isDisabled, placeholder, required, maskedValue, effectiveInvalid, effectiveError, onChange, onBlur, children }) {
|
|
237
|
+
const inputContext = useMemo(() => ({
|
|
238
|
+
ref,
|
|
239
|
+
value: maskedValue,
|
|
240
|
+
placeholder,
|
|
241
|
+
required,
|
|
242
|
+
onChange,
|
|
243
|
+
onBlur
|
|
244
|
+
}), [
|
|
245
|
+
ref,
|
|
246
|
+
maskedValue,
|
|
247
|
+
placeholder,
|
|
248
|
+
required,
|
|
249
|
+
onChange,
|
|
250
|
+
onBlur
|
|
251
|
+
]);
|
|
252
|
+
return /* @__PURE__ */ jsx(Field.Root, {
|
|
253
|
+
"data-fs-phone-field-root": "",
|
|
254
|
+
"data-fs-phone-field-root-country": country,
|
|
255
|
+
"data-fs-field-root": "",
|
|
256
|
+
className,
|
|
257
|
+
invalid: effectiveInvalid,
|
|
258
|
+
disabled: isDisabled,
|
|
259
|
+
children: /* @__PURE__ */ jsx(PhoneFieldInputContext.Provider, {
|
|
260
|
+
value: inputContext,
|
|
261
|
+
children: children ?? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
262
|
+
/* @__PURE__ */ jsx(Field.Label, {
|
|
263
|
+
"data-fs-phone-field-label": "",
|
|
264
|
+
"data-fs-field-label": "",
|
|
265
|
+
children: label
|
|
266
|
+
}),
|
|
267
|
+
/* @__PURE__ */ jsx(PhoneFieldInput, {}),
|
|
268
|
+
effectiveError ? /* @__PURE__ */ jsx(Field.Error, {
|
|
269
|
+
match: true,
|
|
270
|
+
"data-fs-phone-field-error": "",
|
|
271
|
+
"data-fs-field-error": "",
|
|
272
|
+
children: effectiveError
|
|
273
|
+
}) : null
|
|
274
|
+
] })
|
|
275
|
+
})
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
function UnboundPhoneField({ value, defaultValue = "", onValueChange, country = "US", label, isDisabled = false, placeholder, invalid, error, errorInvalid = "Invalid phone number", errorRequired = "Phone number is required", requirement, onBlur, className, ref, children }) {
|
|
182
279
|
const isControlled = value !== void 0;
|
|
183
280
|
const [internalRaw, setInternalRaw] = useState(defaultValue);
|
|
184
281
|
const [localInvalid, setLocalInvalid] = useState(false);
|
|
185
282
|
const [localError, setLocalError] = useState("");
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
188
|
-
|
|
283
|
+
const [prevCountry, setPrevCountry] = useState(country);
|
|
284
|
+
if (prevCountry !== country) {
|
|
285
|
+
setPrevCountry(country);
|
|
189
286
|
setLocalInvalid(false);
|
|
190
287
|
setLocalError("");
|
|
191
288
|
}
|
|
192
289
|
const rawValue = isControlled ? value : internalRaw;
|
|
193
290
|
const maskedValue = applyPhoneMask(rawValue, country);
|
|
291
|
+
const resolvedLabel = label ?? "Phone";
|
|
194
292
|
const effectiveInvalid = invalid !== void 0 ? invalid : localInvalid;
|
|
195
293
|
const effectiveError = error !== void 0 ? error : effectiveInvalid ? localError : "";
|
|
196
294
|
function handleChange(event) {
|
|
@@ -215,36 +313,82 @@ function PhoneField({ value, defaultValue = "", onValueChange, country = "US", l
|
|
|
215
313
|
setLocalError(fieldError);
|
|
216
314
|
onBlur?.();
|
|
217
315
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
"data-fs-phone-field-root-country": country,
|
|
316
|
+
return /* @__PURE__ */ jsx(PhoneFieldView, {
|
|
317
|
+
ref,
|
|
318
|
+
country,
|
|
222
319
|
className,
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
320
|
+
label: resolvedLabel,
|
|
321
|
+
isDisabled,
|
|
322
|
+
placeholder,
|
|
323
|
+
required: requirement === "mandatory",
|
|
324
|
+
maskedValue,
|
|
325
|
+
effectiveInvalid,
|
|
326
|
+
effectiveError,
|
|
327
|
+
onChange: handleChange,
|
|
328
|
+
onBlur: handleBlur,
|
|
329
|
+
children
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
function BoundPhoneField({ value: _value, defaultValue: _defaultValue, onValueChange, country = "US", label, isDisabled, placeholder, invalid, error, errorInvalid = "Invalid phone number", errorRequired = "Phone number is required", requirement, onBlur, className, ref: callerRef, contract, children }) {
|
|
333
|
+
const rule = useMemo(() => mergeFieldValidator(contract.rule, "phoneFormat", (rawValue) => {
|
|
334
|
+
const raw = typeof rawValue === "string" ? rawValue : String(rawValue ?? "");
|
|
335
|
+
return !raw.trim() || validatePhoneNumber(raw, country) || errorInvalid;
|
|
336
|
+
}), [
|
|
337
|
+
contract.rule,
|
|
338
|
+
errorInvalid,
|
|
339
|
+
country
|
|
340
|
+
]);
|
|
341
|
+
const field = useBoundField(contract, TEXT_FIELD_VALUE, rule, {
|
|
342
|
+
required: requirement === "mandatory",
|
|
343
|
+
message: errorRequired
|
|
344
|
+
});
|
|
345
|
+
const { getFieldState } = useFormContext();
|
|
346
|
+
const { invalid: rhfInvalid, error: rhfError } = getFieldState(contract.name, useFormState({ name: contract.name }));
|
|
347
|
+
const composedRef = useComposedRef(callerRef, field.ref);
|
|
348
|
+
const maskedValue = applyPhoneMask(typeof field.value === "string" ? field.value : String(field.value ?? ""), country);
|
|
349
|
+
const resolvedLabel = label ?? contract.label ?? "Phone";
|
|
350
|
+
const resolvedPlaceholder = placeholder ?? contract.placeholder;
|
|
351
|
+
const resolvedIsDisabled = isDisabled ?? contract.disabled ?? false;
|
|
352
|
+
const effectiveInvalid = invalid !== void 0 ? invalid : rhfInvalid;
|
|
353
|
+
const effectiveError = error !== void 0 ? error : effectiveInvalid ? rhfError?.message ?? "" : "";
|
|
354
|
+
function handleChange(event) {
|
|
355
|
+
const raw = removePhoneMask(event.target.value, country);
|
|
356
|
+
field.onChange(raw);
|
|
357
|
+
onValueChange?.(raw);
|
|
358
|
+
}
|
|
359
|
+
function handleBlur(_event) {
|
|
360
|
+
field.onBlur();
|
|
361
|
+
onBlur?.();
|
|
362
|
+
}
|
|
363
|
+
return /* @__PURE__ */ jsx(FieldBinding, {
|
|
364
|
+
name: contract.name,
|
|
365
|
+
label: resolvedLabel,
|
|
366
|
+
placeholder: resolvedPlaceholder,
|
|
367
|
+
defaultValue: contract.defaultValue,
|
|
368
|
+
rule: contract.rule,
|
|
369
|
+
options: contract.options,
|
|
370
|
+
disabled: resolvedIsDisabled,
|
|
371
|
+
readOnly: contract.readOnly,
|
|
372
|
+
autoComplete: contract.autoComplete,
|
|
373
|
+
inputMode: contract.inputMode,
|
|
374
|
+
children: /* @__PURE__ */ jsx(PhoneFieldView, {
|
|
375
|
+
ref: composedRef,
|
|
376
|
+
country,
|
|
377
|
+
className,
|
|
378
|
+
label: resolvedLabel,
|
|
379
|
+
isDisabled: resolvedIsDisabled,
|
|
380
|
+
placeholder: resolvedPlaceholder,
|
|
381
|
+
required: field.required,
|
|
382
|
+
maskedValue,
|
|
383
|
+
effectiveInvalid,
|
|
384
|
+
effectiveError,
|
|
385
|
+
onChange: handleChange,
|
|
386
|
+
onBlur: handleBlur,
|
|
387
|
+
children
|
|
388
|
+
})
|
|
245
389
|
});
|
|
246
390
|
}
|
|
247
391
|
//#endregion
|
|
248
|
-
export { PhoneField, PhoneFieldDefinition, applyPhoneMask, removePhoneMask, toE164, validatePhoneNumber };
|
|
392
|
+
export { PhoneField, PhoneFieldDefinition, PhoneFieldInput, PhoneFieldInputDefinition, applyPhoneMask, removePhoneMask, toE164, validatePhoneNumber };
|
|
249
393
|
|
|
250
394
|
//# sourceMappingURL=phone-field.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"phone-field.js","names":[],"sources":["../../../src/particles/phone-field/phone-field.tsx"],"sourcesContent":["'use client';\nimport { defineComponent } from '@vtex/fs-core';\nimport type { ChangeEvent, FocusEvent } from 'react';\nimport { useRef, useState } from 'react';\nimport { Field } from '../field/field.js';\nimport { Input } from '../input/input.js';\n\n// ---------------------------------------------------------------------------\n// Country masks\n// ---------------------------------------------------------------------------\n\n/**\n * Country codes supported by PhoneField.\n * Only US and BR are implemented; every other ISO-2 code falls back to the US\n * format (Gap-06 in the PhoneField spec).\n */\nexport type PhoneCountry = 'US' | 'BR';\n\n/**\n * Returns true when `value` starts with the E.164 or bare country-code prefix\n * for the given country.\n *\n * US country code: 1 → \"+1…\" or \"1…\"\n * BR country code: 55 → \"+55…\" or \"55…\"\n */\nfunction isValidCountryCode(value: string, country: PhoneCountry): boolean {\n switch (country) {\n case 'US':\n return value.startsWith('+1') || value.startsWith('1');\n case 'BR':\n return value.startsWith('+55') || value.startsWith('55');\n default:\n return false;\n }\n}\n\n/**\n * Strips phone mask formatting from `value`.\n *\n * When the raw string carries a valid country-code prefix (e.g. \"+55…\"), the\n * returned value preserves the leading \"+\" so callers can detect E.164 intent.\n * Otherwise only digits are returned.\n *\n * `removePhoneMask(applyPhoneMask(raw, country), country) === raw` for all\n * valid raw values that do NOT carry a country-code prefix.\n */\nexport function removePhoneMask(value: string, country: PhoneCountry): string {\n const onlyDigits = value.replace(/\\D/g, '');\n\n // Only treat as E.164 when the raw string explicitly carries a \"+\" prefix.\n // A bare digit string that starts with the country-code digits (e.g. \"1234567890\"\n // starting with \"1\") is a local number, not a country-code-prefixed one.\n if (value.startsWith('+') && isValidCountryCode(value, country)) {\n return `+${onlyDigits}`;\n }\n\n return onlyDigits;\n}\n\n/**\n * Formats `value` into the mask pattern for `country`, applying as many mask\n * characters as the digit count allows (progressive masking while typing).\n *\n * US: (123) 456-7890 or +1 (123) 456-7890\n * BR: (11) 99999-9999 or +55 (11) 99999-9999\n *\n * Non-US/BR countries fall back to the US mask.\n */\nexport function applyPhoneMask(value: string, country: PhoneCountry): string {\n switch (country) {\n case 'US':\n return applyUSPhoneMask(value);\n case 'BR':\n return applyBRPhoneMask(value);\n default:\n return applyUSPhoneMask(value);\n }\n}\n\nfunction applyUSPhoneMask(value: string): string {\n const onlyDigits = value.replace(/\\D/g, '');\n\n // E.164 with explicit \"+\" prefix and full country code\n if (value.startsWith('+1') && onlyDigits.length >= 11) {\n const local = value.replace('+1', '').replace(/\\D/g, '');\n return `+1 ${formatUSNumber(local)}`;\n }\n\n // Bare country-code prefix (e.g. \"11234567890\")\n if (onlyDigits.startsWith('1') && onlyDigits.length === 11) {\n return `+1 ${formatUSNumber(onlyDigits.substring(1))}`;\n }\n\n return formatUSNumber(onlyDigits);\n}\n\nfunction formatUSNumber(digits: string): string {\n // US local numbers cap at 10 digits — drop the overflow instead of growing the mask unbounded.\n const local = digits.slice(0, 10);\n if (local.length === 0) return '';\n if (local.length <= 3) return `(${local}`;\n if (local.length <= 6) return `(${local.slice(0, 3)}) ${local.slice(3)}`;\n return `(${local.slice(0, 3)}) ${local.slice(3, 6)}-${local.slice(6)}`;\n}\n\nfunction applyBRPhoneMask(value: string): string {\n const onlyDigits = value.replace(/\\D/g, '');\n\n // E.164 with explicit \"+55\" prefix. 12 digits is a landline (+55 + 10 local),\n // 13 is a mobile (+55 + 11 local); both strip the \"55\" country code.\n if (value.startsWith('+55') && onlyDigits.length >= 12) {\n const local = value.replace('+55', '').replace(/\\D/g, '');\n return `+55 ${formatBRNumber(local)}`;\n }\n\n // Bare country-code prefix (e.g. \"5511999998888\" mobile, \"551133334444\" landline)\n if (onlyDigits.startsWith('55') && (onlyDigits.length === 12 || onlyDigits.length === 13)) {\n return `+55 ${formatBRNumber(onlyDigits.substring(2))}`;\n }\n\n return formatBRNumber(onlyDigits);\n}\n\nfunction formatBRNumber(digits: string): string {\n // BR local numbers cap at 11 digits (mobile) — drop the overflow instead of growing the mask unbounded.\n const local = digits.slice(0, 11);\n if (local.length === 0) return '';\n if (local.length <= 2) return `(${local}`;\n if (local.length <= 6) return `(${local.slice(0, 2)}) ${local.slice(2)}`;\n // 7-10 digits: landline format (XX) XXXX-XXXX\n if (local.length <= 10) return `(${local.slice(0, 2)}) ${local.slice(2, 6)}-${local.slice(6)}`;\n // 11 digits: mobile format (XX) XXXXX-XXXX\n return `(${local.slice(0, 2)}) ${local.slice(2, 7)}-${local.slice(7)}`;\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/**\n * BR local part: 10 digits is a landline; 11 digits is a mobile, which must carry the mandatory\n * ninth digit \"9\" right after the two-digit DDD (the `phone` library rule the spec's Gap-05\n * reimplements — \"11899998888\" is mobile-shaped but not a real BR number).\n */\nfunction isValidBRLocal(digits: string): boolean {\n if (digits.length === 10) return true;\n return digits.length === 11 && digits[2] === '9';\n}\n\n/**\n * Validates a raw (unmasked) phone number string for the given country.\n *\n * NOTE: The spec (KD-3) calls for using the `phone` npm library for validation.\n * The library is listed in Gap-05 as a new dependency to add when available.\n * Until that dependency is installed, this implements an equivalent\n * digit-count validation that covers all spec key-scenarios for US and BR.\n * Country-code and non-country-code inputs are both handled.\n *\n * US valid forms:\n * - 10 digits (local, e.g. \"1234567890\")\n * - 11 raw digits with country code \"1\" (e.g. \"11234567890\")\n * - E.164 \"+1\" + 10 digits (e.g. \"+11234567890\")\n *\n * BR valid forms:\n * - 10 digits (landline, e.g. \"1133334444\")\n * - 11 digits (mobile, e.g. \"11999998888\"; the ninth digit after the DDD must be \"9\")\n * - E.164 \"+55\" + 10 or 11 digits (e.g. \"+5511999998888\")\n */\nexport function validatePhoneNumber(input: string, country: PhoneCountry): boolean {\n if (!input || !input.trim()) return false;\n\n const digits = input.replace(/\\D/g, '');\n\n if (isValidCountryCode(input, country)) {\n // Country-code branch: validate digit count including the country code digits.\n if (country === 'US') {\n // \"+1\" or \"1\" prefix + exactly 10 local digits = 11 raw digits\n if (digits.length === 11) return true;\n // Fall through: could be a 10-digit local number that starts with \"1\"\n } else {\n // BR: \"+55\" or \"55\" prefix + 10 or 11 local digits = 12 or 13 raw digits\n if (digits.length === 12 || digits.length === 13) return isValidBRLocal(digits.slice(2));\n // Fall through: could be local digits that happen to start with \"55\"\n }\n }\n\n // Local (no country code) validation\n if (country === 'US') {\n return digits.length === 10;\n }\n\n return isValidBRLocal(digits);\n}\n\n/**\n * Normalizes a valid raw phone value to E.164 for persistence (legacy checkout parity): the\n * particle emits local digits (\"2015551234\") or a \"+\"-prefixed string; the profile stores\n * \"+12015551234\". Only call this on values `validatePhoneNumber` has already accepted — shared by\n * every binding that persists a phone value (`ClientProfilePhoneControl`, `ClientProfileForm`) so\n * there is exactly one normalization implementation.\n */\nexport function toE164(raw: string, country: PhoneCountry): string {\n const unmasked = removePhoneMask(raw, country);\n if (unmasked.startsWith('+')) return unmasked;\n const code = country === 'BR' ? '55' : '1';\n const localMax = country === 'BR' ? 11 : 10;\n // A bare country-code prefix (\"12015551234\", \"5511999998888\") only needs the \"+\".\n return unmasked.startsWith(code) && unmasked.length > localMax ? `+${unmasked}` : `+${code}${unmasked}`;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype PhoneFieldRequirement = 'mandatory' | 'optional' | 'hidden';\n\nexport type PhoneFieldProps = {\n /** Controlled raw (unmasked) phone value. */\n value?: string;\n /** Uncontrolled initial raw value. */\n defaultValue?: string;\n /** Called with the raw (unmasked) value on every change. */\n onValueChange?: (value: string) => void;\n /** Selects the phone mask and validation rule. */\n country?: PhoneCountry;\n /** Visible field label. */\n label?: string;\n /** Disables the field when true. */\n isDisabled?: boolean;\n /** Input placeholder text. */\n placeholder?: string;\n /**\n * Externally-controlled invalid state. When provided, overrides the\n * particle's internal blur-triggered validation result.\n */\n invalid?: boolean;\n /**\n * Externally-supplied error message. When provided, overrides the\n * particle's internal error string.\n */\n error?: string;\n /** Error string shown when the input format is invalid. */\n errorInvalid?: string;\n /** Error string shown when the mandatory field is left empty. */\n errorRequired?: string;\n /**\n * Controls field visibility and required validation.\n *\n * - `'mandatory'` (default): empty input blocks submission.\n * - `'optional'`: empty input is allowed; format is still checked when a\n * value is present.\n * - `'hidden'`: renders null — no DOM node is emitted.\n */\n requirement?: PhoneFieldRequirement;\n /** Called after the field loses focus (post-validation). */\n onBlur?: () => void;\n /** Additional class applied to the root element. */\n className?: string;\n};\n\n// ---------------------------------------------------------------------------\n// Definition\n// ---------------------------------------------------------------------------\n\nexport const PhoneFieldDefinition = defineComponent<PhoneFieldProps>({\n name: 'PhoneField',\n importPath: '@vtex/faststore/components',\n component: PhoneField,\n editor: { label: 'Phone Field', category: 'action' },\n schema: {\n country: { type: 'string', default: 'US', label: 'Country (US or BR)' },\n label: { type: 'string', default: 'Phone', label: 'Label' },\n requirement: { type: 'string', default: 'mandatory', label: 'Requirement' },\n errorInvalid: { type: 'string', default: 'Invalid phone number', label: 'Invalid error message' },\n errorRequired: { type: 'string', default: 'Phone number is required', label: 'Required error message' },\n },\n guideline:\n 'Locale-aware, business-agnostic phone number input composed from the field + input particles. Applies country masking, validates on blur, and exposes its value via onValueChange. Bind it to a data source (e.g. ClientProfilePhoneControl for checkout) to persist the value.',\n});\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\n/**\n * Presentational, business-agnostic phone field. Composes the `field` and `input` particles: the\n * `Field.Root` owns the invalid/disabled state and the `Field.Error match` slot, while the `Input`\n * particle renders the masked `<tel>` control. The particle stays reusable (checkout binding today,\n * MyAccount / extensions later); a binding owns the data source and persistence.\n *\n * US/BR masking, blur validation, and the `data-fs-phone-field*` attributes (plus the country variant\n * attribute) are kept; error strings remain props (a documented i18n divergence — the country tables\n * are internal, the copy is owner-supplied).\n */\nexport function PhoneField({\n value,\n defaultValue = '',\n onValueChange,\n country = 'US',\n label = 'Phone',\n isDisabled = false,\n placeholder,\n invalid,\n error,\n errorInvalid = 'Invalid phone number',\n errorRequired = 'Phone number is required',\n requirement = 'mandatory',\n onBlur,\n className,\n}: PhoneFieldProps) {\n const isControlled = value !== undefined;\n\n const [internalRaw, setInternalRaw] = useState(defaultValue);\n const [localInvalid, setLocalInvalid] = useState(false);\n const [localError, setLocalError] = useState('');\n\n // Clear local validation state when country changes so the next blur\n // re-validates against the new country's rules. Done during render\n // (before paint) to avoid the useEffect trigger-dependency lint warning.\n const prevCountryRef = useRef(country);\n if (prevCountryRef.current !== country) {\n // react-doctor-disable-next-line no-ref-current-in-render -- paired with the setState calls right below, both replay together\n prevCountryRef.current = country;\n setLocalInvalid(false);\n setLocalError('');\n }\n\n const rawValue = isControlled ? (value as string) : internalRaw;\n const maskedValue = applyPhoneMask(rawValue, country);\n\n // External props take precedence over internal validation state.\n // When `invalid` is externally controlled, the error display is also fully\n // controlled: use the explicit `error` prop, or fall back to empty string so\n // no stale local error leaks through when the parent says the field is valid.\n const effectiveInvalid = invalid !== undefined ? invalid : localInvalid;\n const effectiveError = error !== undefined ? error : effectiveInvalid ? localError : '';\n\n function handleChange(event: ChangeEvent<HTMLInputElement>) {\n if (isDisabled) return;\n const raw = removePhoneMask(event.target.value, country);\n if (!isControlled) {\n setInternalRaw(raw);\n }\n onValueChange?.(raw);\n }\n\n function handleBlur(_event: FocusEvent<HTMLInputElement>) {\n if (isDisabled) return;\n\n let isFieldInvalid = false;\n let fieldError = '';\n\n if (requirement === 'mandatory' && !rawValue.trim()) {\n isFieldInvalid = true;\n fieldError = errorRequired;\n } else if (rawValue.trim()) {\n const valid = validatePhoneNumber(rawValue, country);\n isFieldInvalid = !valid;\n fieldError = valid ? '' : errorInvalid;\n }\n // optional + empty → no error, keep isFieldInvalid = false / fieldError = ''\n\n setLocalInvalid(isFieldInvalid);\n setLocalError(fieldError);\n onBlur?.();\n }\n\n if (requirement === 'hidden') return null;\n\n return (\n <Field.Root\n data-fs-phone-field-root=\"\"\n data-fs-phone-field-root-country={country}\n className={className}\n invalid={effectiveInvalid}\n disabled={isDisabled}\n >\n <Field.Label data-fs-phone-field-label=\"\">{label}</Field.Label>\n {/* The `input` particle wraps Base UI `Input`, which wires into `Field.Root` out of the box\n (no `Field.Control` wrapper needed); see the Base UI Field docs. */}\n <Input\n data-fs-phone-field-input=\"\"\n type=\"tel\"\n inputMode=\"numeric\"\n value={maskedValue}\n onChange={handleChange}\n onBlur={handleBlur}\n placeholder={placeholder}\n />\n {effectiveError ? (\n <Field.Error match data-fs-phone-field-error=\"\">\n {effectiveError}\n </Field.Error>\n ) : null}\n </Field.Root>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"phone-field.js","names":["BaseField"],"sources":["../../../src/particles/phone-field/phone-field.tsx"],"sourcesContent":["'use client';\nimport { Field as BaseField } from '@base-ui/react/field';\nimport { defineComponent } from '@vtex/fs-core';\nimport type { ChangeEvent, FocusEvent, ReactNode, Ref } from 'react';\nimport { createContext, useContext, useMemo, useState } from 'react';\nimport { useFormContext, useFormState } from 'react-hook-form';\nimport {\n FieldBinding,\n type FieldContract,\n type FieldRule,\n mergeFieldValidator,\n TEXT_FIELD_VALUE,\n useBoundField,\n useField,\n} from '../field/field.js';\nimport { Input } from '../input/input.js';\nimport { useComposedRef } from '../shared/compose-refs.js';\n\n// ---------------------------------------------------------------------------\n// Country masks\n// ---------------------------------------------------------------------------\n\n/**\n * Country codes supported by PhoneField.\n * Only US and BR are implemented; every other ISO-2 code falls back to the US\n * format (Gap-06 in the PhoneField spec).\n */\nexport type PhoneCountry = 'US' | 'BR';\n\n/**\n * Returns true when `value` starts with the E.164 or bare country-code prefix\n * for the given country.\n *\n * US country code: 1 → \"+1…\" or \"1…\"\n * BR country code: 55 → \"+55…\" or \"55…\"\n */\nfunction isValidCountryCode(value: string, country: PhoneCountry): boolean {\n switch (country) {\n case 'US':\n return value.startsWith('+1') || value.startsWith('1');\n case 'BR':\n return value.startsWith('+55') || value.startsWith('55');\n default:\n return false;\n }\n}\n\n/**\n * Strips phone mask formatting from `value`.\n *\n * When the raw string carries a valid country-code prefix (e.g. \"+55…\"), the\n * returned value preserves the leading \"+\" so callers can detect E.164 intent.\n * Otherwise only digits are returned.\n *\n * `removePhoneMask(applyPhoneMask(raw, country), country) === raw` for all\n * valid raw values that do NOT carry a country-code prefix.\n */\nexport function removePhoneMask(value: string, country: PhoneCountry): string {\n const onlyDigits = value.replace(/\\D/g, '');\n\n // Only treat as E.164 when the raw string explicitly carries a \"+\" prefix.\n // A bare digit string that starts with the country-code digits (e.g. \"1234567890\"\n // starting with \"1\") is a local number, not a country-code-prefixed one.\n if (value.startsWith('+') && isValidCountryCode(value, country)) {\n return `+${onlyDigits}`;\n }\n\n return onlyDigits;\n}\n\n/**\n * Formats `value` into the mask pattern for `country`, applying as many mask\n * characters as the digit count allows (progressive masking while typing).\n *\n * US: (123) 456-7890 or +1 (123) 456-7890\n * BR: (11) 99999-9999 or +55 (11) 99999-9999\n *\n * Non-US/BR countries fall back to the US mask.\n */\nexport function applyPhoneMask(value: string, country: PhoneCountry): string {\n switch (country) {\n case 'US':\n return applyUSPhoneMask(value);\n case 'BR':\n return applyBRPhoneMask(value);\n default:\n return applyUSPhoneMask(value);\n }\n}\n\nfunction applyUSPhoneMask(value: string): string {\n const onlyDigits = value.replace(/\\D/g, '');\n\n // E.164 with explicit \"+\" prefix and full country code\n if (value.startsWith('+1') && onlyDigits.length >= 11) {\n const local = value.replace('+1', '').replace(/\\D/g, '');\n return `+1 ${formatUSNumber(local)}`;\n }\n\n // Bare country-code prefix (e.g. \"11234567890\")\n if (onlyDigits.startsWith('1') && onlyDigits.length === 11) {\n return `+1 ${formatUSNumber(onlyDigits.substring(1))}`;\n }\n\n return formatUSNumber(onlyDigits);\n}\n\nfunction formatUSNumber(digits: string): string {\n // US local numbers cap at 10 digits — drop the overflow instead of growing the mask unbounded.\n const local = digits.slice(0, 10);\n if (local.length === 0) return '';\n if (local.length <= 3) return `(${local}`;\n if (local.length <= 6) return `(${local.slice(0, 3)}) ${local.slice(3)}`;\n return `(${local.slice(0, 3)}) ${local.slice(3, 6)}-${local.slice(6)}`;\n}\n\nfunction applyBRPhoneMask(value: string): string {\n const onlyDigits = value.replace(/\\D/g, '');\n\n // E.164 with explicit \"+55\" prefix. 12 digits is a landline (+55 + 10 local),\n // 13 is a mobile (+55 + 11 local); both strip the \"55\" country code.\n if (value.startsWith('+55') && onlyDigits.length >= 12) {\n const local = value.replace('+55', '').replace(/\\D/g, '');\n return `+55 ${formatBRNumber(local)}`;\n }\n\n // Bare country-code prefix (e.g. \"5511999998888\" mobile, \"551133334444\" landline)\n if (onlyDigits.startsWith('55') && (onlyDigits.length === 12 || onlyDigits.length === 13)) {\n return `+55 ${formatBRNumber(onlyDigits.substring(2))}`;\n }\n\n return formatBRNumber(onlyDigits);\n}\n\nfunction formatBRNumber(digits: string): string {\n // BR local numbers cap at 11 digits (mobile) — drop the overflow instead of growing the mask unbounded.\n const local = digits.slice(0, 11);\n if (local.length === 0) return '';\n if (local.length <= 2) return `(${local}`;\n if (local.length <= 6) return `(${local.slice(0, 2)}) ${local.slice(2)}`;\n // 7-10 digits: landline format (XX) XXXX-XXXX\n if (local.length <= 10) return `(${local.slice(0, 2)}) ${local.slice(2, 6)}-${local.slice(6)}`;\n // 11 digits: mobile format (XX) XXXXX-XXXX\n return `(${local.slice(0, 2)}) ${local.slice(2, 7)}-${local.slice(7)}`;\n}\n\n// ---------------------------------------------------------------------------\n// Validation\n// ---------------------------------------------------------------------------\n\n/**\n * BR local part: 10 digits is a landline; 11 digits is a mobile, which must carry the mandatory\n * ninth digit \"9\" right after the two-digit DDD (the `phone` library rule the spec's Gap-05\n * reimplements — \"11899998888\" is mobile-shaped but not a real BR number).\n */\nfunction isValidBRLocal(digits: string): boolean {\n if (digits.length === 10) return true;\n return digits.length === 11 && digits[2] === '9';\n}\n\n/**\n * Validates a raw (unmasked) phone number string for the given country.\n *\n * NOTE: The spec (KD-3) calls for using the `phone` npm library for validation.\n * The library is listed in Gap-05 as a new dependency to add when available.\n * Until that dependency is installed, this implements an equivalent\n * digit-count validation that covers all spec key-scenarios for US and BR.\n * Country-code and non-country-code inputs are both handled.\n *\n * US valid forms:\n * - 10 digits (local, e.g. \"1234567890\")\n * - 11 raw digits with country code \"1\" (e.g. \"11234567890\")\n * - E.164 \"+1\" + 10 digits (e.g. \"+11234567890\")\n *\n * BR valid forms:\n * - 10 digits (landline, e.g. \"1133334444\")\n * - 11 digits (mobile, e.g. \"11999998888\"; the ninth digit after the DDD must be \"9\")\n * - E.164 \"+55\" + 10 or 11 digits (e.g. \"+5511999998888\")\n */\nexport function validatePhoneNumber(input: string, country: PhoneCountry): boolean {\n if (!input || !input.trim()) return false;\n\n const digits = input.replace(/\\D/g, '');\n\n if (isValidCountryCode(input, country)) {\n // Country-code branch: validate digit count including the country code digits.\n if (country === 'US') {\n // \"+1\" or \"1\" prefix + exactly 10 local digits = 11 raw digits\n if (digits.length === 11) return true;\n // Fall through: could be a 10-digit local number that starts with \"1\"\n } else {\n // BR: \"+55\" or \"55\" prefix + 10 or 11 local digits = 12 or 13 raw digits\n if (digits.length === 12 || digits.length === 13) return isValidBRLocal(digits.slice(2));\n // Fall through: could be local digits that happen to start with \"55\"\n }\n }\n\n // Local (no country code) validation\n if (country === 'US') {\n return digits.length === 10;\n }\n\n return isValidBRLocal(digits);\n}\n\n/**\n * Normalizes a valid raw phone value to E.164 for persistence (legacy checkout parity): the\n * particle emits local digits (\"2015551234\") or a \"+\"-prefixed string; the profile stores\n * \"+12015551234\". Only call this on values `validatePhoneNumber` has already accepted — shared by\n * every binding that persists a phone value (`ClientProfilePhoneControl`, `ClientProfileFieldset`)\n * so there is exactly one normalization implementation.\n */\nexport function toE164(raw: string, country: PhoneCountry): string {\n const unmasked = removePhoneMask(raw, country);\n if (unmasked.startsWith('+')) return unmasked;\n const code = country === 'BR' ? '55' : '1';\n const localMax = country === 'BR' ? 11 : 10;\n // A bare country-code prefix (\"12015551234\", \"5511999998888\") only needs the \"+\".\n return unmasked.startsWith(code) && unmasked.length > localMax ? `+${unmasked}` : `+${code}${unmasked}`;\n}\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\ntype PhoneFieldRequirement = 'mandatory' | 'optional';\n\nexport type PhoneFieldProps = {\n /** Controlled raw (unmasked) phone value. Unused on the bound path — react-hook-form owns the value. */\n value?: string;\n /** Uncontrolled initial raw value. Unused on the bound path — the field contract's `defaultValue` seeds it instead. */\n defaultValue?: string;\n /** Called with the raw (unmasked) value on every change. */\n onValueChange?: (value: string) => void;\n /** Selects the phone mask and validation rule. */\n country?: PhoneCountry;\n /**\n * Visible field label. No signature default (Decision 5 of the wrapped-field-particles spec) — an\n * omitted label falls back to the field contract's `label` when bound, and to `'Phone'` when not.\n */\n label?: string;\n /** Disables the field when true. Falls back to the field contract's `disabled` when bound. */\n isDisabled?: boolean;\n /** Input placeholder text. Falls back to the field contract's `placeholder` when bound. */\n placeholder?: string;\n /**\n * Externally-controlled invalid state. When provided, overrides the\n * particle's internal blur-triggered validation result.\n */\n invalid?: boolean;\n /**\n * Externally-supplied error message. When provided, overrides the\n * particle's internal error string.\n */\n error?: string;\n /** Error string shown when the input format is invalid. */\n errorInvalid?: string;\n /** Error string shown when the mandatory field is left empty. */\n errorRequired?: string;\n /**\n * Raises the field to required. It only ever raises: bound to a fieldset whose rule already says\n * required, `'optional'` does not lower it.\n *\n * - `'mandatory'`: empty input blocks submission.\n * - `'optional'`: empty input is allowed; format is still checked when a\n * value is present.\n * - unset: no opinion, so a phone with no baseline is optional.\n */\n requirement?: PhoneFieldRequirement;\n /** Called after the field loses focus (post-validation). */\n onBlur?: () => void;\n /** Additional class applied to the root element. */\n className?: string;\n /**\n * Lands on the masked `<input type=\"tel\">` the particle renders. Composed with react-hook-form's own\n * ref on the bound path — a caller ref and react-hook-form's ref are both honoured.\n */\n ref?: Ref<HTMLInputElement>;\n /**\n * Given, replaces the default `Field.Label` + `PhoneFieldInput` + `Field.Error` anatomy — any order,\n * any subset. `PhoneFieldInput` is the one part that carries the mask, the composed ref and the\n * particle's own value/handlers; a composed generic `Field.Control` would call `useController` a\n * second time for the same name (RFC's named, accepted double-binder hole).\n */\n children?: ReactNode;\n};\n\n// ---------------------------------------------------------------------------\n// Definition\n// ---------------------------------------------------------------------------\n\nexport const PhoneFieldDefinition = defineComponent<PhoneFieldProps>({\n name: 'PhoneField',\n importPath: '@vtex/faststore/components',\n component: PhoneField,\n providesContext: 'phone-field',\n editor: { label: 'Phone Field', category: 'action' },\n schema: {\n country: { type: 'string', default: 'US', label: 'Country (US or BR)' },\n label: { type: 'string', default: 'Phone', label: 'Label' },\n // No default: an unset requirement is no opinion, not a raise to mandatory.\n requirement: { type: ['mandatory', 'optional'] as const, label: 'Requirement' },\n errorInvalid: { type: 'string', default: 'Invalid phone number', label: 'Invalid error message' },\n errorRequired: { type: 'string', default: 'Phone number is required', label: 'Required error message' },\n },\n guideline:\n 'Locale-aware, business-agnostic phone number input composed from the field + input particles. Applies country masking, validates on blur, and exposes its value via onValueChange. Bind it to a data source (e.g. ClientProfilePhoneControl for checkout) to persist the value. Recomposable (RFC:503): given children, they replace the default Field.Label + PhoneFieldInput + Field.Error anatomy, in any order.',\n});\n\n// ---------------------------------------------------------------------------\n// Component\n// ---------------------------------------------------------------------------\n\n/**\n * Presentational, business-agnostic phone field. Composes the `field` and `input` particles: the\n * `Field.Root` owns the invalid/disabled state and the `Field.Error match` slot, while the `Input`\n * particle renders the masked `<tel>` control. The particle stays reusable (checkout binding today,\n * MyAccount / extensions later); a binding owns the data source and persistence.\n *\n * US/BR masking, blur validation, and the `data-fs-phone-field*` attributes (plus the country variant\n * attribute) are kept; error strings remain props (a documented i18n divergence — the country tables\n * are internal, the copy is owner-supplied).\n *\n * Reads the field contract through `useField()` and `useFormContext()` and dispatches to a bound or an\n * unbound sub-component — \"bound\" means both are present. Every react-hook-form hook lives in\n * `BoundPhoneField`, so hook order never changes under a mounted component. Both sub-components render\n * the same `PhoneFieldView`, so the two paths cannot drift into different DOM.\n */\nexport function PhoneField({ requirement, ...props }: PhoneFieldProps): ReactNode {\n const contract = useField();\n const formContext = useFormContext();\n\n if (contract && formContext) {\n return <BoundPhoneField {...props} requirement={requirement} contract={contract} />;\n }\n\n return <UnboundPhoneField {...props} requirement={requirement} />;\n}\n\n// ---------------------------------------------------------------------------\n// Input part — catalogued, placeable (Decision 4). Carries the mask, the composed ref, and the\n// particle's own onChange/onBlur; a generic Field.Control cannot be this part, because the mask and\n// the country/requirement validator belong to PhoneField, not to react-hook-form's own registration.\n// ---------------------------------------------------------------------------\n\ntype PhoneFieldInputContextValue = {\n ref?: Ref<HTMLInputElement>;\n value: string;\n placeholder?: string;\n /** Resolved by the binder, threaded here because this part is not the one that binds. */\n required: boolean;\n onChange: (event: ChangeEvent<HTMLInputElement>) => void;\n onBlur: (event: FocusEvent<HTMLInputElement>) => void;\n};\n\nconst PhoneFieldInputContext = createContext<PhoneFieldInputContextValue | null>(null);\n\nfunction usePhoneFieldInputContext(): PhoneFieldInputContextValue {\n const context = useContext(PhoneFieldInputContext);\n if (!context) {\n throw new Error('PhoneFieldInput must be a descendant of PhoneField.');\n }\n return context;\n}\n\nexport type PhoneFieldInputProps = { className?: string };\n\nexport const PhoneFieldInputDefinition = defineComponent<PhoneFieldInputProps>({\n name: 'PhoneFieldInput',\n importPath: '@vtex/faststore/components',\n component: PhoneFieldInput,\n requiresContext: 'phone-field',\n editor: { label: 'Phone Field Input', category: 'action' },\n schema: {},\n guideline:\n \"The masked <input type=\\\"tel\\\"> PhoneField owns. Must sit inside PhoneField, in either the default anatomy or an author's composed children. Carries the country mask, the composed ref, and PhoneField's own onChange/onBlur — a composed Field.Control here instead re-registers the field with react-hook-form and loses the mask (unsupported, RFC's named double-binder hole).\",\n});\n\n/** Renders the masked `<input>`, reading its value/handlers/ref from the enclosing `PhoneField`. */\nexport function PhoneFieldInput({ className }: PhoneFieldInputProps): ReactNode {\n const { ref, value, placeholder, required, onChange, onBlur } = usePhoneFieldInputContext();\n\n return (\n <Input\n ref={ref}\n data-fs-phone-field-input=\"\"\n type=\"tel\"\n inputMode=\"numeric\"\n // `aria-required`, not native `required`, for the reason password-field.tsx:89-91 records:\n // `Input` is a Base UI `Field.Control`, and native constraint tracking would flip\n // `aria-invalid` on its own, ahead of react-hook-form's own validation.\n aria-required={required || undefined}\n value={value}\n onChange={onChange}\n onBlur={onBlur}\n placeholder={placeholder}\n className={className}\n />\n );\n}\n\n// ---------------------------------------------------------------------------\n// Shared view — identical DOM whether bound or unbound\n// ---------------------------------------------------------------------------\n\ntype PhoneFieldViewProps = {\n ref?: Ref<HTMLInputElement>;\n country: PhoneCountry;\n className?: string;\n label: string;\n isDisabled: boolean;\n placeholder?: string;\n required: boolean;\n maskedValue: string;\n effectiveInvalid: boolean;\n effectiveError: string;\n onChange: (event: ChangeEvent<HTMLInputElement>) => void;\n onBlur: (event: FocusEvent<HTMLInputElement>) => void;\n children?: ReactNode;\n};\n\nfunction PhoneFieldView({\n ref,\n country,\n className,\n label,\n isDisabled,\n placeholder,\n required,\n maskedValue,\n effectiveInvalid,\n effectiveError,\n onChange,\n onBlur,\n children,\n}: PhoneFieldViewProps) {\n const inputContext = useMemo<PhoneFieldInputContextValue>(\n () => ({ ref, value: maskedValue, placeholder, required, onChange, onBlur }),\n [ref, maskedValue, placeholder, required, onChange, onBlur],\n );\n\n return (\n // Deliberately the RAW Base UI Field primitives, not this package's own Field.Root/Label/Error\n // wrapper, for the default anatomy: this reproduces exactly what rendered before Decision 3\n // (when FieldContractBoundary shielded this subtree from the ambient contract) — the resolved\n // `label`/`effectiveError` text renders unconditionally, and Field.Root's own dirty/touched\n // fallback tracking stays in charge, rather than react-hook-form's. Composed children, below,\n // use the package's own Field.* parts on purpose — that is what makes a childless Field.Label /\n // Field.Error resolve real text (FR-5).\n <BaseField.Root\n data-fs-phone-field-root=\"\"\n data-fs-phone-field-root-country={country}\n data-fs-field-root=\"\"\n className={className}\n invalid={effectiveInvalid}\n disabled={isDisabled}\n >\n <PhoneFieldInputContext.Provider value={inputContext}>\n {children ?? (\n <>\n <BaseField.Label data-fs-phone-field-label=\"\" data-fs-field-label=\"\">\n {label}\n </BaseField.Label>\n <PhoneFieldInput />\n {effectiveError ? (\n <BaseField.Error match data-fs-phone-field-error=\"\" data-fs-field-error=\"\">\n {effectiveError}\n </BaseField.Error>\n ) : null}\n </>\n )}\n </PhoneFieldInputContext.Provider>\n </BaseField.Root>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Unbound — today's body, moved verbatim\n// ---------------------------------------------------------------------------\n\ntype UnboundPhoneFieldProps = PhoneFieldProps;\n\nfunction UnboundPhoneField({\n value,\n defaultValue = '',\n onValueChange,\n country = 'US',\n label,\n isDisabled = false,\n placeholder,\n invalid,\n error,\n errorInvalid = 'Invalid phone number',\n errorRequired = 'Phone number is required',\n requirement,\n onBlur,\n className,\n ref,\n children,\n}: UnboundPhoneFieldProps) {\n const isControlled = value !== undefined;\n\n const [internalRaw, setInternalRaw] = useState(defaultValue);\n const [localInvalid, setLocalInvalid] = useState(false);\n const [localError, setLocalError] = useState('');\n\n // Clear local validation state when country changes so the next blur re-validates against the new\n // country's rules. The comparison baseline lives in state, not a ref, and the compare-and-set runs\n // during render (before paint), so a render React abandons without committing — a suspended\n // transition, a Suspense retry — never leaves a stale baseline behind the way a ref write would.\n const [prevCountry, setPrevCountry] = useState(country);\n if (prevCountry !== country) {\n setPrevCountry(country);\n setLocalInvalid(false);\n setLocalError('');\n }\n\n const rawValue = isControlled ? (value as string) : internalRaw;\n const maskedValue = applyPhoneMask(rawValue, country);\n const resolvedLabel = label ?? 'Phone';\n\n // External props take precedence over internal validation state.\n // When `invalid` is externally controlled, the error display is also fully\n // controlled: use the explicit `error` prop, or fall back to empty string so\n // no stale local error leaks through when the parent says the field is valid.\n const effectiveInvalid = invalid !== undefined ? invalid : localInvalid;\n const effectiveError = error !== undefined ? error : effectiveInvalid ? localError : '';\n\n function handleChange(event: ChangeEvent<HTMLInputElement>) {\n if (isDisabled) return;\n const raw = removePhoneMask(event.target.value, country);\n if (!isControlled) {\n setInternalRaw(raw);\n }\n onValueChange?.(raw);\n }\n\n function handleBlur(_event: FocusEvent<HTMLInputElement>) {\n if (isDisabled) return;\n\n let isFieldInvalid = false;\n let fieldError = '';\n\n if (requirement === 'mandatory' && !rawValue.trim()) {\n isFieldInvalid = true;\n fieldError = errorRequired;\n } else if (rawValue.trim()) {\n const valid = validatePhoneNumber(rawValue, country);\n isFieldInvalid = !valid;\n fieldError = valid ? '' : errorInvalid;\n }\n // optional + empty → no error, keep isFieldInvalid = false / fieldError = ''\n\n setLocalInvalid(isFieldInvalid);\n setLocalError(fieldError);\n onBlur?.();\n }\n\n return (\n <PhoneFieldView\n ref={ref}\n country={country}\n className={className}\n label={resolvedLabel}\n isDisabled={isDisabled}\n placeholder={placeholder}\n required={requirement === 'mandatory'}\n maskedValue={maskedValue}\n effectiveInvalid={effectiveInvalid}\n effectiveError={effectiveError}\n onChange={handleChange}\n onBlur={handleBlur}\n >\n {children}\n </PhoneFieldView>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Bound — reads the field contract, binds through useController\n// ---------------------------------------------------------------------------\n\ntype BoundPhoneFieldProps = UnboundPhoneFieldProps & { contract: FieldContract };\n\nfunction BoundPhoneField({\n value: _value,\n defaultValue: _defaultValue,\n onValueChange,\n country = 'US',\n label,\n isDisabled,\n placeholder,\n invalid,\n error,\n errorInvalid = 'Invalid phone number',\n errorRequired = 'Phone number is required',\n requirement,\n onBlur,\n className,\n ref: callerRef,\n contract,\n children,\n}: BoundPhoneFieldProps) {\n // Format only. Requiredness is not decided here: `useBoundField` resolves it from the fieldset's\n // baseline and the raise below, and installs the blank check, so the announcement and the\n // validation cannot disagree. The check composes alongside whatever the fieldset validates —\n // before this merge, any contract validator discarded the particle's own validation whole.\n const rule = useMemo<FieldRule>(\n () =>\n mergeFieldValidator(contract.rule, 'phoneFormat', (rawValue) => {\n const raw = typeof rawValue === 'string' ? rawValue : String(rawValue ?? '');\n return !raw.trim() || validatePhoneNumber(raw, country) || errorInvalid;\n }),\n [contract.rule, errorInvalid, country],\n );\n\n const field = useBoundField(contract, TEXT_FIELD_VALUE, rule, {\n required: requirement === 'mandatory',\n message: errorRequired,\n });\n const { getFieldState } = useFormContext();\n const { invalid: rhfInvalid, error: rhfError } = getFieldState(contract.name, useFormState({ name: contract.name }));\n\n const composedRef = useComposedRef<HTMLInputElement>(callerRef, field.ref);\n\n const rawValue = typeof field.value === 'string' ? field.value : String(field.value ?? '');\n const maskedValue = applyPhoneMask(rawValue, country);\n const resolvedLabel = label ?? contract.label ?? 'Phone';\n const resolvedPlaceholder = placeholder ?? contract.placeholder;\n const resolvedIsDisabled = isDisabled ?? contract.disabled ?? false;\n\n const effectiveInvalid = invalid !== undefined ? invalid : rhfInvalid;\n const effectiveError = error !== undefined ? error : effectiveInvalid ? (rhfError?.message ?? '') : '';\n\n function handleChange(event: ChangeEvent<HTMLInputElement>) {\n const raw = removePhoneMask(event.target.value, country);\n field.onChange(raw);\n onValueChange?.(raw);\n }\n\n function handleBlur(_event: FocusEvent<HTMLInputElement>) {\n field.onBlur();\n onBlur?.();\n }\n\n return (\n // Decision 3: replaces the empty-contract FieldContractBoundary with a re-provided contract\n // carrying the RESOLVED name/label/placeholder — the values a composed childless Field.Label /\n // Field.Error actually need, as opposed to the raw (possibly-undefined) ones the ambient\n // contract from the enclosing FieldBinding carries. useBoundField above remains the sole binder\n // for contract.name; this FieldBinding never registers anything itself.\n <FieldBinding\n name={contract.name}\n label={resolvedLabel}\n placeholder={resolvedPlaceholder}\n defaultValue={contract.defaultValue}\n rule={contract.rule}\n options={contract.options}\n disabled={resolvedIsDisabled}\n readOnly={contract.readOnly}\n autoComplete={contract.autoComplete}\n inputMode={contract.inputMode}\n >\n <PhoneFieldView\n ref={composedRef}\n country={country}\n className={className}\n label={resolvedLabel}\n isDisabled={resolvedIsDisabled}\n placeholder={resolvedPlaceholder}\n required={field.required}\n maskedValue={maskedValue}\n effectiveInvalid={effectiveInvalid}\n effectiveError={effectiveError}\n onChange={handleChange}\n onBlur={handleBlur}\n >\n {children}\n </PhoneFieldView>\n </FieldBinding>\n );\n}\n"],"mappings":"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"}
|
|
@@ -3,8 +3,8 @@ import { defineComponent } from "../../core/dist/define-component.js";
|
|
|
3
3
|
import { defineContentType } from "../../core/dist/define-content-type.js";
|
|
4
4
|
import "../../core/dist/index.js";
|
|
5
5
|
import { Field } from "../field/field.js";
|
|
6
|
-
import { Input } from "../input/input.js";
|
|
7
6
|
import { applyMask, countSlots, isDigitOnly, isSlotToken } from "../shared/input-mask.js";
|
|
7
|
+
import { Input } from "../input/input.js";
|
|
8
8
|
import { useState } from "react";
|
|
9
9
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
10
10
|
//#region src/particles/postal-code-field/postal-code-field.tsx
|
|
@@ -73,7 +73,7 @@ const PostalCodeFieldDefinition = defineComponent({
|
|
|
73
73
|
schema: {
|
|
74
74
|
mask: {
|
|
75
75
|
type: "string",
|
|
76
|
-
default:
|
|
76
|
+
default: "99999-999",
|
|
77
77
|
label: "Mask pattern"
|
|
78
78
|
},
|
|
79
79
|
label: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postal-code-field.js","names":[],"sources":["../../../src/particles/postal-code-field/postal-code-field.tsx"],"sourcesContent":["'use client';\nimport { type ContentValue, defineComponent, defineContentType } from '@vtex/fs-core';\nimport type { ChangeEvent, ComponentPropsWithRef, FocusEvent } from 'react';\nimport { useState } from 'react';\nimport { Field } from '../field/field.js';\nimport { Input } from '../input/input.js';\nimport { applyMask, countSlots, isDigitOnly, isSlotToken } from '../shared/input-mask.js';\n\n/** Brazilian CEP. */\nconst DEFAULT_MASK = '99999-999';\n\nexport const PostalCodeFieldContent = defineContentType({\n id: 'postal-code-field',\n title: 'Postal Code Field',\n description: 'Editorial copy for the postal-code input.',\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n label: { type: 'string', title: 'Label' },\n placeholder: { type: 'string', title: 'Placeholder' },\n errorMessage: { type: 'string', title: 'Error message' },\n },\n },\n});\n\nexport type PostalCodeFieldProps = Omit<\n ComponentPropsWithRef<'div'>,\n 'defaultValue' | 'value' | 'onChange' | 'onBlur'\n> & {\n /** Mask template: `9` digit, `A` alpha, `S` alphanumeric, `*` any, interleaved with literals.\n * Pipe-delimited alternates (`99999|99999-999`) cover formats of different lengths. */\n mask?: string;\n label?: string;\n placeholder?: string;\n errorMessage?: string;\n /** Controlled value; masked on render, so a raw value is accepted. */\n value?: string;\n defaultValue?: string;\n /** Called with the masked value on every change. */\n onValueChange?: (value: string) => void;\n onBlur?: (event: FocusEvent<HTMLInputElement>) => void;\n /** Host-forced invalid state (e.g. a server rejection); ORs with the internal blur verdict. */\n invalid?: boolean;\n required?: boolean;\n disabled?: boolean;\n name?: string;\n /** Merchant-authored copy, sliced from the page/layout content entry; wins over the props when\n * present. Absent in a non-CMS store — the props are the fallback. The mask is not authored here:\n * it is a build-time schema prop. */\n $content?: ContentValue<typeof PostalCodeFieldContent>;\n};\n\n/** Slot count of the longest mask in a (possibly pipe-delimited) template. */\nfunction maxSlots(mask: string): number {\n return Math.max(...mask.split('|').map(countSlots));\n}\n\n/** Remove the mask's literal separators, leaving only slot characters. */\nexport function stripPostalCodeMask(value: string, mask: string): string {\n const literals = new Set([...mask].filter((char) => char !== '|' && !isSlotToken(char)));\n return [...value].filter((char) => !literals.has(char)).join('');\n}\n\n/**\n * Sanitize (digit-only masks), truncate to the mask's slot count, then apply the mask. Idempotent:\n * stripping the literals first means an already-masked value and a raw one produce the same display.\n */\nexport function applyPostalCodeMask(rawValue: string, mask: string): string {\n const slotChars = stripPostalCodeMask(rawValue, mask);\n const sanitized = isDigitOnly(mask) ? slotChars.replaceAll(/\\D/g, '') : slotChars;\n return applyMask(sanitized.slice(0, maxSlots(mask)), mask);\n}\n\n/**\n * True when the value fills every slot of the mask — of any mask in a pipe-delimited set, so a\n * shorter alternate counts as complete. An empty value is NOT complete: emptiness is the host's\n * submit-gating concern, not this field's.\n */\nexport function isPostalCodeComplete(value: string, mask: string): boolean {\n const slotChars = stripPostalCodeMask(value, mask);\n if (!slotChars) return false;\n return mask.split('|').some((single) => slotChars.length === countSlots(single));\n}\n\nexport const PostalCodeFieldDefinition = defineComponent<PostalCodeFieldProps>({\n name: 'PostalCodeField',\n importPath: '@vtex/faststore/components',\n component: PostalCodeField,\n content: PostalCodeFieldContent,\n editor: { label: 'Postal Code Field', category: 'content' },\n schema: {\n mask: { type: 'string', default: DEFAULT_MASK, label: 'Mask pattern' },\n label: { type: 'string', label: 'Label' },\n placeholder: { type: 'string', label: 'Placeholder' },\n errorMessage: { type: 'string', label: 'Error message' },\n required: { type: 'boolean', default: false, label: 'Required' },\n disabled: { type: 'boolean', default: false, label: 'Disabled' },\n },\n guideline:\n 'Presentational postal-code input composing the Field and Input particles; formats the value against the configured mask template as the shopper types and flags an incomplete value on blur. Data-agnostic (value/defaultValue/onValueChange) — a host such as the region modal owns submission and surfaces server errors through invalid/errorMessage. Set mask to the country format at build time (default 99999-999, BR CEP); the CMS edits only label, placeholder, and error copy.',\n});\n\n/**\n * Presentational postal-code input (particle). Composes the `Field` and `Input` particles and owns\n * one piece of logic: applying the configured mask template on every change, plus a blur verdict on\n * whether the value fills the mask. It is country-agnostic — the pattern is configuration, not a\n * country table — and data-agnostic: the value, the submit, and the server's verdict belong to the\n * host, which surfaces a rejection through `invalid` + `errorMessage`.\n */\nexport function PostalCodeField({\n mask = DEFAULT_MASK,\n label,\n placeholder,\n errorMessage,\n value,\n defaultValue = '',\n onValueChange,\n onBlur,\n invalid = false,\n required = false,\n disabled = false,\n name = 'postalCode',\n $content,\n className,\n // Naming attributes belong on the control, not on the root the rest of the props land on. They are\n // the only way to name the field when no label resolves.\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n ...props\n}: PostalCodeFieldProps) {\n const isControlled = value !== undefined;\n const [internalValue, setInternalValue] = useState(() => applyPostalCodeMask(defaultValue, mask));\n const [blurInvalid, setBlurInvalid] = useState(false);\n\n const currentValue = isControlled ? value : internalValue;\n const displayValue = applyPostalCodeMask(currentValue, mask);\n const showInvalid = invalid || blurInvalid;\n const numericOnly = isDigitOnly(mask);\n\n // Each content field falls back on its own, so a cleared one keeps the authored prop.\n const resolvedLabel = $content?.label?.trim() ? $content.label : label;\n const resolvedPlaceholder = $content?.placeholder?.trim() ? $content.placeholder : placeholder;\n const resolvedError = $content?.errorMessage?.trim() ? $content.errorMessage : errorMessage;\n\n function handleChange(event: ChangeEvent<HTMLInputElement>) {\n const masked = applyPostalCodeMask(event.target.value, mask);\n if (!isControlled) setInternalValue(masked);\n // Clear a prior blur verdict while typing; it is re-evaluated on the next blur.\n if (blurInvalid) setBlurInvalid(false);\n onValueChange?.(masked);\n }\n\n function handleBlur(event: FocusEvent<HTMLInputElement>) {\n // Judge the displayed value: a host can hand over a raw one carrying characters the mask drops,\n // and a verdict on those would contradict what the shopper sees.\n setBlurInvalid(displayValue.trim() !== '' && !isPostalCodeComplete(displayValue, mask));\n onBlur?.(event);\n }\n\n return (\n <Field.Root\n {...props}\n className={className}\n name={name}\n invalid={showInvalid}\n disabled={disabled}\n data-fs-postal-code-field=\"\"\n >\n {resolvedLabel ? <Field.Label data-fs-postal-code-field-label=\"\">{resolvedLabel}</Field.Label> : null}\n {/* `Input` wires into `Field.Root` on its own — no `Field.Control` wrapper. */}\n <Input\n data-fs-postal-code-field-input=\"\"\n type=\"text\"\n name={name}\n required={required}\n disabled={disabled}\n placeholder={resolvedPlaceholder}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n autoComplete=\"postal-code\"\n value={displayValue}\n {...(numericOnly ? { inputMode: 'numeric' as const } : {})}\n onChange={handleChange}\n onBlur={handleBlur}\n />\n {showInvalid && resolvedError ? (\n <Field.Error match data-fs-postal-code-field-error=\"\">\n {resolvedError}\n </Field.Error>\n ) : null}\n </Field.Root>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"postal-code-field.js","names":[],"sources":["../../../src/particles/postal-code-field/postal-code-field.tsx"],"sourcesContent":["'use client';\nimport { type ContentValue, defineComponent, defineContentType } from '@vtex/fs-core';\nimport type { ChangeEvent, ComponentPropsWithRef, FocusEvent } from 'react';\nimport { useState } from 'react';\nimport { Field } from '../field/field.js';\nimport { Input } from '../input/input.js';\nimport { applyMask, countSlots, isDigitOnly, isSlotToken } from '../shared/input-mask.js';\n\n/** Brazilian CEP. */\nconst DEFAULT_MASK = '99999-999';\n\nexport const PostalCodeFieldContent = defineContentType({\n id: 'postal-code-field',\n title: 'Postal Code Field',\n description: 'Editorial copy for the postal-code input.',\n schema: {\n type: 'object',\n additionalProperties: false,\n properties: {\n label: { type: 'string', title: 'Label' },\n placeholder: { type: 'string', title: 'Placeholder' },\n errorMessage: { type: 'string', title: 'Error message' },\n },\n },\n});\n\nexport type PostalCodeFieldProps = Omit<\n ComponentPropsWithRef<'div'>,\n 'defaultValue' | 'value' | 'onChange' | 'onBlur'\n> & {\n /** Mask template: `9` digit, `A` alpha, `S` alphanumeric, `*` any, interleaved with literals.\n * Pipe-delimited alternates (`99999|99999-999`) cover formats of different lengths. */\n mask?: string;\n label?: string;\n placeholder?: string;\n errorMessage?: string;\n /** Controlled value; masked on render, so a raw value is accepted. */\n value?: string;\n defaultValue?: string;\n /** Called with the masked value on every change. */\n onValueChange?: (value: string) => void;\n onBlur?: (event: FocusEvent<HTMLInputElement>) => void;\n /** Host-forced invalid state (e.g. a server rejection); ORs with the internal blur verdict. */\n invalid?: boolean;\n required?: boolean;\n disabled?: boolean;\n name?: string;\n /** Merchant-authored copy, sliced from the page/layout content entry; wins over the props when\n * present. Absent in a non-CMS store — the props are the fallback. The mask is not authored here:\n * it is a build-time schema prop. */\n $content?: ContentValue<typeof PostalCodeFieldContent>;\n};\n\n/** Slot count of the longest mask in a (possibly pipe-delimited) template. */\nfunction maxSlots(mask: string): number {\n return Math.max(...mask.split('|').map(countSlots));\n}\n\n/** Remove the mask's literal separators, leaving only slot characters. */\nexport function stripPostalCodeMask(value: string, mask: string): string {\n const literals = new Set([...mask].filter((char) => char !== '|' && !isSlotToken(char)));\n return [...value].filter((char) => !literals.has(char)).join('');\n}\n\n/**\n * Sanitize (digit-only masks), truncate to the mask's slot count, then apply the mask. Idempotent:\n * stripping the literals first means an already-masked value and a raw one produce the same display.\n */\nexport function applyPostalCodeMask(rawValue: string, mask: string): string {\n const slotChars = stripPostalCodeMask(rawValue, mask);\n const sanitized = isDigitOnly(mask) ? slotChars.replaceAll(/\\D/g, '') : slotChars;\n return applyMask(sanitized.slice(0, maxSlots(mask)), mask);\n}\n\n/**\n * True when the value fills every slot of the mask — of any mask in a pipe-delimited set, so a\n * shorter alternate counts as complete. An empty value is NOT complete: emptiness is the host's\n * submit-gating concern, not this field's.\n */\nexport function isPostalCodeComplete(value: string, mask: string): boolean {\n const slotChars = stripPostalCodeMask(value, mask);\n if (!slotChars) return false;\n return mask.split('|').some((single) => slotChars.length === countSlots(single));\n}\n\nexport const PostalCodeFieldDefinition = defineComponent<PostalCodeFieldProps>({\n name: 'PostalCodeField',\n importPath: '@vtex/faststore/components',\n component: PostalCodeField,\n content: PostalCodeFieldContent,\n editor: { label: 'Postal Code Field', category: 'content' },\n schema: {\n mask: { type: 'string', default: '99999-999', label: 'Mask pattern' },\n label: { type: 'string', label: 'Label' },\n placeholder: { type: 'string', label: 'Placeholder' },\n errorMessage: { type: 'string', label: 'Error message' },\n required: { type: 'boolean', default: false, label: 'Required' },\n disabled: { type: 'boolean', default: false, label: 'Disabled' },\n },\n guideline:\n 'Presentational postal-code input composing the Field and Input particles; formats the value against the configured mask template as the shopper types and flags an incomplete value on blur. Data-agnostic (value/defaultValue/onValueChange) — a host such as the region modal owns submission and surfaces server errors through invalid/errorMessage. Set mask to the country format at build time (default 99999-999, BR CEP); the CMS edits only label, placeholder, and error copy.',\n});\n\n/**\n * Presentational postal-code input (particle). Composes the `Field` and `Input` particles and owns\n * one piece of logic: applying the configured mask template on every change, plus a blur verdict on\n * whether the value fills the mask. It is country-agnostic — the pattern is configuration, not a\n * country table — and data-agnostic: the value, the submit, and the server's verdict belong to the\n * host, which surfaces a rejection through `invalid` + `errorMessage`.\n */\nexport function PostalCodeField({\n mask = DEFAULT_MASK,\n label,\n placeholder,\n errorMessage,\n value,\n defaultValue = '',\n onValueChange,\n onBlur,\n invalid = false,\n required = false,\n disabled = false,\n name = 'postalCode',\n $content,\n className,\n // Naming attributes belong on the control, not on the root the rest of the props land on. They are\n // the only way to name the field when no label resolves.\n 'aria-label': ariaLabel,\n 'aria-labelledby': ariaLabelledBy,\n ...props\n}: PostalCodeFieldProps) {\n const isControlled = value !== undefined;\n const [internalValue, setInternalValue] = useState(() => applyPostalCodeMask(defaultValue, mask));\n const [blurInvalid, setBlurInvalid] = useState(false);\n\n const currentValue = isControlled ? value : internalValue;\n const displayValue = applyPostalCodeMask(currentValue, mask);\n const showInvalid = invalid || blurInvalid;\n const numericOnly = isDigitOnly(mask);\n\n // Each content field falls back on its own, so a cleared one keeps the authored prop.\n const resolvedLabel = $content?.label?.trim() ? $content.label : label;\n const resolvedPlaceholder = $content?.placeholder?.trim() ? $content.placeholder : placeholder;\n const resolvedError = $content?.errorMessage?.trim() ? $content.errorMessage : errorMessage;\n\n function handleChange(event: ChangeEvent<HTMLInputElement>) {\n const masked = applyPostalCodeMask(event.target.value, mask);\n if (!isControlled) setInternalValue(masked);\n // Clear a prior blur verdict while typing; it is re-evaluated on the next blur.\n if (blurInvalid) setBlurInvalid(false);\n onValueChange?.(masked);\n }\n\n function handleBlur(event: FocusEvent<HTMLInputElement>) {\n // Judge the displayed value: a host can hand over a raw one carrying characters the mask drops,\n // and a verdict on those would contradict what the shopper sees.\n setBlurInvalid(displayValue.trim() !== '' && !isPostalCodeComplete(displayValue, mask));\n onBlur?.(event);\n }\n\n return (\n <Field.Root\n {...props}\n className={className}\n name={name}\n invalid={showInvalid}\n disabled={disabled}\n data-fs-postal-code-field=\"\"\n >\n {resolvedLabel ? <Field.Label data-fs-postal-code-field-label=\"\">{resolvedLabel}</Field.Label> : null}\n {/* `Input` wires into `Field.Root` on its own — no `Field.Control` wrapper. */}\n <Input\n data-fs-postal-code-field-input=\"\"\n type=\"text\"\n name={name}\n required={required}\n disabled={disabled}\n placeholder={resolvedPlaceholder}\n aria-label={ariaLabel}\n aria-labelledby={ariaLabelledBy}\n autoComplete=\"postal-code\"\n value={displayValue}\n {...(numericOnly ? { inputMode: 'numeric' as const } : {})}\n onChange={handleChange}\n onBlur={handleBlur}\n />\n {showInvalid && resolvedError ? (\n <Field.Error match data-fs-postal-code-field-error=\"\">\n {resolvedError}\n </Field.Error>\n ) : null}\n </Field.Root>\n );\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { ComponentDefinition } from "../../core/dist/define-component.js";
|
|
2
|
+
import "../../core/dist/index.js";
|
|
3
|
+
import { ComponentPropsWithRef, ReactNode } from "react";
|
|
4
|
+
//#region src/particles/select/field-select.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Binds a chosen-option field. Reads the enclosing field contract, registers with react-hook-form,
|
|
7
|
+
* and drives the shipped `Select.Root`. Childless, it renders the default nine-node anatomy; given
|
|
8
|
+
* children, it renders them and they bind the same way.
|
|
9
|
+
*/
|
|
10
|
+
export type FieldSelectRootProps = {
|
|
11
|
+
/** Base UI's modal behaviour. The one authorable knob that carries no field meaning. */
|
|
12
|
+
modal?: boolean;
|
|
13
|
+
children?: ReactNode;
|
|
14
|
+
};
|
|
15
|
+
/** The field's requiredness and the bound select's styling hook, on the focusable control. */
|
|
16
|
+
export type FieldSelectTriggerProps = Omit<ComponentPropsWithRef<'button'>, 'dangerouslySetInnerHTML'>;
|
|
17
|
+
/** The chosen option's label, resolved from the contract's options. */
|
|
18
|
+
export type FieldSelectValueProps = Omit<ComponentPropsWithRef<'span'>, 'children' | 'dangerouslySetInnerHTML'> & {
|
|
19
|
+
placeholder?: string;
|
|
20
|
+
};
|
|
21
|
+
/** One `Select.Item` per contract option. Renders nothing when the contract carries none. */
|
|
22
|
+
export type FieldSelectOptionsProps = {
|
|
23
|
+
/** Applied to every item this part renders; it renders no wrapper of its own. */
|
|
24
|
+
className?: string;
|
|
25
|
+
/** Rendered inside every item, after its text — e.g. a `Select.ItemIndicator`. */
|
|
26
|
+
children?: ReactNode;
|
|
27
|
+
};
|
|
28
|
+
export declare const FieldSelectRootDefinition: ComponentDefinition<FieldSelectRootProps>;
|
|
29
|
+
export declare const FieldSelectTriggerDefinition: ComponentDefinition<FieldSelectTriggerProps>;
|
|
30
|
+
export declare const FieldSelectValueDefinition: ComponentDefinition<FieldSelectValueProps>;
|
|
31
|
+
export declare const FieldSelectOptionsDefinition: ComponentDefinition<FieldSelectOptionsProps>;
|
|
32
|
+
export declare function FieldSelectRoot({ modal, children }: FieldSelectRootProps): ReactNode;
|
|
33
|
+
export declare function FieldSelectTrigger({ children, ...props }: FieldSelectTriggerProps): ReactNode;
|
|
34
|
+
export declare function FieldSelectValue({ placeholder, ...props }: FieldSelectValueProps): ReactNode;
|
|
35
|
+
export declare function FieldSelectOptions({ className, children }: FieldSelectOptionsProps): ReactNode;
|
|
36
|
+
export declare const FieldSelect: {
|
|
37
|
+
Root: typeof FieldSelectRoot;
|
|
38
|
+
Trigger: typeof FieldSelectTrigger;
|
|
39
|
+
Value: typeof FieldSelectValue;
|
|
40
|
+
Options: typeof FieldSelectOptions;
|
|
41
|
+
};
|
|
42
|
+
//#endregion
|
|
43
|
+
//# sourceMappingURL=field-select.d.ts.map
|