@asteby/storefront-react 0.16.1 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@ type AddressFieldKey = 'zip' | 'province' | 'city' | 'neighborhood' | 'address1'
8
8
  type ContactFieldKey = 'email' | 'phone' | 'first_name' | 'last_name';
9
9
  type AddressFieldsMessages = Record<AddressFieldKey | ContactFieldKey, string> & {
10
10
  provincePlaceholder: string;
11
+ neighborhoodPlaceholder: string;
11
12
  address1Hint: string;
12
13
  referenceHint: string;
13
14
  emailHint: string;
@@ -74,6 +75,12 @@ type AddressFieldsProps = Common & {
74
75
  * Never overwrites what the buyer typed.
75
76
  */
76
77
  lookupPostalCode?: (zip: string) => Promise<PostalCodeInfo | null>;
78
+ /**
79
+ * How to offer the colonias when the postal code has SEVERAL: `'datalist'` (default, suggestions on a text field) or
80
+ * `'select'` (a real list; the field becomes a `<select>` of those colonias, unless the buyer already typed one that
81
+ * is not in the list, which is kept as text). With exactly one colonia it is filled in either mode.
82
+ */
83
+ neighborhoodList?: 'datalist' | 'select';
77
84
  /** Show the colonia field: `true` (default, optional), `'required'`, or `false`. */
78
85
  neighborhood?: boolean | 'required';
79
86
  /** Show the delivery references field (default true, optional). */
@@ -88,6 +95,13 @@ type AddressFieldsProps = Common & {
88
95
  /** Rendered after the fields (e.g. "save this address"). */
89
96
  children?: ReactNode;
90
97
  };
98
+ /** Name of the property set to `true` on every event the lookup dispatches when it fills a field. */
99
+ declare const ADDRESS_FILL_EVENT_FLAG = "sfSynthetic";
100
+ /**
101
+ * Was this `input` event dispatched by `AddressFields` itself (a postal-code fill) and not typed by the buyer? The shop's
102
+ * own listeners (validation, autosave, delivery re-quote...) can ignore these. Real typing has `isTrusted: true`.
103
+ */
104
+ declare function isAddressFillEvent(event: Event): boolean;
91
105
  declare function AddressFields(props: AddressFieldsProps): react.JSX.Element;
92
106
 
93
107
  type OrderSummaryMessages = {
@@ -452,4 +466,4 @@ type FlatRatesOptions = {
452
466
  */
453
467
  declare function flatRates<R extends ShippingRate>(input: readonly R[], options?: FlatRatesOptions): RecommendedRate<R>[];
454
468
 
455
- export { type AddressFieldKey, AddressFields, type AddressFieldsMessages, type AddressFieldsProps, type Brand, type BrandKind, BrandMark, type BrandMarkProps, type BrandMarkSource, CheckoutFieldClassNames, type ContactFieldKey, ContactFields, type ContactFieldsProps, type DeliveryMessages, DeliveryOptions, type DeliveryOptionsClassNames, type DeliveryOptionsProps, FieldErrors, type FieldLayoutClassNames, FieldMessages, type FlatRatesOptions, OrderSummary, type OrderSummaryClassNames, OrderSummaryDisclosure, type OrderSummaryDisclosureProps, type OrderSummaryMessages, type OrderSummaryProps, type PostalCodeInfo, type QuoteNote, type RateBadge, type RateGroup, type RateTag, type RecommendRatesOptions, type RecommendedRate, type RecommendedRates, addressFieldsMessages, brandKey, brandLogo, brandName, dedupeRates, deliveryMessages, findBrand, flatRates, isQuoteChange, listBrands, matchRate, orderSummaryMessages, quoteDeadline, quoteNote, rateBadges, rateCarrier, rateEta, rateService, recommendRates };
469
+ export { ADDRESS_FILL_EVENT_FLAG, type AddressFieldKey, AddressFields, type AddressFieldsMessages, type AddressFieldsProps, type Brand, type BrandKind, BrandMark, type BrandMarkProps, type BrandMarkSource, CheckoutFieldClassNames, type ContactFieldKey, ContactFields, type ContactFieldsProps, type DeliveryMessages, DeliveryOptions, type DeliveryOptionsClassNames, type DeliveryOptionsProps, FieldErrors, type FieldLayoutClassNames, FieldMessages, type FlatRatesOptions, OrderSummary, type OrderSummaryClassNames, OrderSummaryDisclosure, type OrderSummaryDisclosureProps, type OrderSummaryMessages, type OrderSummaryProps, type PostalCodeInfo, type QuoteNote, type RateBadge, type RateGroup, type RateTag, type RecommendRatesOptions, type RecommendedRate, type RecommendedRates, addressFieldsMessages, brandKey, brandLogo, brandName, dedupeRates, deliveryMessages, findBrand, flatRates, isAddressFillEvent, isQuoteChange, listBrands, matchRate, orderSummaryMessages, quoteDeadline, quoteNote, rateBadges, rateCarrier, rateEta, rateService, recommendRates };
@@ -8,6 +8,7 @@ type AddressFieldKey = 'zip' | 'province' | 'city' | 'neighborhood' | 'address1'
8
8
  type ContactFieldKey = 'email' | 'phone' | 'first_name' | 'last_name';
9
9
  type AddressFieldsMessages = Record<AddressFieldKey | ContactFieldKey, string> & {
10
10
  provincePlaceholder: string;
11
+ neighborhoodPlaceholder: string;
11
12
  address1Hint: string;
12
13
  referenceHint: string;
13
14
  emailHint: string;
@@ -74,6 +75,12 @@ type AddressFieldsProps = Common & {
74
75
  * Never overwrites what the buyer typed.
75
76
  */
76
77
  lookupPostalCode?: (zip: string) => Promise<PostalCodeInfo | null>;
78
+ /**
79
+ * How to offer the colonias when the postal code has SEVERAL: `'datalist'` (default, suggestions on a text field) or
80
+ * `'select'` (a real list; the field becomes a `<select>` of those colonias, unless the buyer already typed one that
81
+ * is not in the list, which is kept as text). With exactly one colonia it is filled in either mode.
82
+ */
83
+ neighborhoodList?: 'datalist' | 'select';
77
84
  /** Show the colonia field: `true` (default, optional), `'required'`, or `false`. */
78
85
  neighborhood?: boolean | 'required';
79
86
  /** Show the delivery references field (default true, optional). */
@@ -88,6 +95,13 @@ type AddressFieldsProps = Common & {
88
95
  /** Rendered after the fields (e.g. "save this address"). */
89
96
  children?: ReactNode;
90
97
  };
98
+ /** Name of the property set to `true` on every event the lookup dispatches when it fills a field. */
99
+ declare const ADDRESS_FILL_EVENT_FLAG = "sfSynthetic";
100
+ /**
101
+ * Was this `input` event dispatched by `AddressFields` itself (a postal-code fill) and not typed by the buyer? The shop's
102
+ * own listeners (validation, autosave, delivery re-quote...) can ignore these. Real typing has `isTrusted: true`.
103
+ */
104
+ declare function isAddressFillEvent(event: Event): boolean;
91
105
  declare function AddressFields(props: AddressFieldsProps): react.JSX.Element;
92
106
 
93
107
  type OrderSummaryMessages = {
@@ -452,4 +466,4 @@ type FlatRatesOptions = {
452
466
  */
453
467
  declare function flatRates<R extends ShippingRate>(input: readonly R[], options?: FlatRatesOptions): RecommendedRate<R>[];
454
468
 
455
- export { type AddressFieldKey, AddressFields, type AddressFieldsMessages, type AddressFieldsProps, type Brand, type BrandKind, BrandMark, type BrandMarkProps, type BrandMarkSource, CheckoutFieldClassNames, type ContactFieldKey, ContactFields, type ContactFieldsProps, type DeliveryMessages, DeliveryOptions, type DeliveryOptionsClassNames, type DeliveryOptionsProps, FieldErrors, type FieldLayoutClassNames, FieldMessages, type FlatRatesOptions, OrderSummary, type OrderSummaryClassNames, OrderSummaryDisclosure, type OrderSummaryDisclosureProps, type OrderSummaryMessages, type OrderSummaryProps, type PostalCodeInfo, type QuoteNote, type RateBadge, type RateGroup, type RateTag, type RecommendRatesOptions, type RecommendedRate, type RecommendedRates, addressFieldsMessages, brandKey, brandLogo, brandName, dedupeRates, deliveryMessages, findBrand, flatRates, isQuoteChange, listBrands, matchRate, orderSummaryMessages, quoteDeadline, quoteNote, rateBadges, rateCarrier, rateEta, rateService, recommendRates };
469
+ export { ADDRESS_FILL_EVENT_FLAG, type AddressFieldKey, AddressFields, type AddressFieldsMessages, type AddressFieldsProps, type Brand, type BrandKind, BrandMark, type BrandMarkProps, type BrandMarkSource, CheckoutFieldClassNames, type ContactFieldKey, ContactFields, type ContactFieldsProps, type DeliveryMessages, DeliveryOptions, type DeliveryOptionsClassNames, type DeliveryOptionsProps, FieldErrors, type FieldLayoutClassNames, FieldMessages, type FlatRatesOptions, OrderSummary, type OrderSummaryClassNames, OrderSummaryDisclosure, type OrderSummaryDisclosureProps, type OrderSummaryMessages, type OrderSummaryProps, type PostalCodeInfo, type QuoteNote, type RateBadge, type RateGroup, type RateTag, type RecommendRatesOptions, type RecommendedRate, type RecommendedRates, addressFieldsMessages, brandKey, brandLogo, brandName, dedupeRates, deliveryMessages, findBrand, flatRates, isAddressFillEvent, isQuoteChange, listBrands, matchRate, orderSummaryMessages, quoteDeadline, quoteNote, rateBadges, rateCarrier, rateEta, rateService, recommendRates };
package/dist/checkout.js CHANGED
@@ -21,6 +21,7 @@ var ES = {
21
21
  address2: "N\xFAmero interior o departamento",
22
22
  reference: "Referencias para la entrega",
23
23
  provincePlaceholder: "Elige tu estado",
24
+ neighborhoodPlaceholder: "Elige tu colonia",
24
25
  address1Hint: "Por ejemplo: Av. Insurgentes Sur 1234",
25
26
  referenceHint: "Entre qu\xE9 calles, color de la fachada\u2026",
26
27
  emailHint: "Te enviamos aqu\xED la confirmaci\xF3n y el seguimiento.",
@@ -40,6 +41,7 @@ var EN = {
40
41
  address2: "Apartment, suite",
41
42
  reference: "Delivery notes",
42
43
  provincePlaceholder: "Choose your state",
44
+ neighborhoodPlaceholder: "Choose your neighborhood",
43
45
  address1Hint: "For example: 1234 Main St",
44
46
  referenceHint: "Cross streets, building color\u2026",
45
47
  emailHint: "We send the confirmation and tracking here.",
@@ -82,6 +84,10 @@ function lockField(el) {
82
84
  el.readOnly = true;
83
85
  }
84
86
  }
87
+ var ADDRESS_FILL_EVENT_FLAG = "sfSynthetic";
88
+ function isAddressFillEvent(event) {
89
+ return event[ADDRESS_FILL_EVENT_FLAG] === true;
90
+ }
85
91
  function AddressFields(props) {
86
92
  const { defaults, names = {}, errors = {}, countryCode, provinces, postalCode, lookupPostalCode, lockProvinceOnFill, locale, messages, fieldMessages: fm, classNames: cx = {}, children } = props;
87
93
  const showNeighborhood = props.neighborhood ?? true;
@@ -92,10 +98,36 @@ function AddressFields(props) {
92
98
  const listId = useId();
93
99
  const [colonias, setColonias] = useState([]);
94
100
  const [filled, setFilled] = useState(false);
101
+ const [coloniaSelect, setColoniaSelect] = useState(false);
102
+ const coloniaSelectRef = useRef(false);
103
+ const coloniaCarry = useRef(null);
95
104
  const lastZip = useRef(null);
96
105
  const zipRef = useRef(null);
97
106
  const lookupRef = useRef(lookupPostalCode);
98
107
  lookupRef.current = lookupPostalCode;
108
+ const writer = useRef(Promise.resolve());
109
+ const enqueue = (task) => {
110
+ writer.current = writer.current.then(task).catch(() => {
111
+ });
112
+ };
113
+ useEffect(() => {
114
+ const form = zipRef.current?.closest("form");
115
+ if (!form) return;
116
+ const mark = (e) => {
117
+ if (!e.isTrusted) return;
118
+ const el = e.target;
119
+ if (el instanceof HTMLInputElement || el instanceof HTMLSelectElement) {
120
+ el.dataset.sfUserEdited = "1";
121
+ delete el.dataset.sfAutofilled;
122
+ }
123
+ };
124
+ form.addEventListener("input", mark, true);
125
+ form.addEventListener("change", mark, true);
126
+ return () => {
127
+ form.removeEventListener("input", mark, true);
128
+ form.removeEventListener("change", mark, true);
129
+ };
130
+ }, []);
99
131
  const runLookup = (zip) => {
100
132
  const lookup = lookupRef.current;
101
133
  if (!lookup || !zip || zip === lastZip.current) return;
@@ -104,28 +136,65 @@ function AddressFields(props) {
104
136
  const form = zipRef.current?.closest("form");
105
137
  void lookup(zip).then(
106
138
  (info) => {
107
- if (!info || lastZip.current !== zip) return;
108
- const fill2 = (name, value, lock) => {
109
- const el = form?.elements.namedItem(name);
110
- if (!el || !value || "length" in el && !(el instanceof HTMLSelectElement)) return false;
111
- if (el.dataset.sfLocked === "1") return false;
112
- if (el.value && el.dataset.sfAutofilled !== "1") return false;
113
- if (el instanceof HTMLSelectElement && ![...el.options].some((o) => o.value === value)) return false;
114
- el.value = value;
115
- el.dataset.sfAutofilled = "1";
116
- el.dispatchEvent(new Event("input", { bubbles: true }));
117
- if (lock) lockField(el);
118
- return true;
119
- };
120
- const a = fill2(n("province"), info.province, lockProvinceOnFill);
121
- const b = fill2(n("city"), info.city);
122
- setColonias(info.neighborhoods ?? []);
123
- setFilled(a || b);
139
+ if (!info) return;
140
+ enqueue(() => {
141
+ if (lastZip.current !== zip) return;
142
+ const fill2 = (name, value, lock) => {
143
+ const el2 = form?.elements.namedItem(name);
144
+ if (!el2 || !value || "length" in el2 && !(el2 instanceof HTMLSelectElement)) return false;
145
+ if (el2.dataset.sfLocked === "1") return false;
146
+ if (el2.dataset.sfUserEdited === "1" || el2.ownerDocument.activeElement === el2 && el2.value) return false;
147
+ if (el2.value && el2.dataset.sfAutofilled !== "1") return false;
148
+ if (el2 instanceof HTMLSelectElement && ![...el2.options].some((o) => o.value === value)) return false;
149
+ el2.value = value;
150
+ el2.dataset.sfAutofilled = "1";
151
+ const event = new Event("input", { bubbles: true });
152
+ Object.defineProperty(event, ADDRESS_FILL_EVENT_FLAG, { value: true });
153
+ el2.dispatchEvent(event);
154
+ if (lock) lockField(el2);
155
+ return true;
156
+ };
157
+ const a = fill2(n("province"), info.province, lockProvinceOnFill);
158
+ const b = fill2(n("city"), info.city);
159
+ const only = info.neighborhoods?.length === 1 ? info.neighborhoods[0] : null;
160
+ const c = showNeighborhood ? fill2(n("neighborhood"), only) : false;
161
+ setColonias(info.neighborhoods ?? []);
162
+ const list = info.neighborhoods ?? [];
163
+ const nameEl = form?.elements.namedItem(n("neighborhood"));
164
+ const el = nameEl && !("length" in nameEl && !(nameEl instanceof HTMLSelectElement)) ? nameEl : null;
165
+ const typed = el ? el.value : "";
166
+ const wantSelect = props.neighborhoodList === "select" && showNeighborhood !== false && list.length > 1 && (!typed || list.includes(typed));
167
+ let carried = false;
168
+ if (showNeighborhood && wantSelect !== coloniaSelectRef.current) {
169
+ const userEdited = el?.dataset.sfUserEdited === "1";
170
+ const value = only && !userEdited ? only : wantSelect ? list.includes(typed) ? typed : "" : typed;
171
+ coloniaCarry.current = { value, autofilled: !userEdited && (!!only || el?.dataset.sfAutofilled === "1"), userEdited };
172
+ carried = !!only && !userEdited;
173
+ }
174
+ coloniaSelectRef.current = wantSelect;
175
+ setColoniaSelect(wantSelect);
176
+ setFilled(a || b || c || carried);
177
+ });
124
178
  },
125
179
  () => {
126
180
  }
127
181
  );
128
182
  };
183
+ useEffect(() => {
184
+ const carry = coloniaCarry.current;
185
+ coloniaCarry.current = null;
186
+ if (!carry || !carry.value) return;
187
+ const form = zipRef.current?.closest("form");
188
+ const el = form?.elements.namedItem(n("neighborhood"));
189
+ if (!el || "length" in el && !(el instanceof HTMLSelectElement)) return;
190
+ if (el instanceof HTMLSelectElement && ![...el.options].some((o) => o.value === carry.value)) return;
191
+ el.value = carry.value;
192
+ if (carry.userEdited) el.dataset.sfUserEdited = "1";
193
+ else if (carry.autofilled) el.dataset.sfAutofilled = "1";
194
+ const event = new Event("input", { bubbles: true });
195
+ Object.defineProperty(event, ADDRESS_FILL_EVENT_FLAG, { value: true });
196
+ el.dispatchEvent(event);
197
+ }, [coloniaSelect]);
129
198
  useEffect(() => {
130
199
  const zip = defaults?.zip;
131
200
  if (zip) runLookup(zip);
@@ -159,7 +228,24 @@ function AddressFields(props) {
159
228
  provinces.map((p) => /* @__PURE__ */ jsx("option", { value: p.name, children: p.name }, p.code ?? p.name))
160
229
  ] }) : /* @__PURE__ */ jsx(CheckoutField, { ...common, name: n("province"), label: m.province, autoComplete: "address-level1", defaultValue: defaults?.province ?? "", error: errors[n("province")] ?? null, classNames: field(cx, "half") }),
161
230
  /* @__PURE__ */ jsx(CheckoutField, { ...common, name: n("city"), label: m.city, autoComplete: "address-level2", defaultValue: defaults?.city ?? "", error: errors[n("city")] ?? null, classNames: field(cx, "half") }),
162
- showNeighborhood ? /* @__PURE__ */ jsx(
231
+ showNeighborhood && coloniaSelect ? /* @__PURE__ */ jsxs(
232
+ CheckoutField,
233
+ {
234
+ ...common,
235
+ as: "select",
236
+ name: n("neighborhood"),
237
+ label: m.neighborhood,
238
+ autoComplete: "address-level3",
239
+ optional: showNeighborhood !== "required",
240
+ defaultValue: colonias.includes(defaults?.neighborhood ?? "") ? defaults?.neighborhood ?? "" : "",
241
+ error: errors[n("neighborhood")] ?? null,
242
+ classNames: field(cx, "half"),
243
+ children: [
244
+ /* @__PURE__ */ jsx("option", { value: "", disabled: showNeighborhood === "required", children: m.neighborhoodPlaceholder }),
245
+ colonias.map((c) => /* @__PURE__ */ jsx("option", { value: c, children: c }, c))
246
+ ]
247
+ }
248
+ ) : showNeighborhood ? /* @__PURE__ */ jsx(
163
249
  CheckoutField,
164
250
  {
165
251
  ...common,
@@ -170,10 +256,10 @@ function AddressFields(props) {
170
256
  defaultValue: defaults?.neighborhood ?? "",
171
257
  error: errors[n("neighborhood")] ?? null,
172
258
  classNames: field(cx, "half"),
173
- ...colonias.length ? { list: listId } : {}
259
+ ...colonias.length && !coloniaSelect ? { list: listId } : {}
174
260
  }
175
261
  ) : null,
176
- colonias.length ? /* @__PURE__ */ jsx("datalist", { id: listId, children: colonias.map((c) => /* @__PURE__ */ jsx("option", { value: c }, c)) }) : null,
262
+ colonias.length && !coloniaSelect ? /* @__PURE__ */ jsx("datalist", { id: listId, children: colonias.map((c) => /* @__PURE__ */ jsx("option", { value: c }, c)) }) : null,
177
263
  /* @__PURE__ */ jsx("p", { role: "status", "aria-live": "polite", "data-sf-zip-status": true, style: filled ? void 0 : { position: "absolute", width: 1, height: 1, overflow: "hidden", clip: "rect(0 0 0 0)", whiteSpace: "nowrap" }, className: filled ? join(cx.hint, cx.wide) : void 0, children: filled ? m.lookupFilled : "" }),
178
264
  /* @__PURE__ */ jsx(CheckoutField, { ...common, name: n("address1"), label: m.address1, autoComplete: "address-line1", defaultValue: defaults?.address1 ?? "", error: errors[n("address1")] ?? null, hint: m.address1Hint, classNames: field(cx, "wide") }),
179
265
  /* @__PURE__ */ jsx(CheckoutField, { ...common, name: n("address2"), label: m.address2, autoComplete: "address-line2", optional: true, defaultValue: defaults?.address2 ?? "", error: errors[n("address2")] ?? null, classNames: field(cx, "half") }),
@@ -667,6 +753,6 @@ function DeliveryOptions(props) {
667
753
  ] });
668
754
  }
669
755
 
670
- export { AddressFields, ContactFields, DeliveryOptions, OrderSummary, OrderSummaryDisclosure, addressFieldsMessages, dedupeRates, deliveryMessages, flatRates, isQuoteChange, matchRate, orderSummaryMessages, quoteDeadline, quoteNote, rateBadges, rateCarrier, rateEta, rateService, recommendRates };
756
+ export { ADDRESS_FILL_EVENT_FLAG, AddressFields, ContactFields, DeliveryOptions, OrderSummary, OrderSummaryDisclosure, addressFieldsMessages, dedupeRates, deliveryMessages, flatRates, isAddressFillEvent, isQuoteChange, matchRate, orderSummaryMessages, quoteDeadline, quoteNote, rateBadges, rateCarrier, rateEta, rateService, recommendRates };
671
757
  //# sourceMappingURL=checkout.js.map
672
758
  //# sourceMappingURL=checkout.js.map