@12-apps/ui 6.82.0 → 6.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/components-catalog.md +7 -0
- package/dist/_internal/components/form/CepField/CepField.js +1 -1
- package/dist/_internal/components/form/index.js +1 -1
- package/dist/_internal/components/form/total-form/Fields/CepField.js +2 -2
- package/dist/_internal/components/form/total-form/Fields/index.js +3 -3
- package/dist/_internal/components/index.js +1 -1
- package/dist/{chunk-THD3N4BJ.js → chunk-EBEMNKTK.js} +19 -3
- package/dist/chunk-EBEMNKTK.js.map +1 -0
- package/dist/{chunk-SMANO5OD.js → chunk-GJOU26UW.js} +2 -2
- package/dist/{chunk-QCNUMOTL.js → chunk-XNLJ2Z7T.js} +7 -3
- package/dist/chunk-XNLJ2Z7T.js.map +1 -0
- package/dist/form/CepField.js +1 -1
- package/dist/form/total-form.js +3 -3
- package/dist/types/components/form/CepField/CepField.d.ts +2 -1
- package/dist/types/components/form/CepField/CepField.d.ts.map +1 -1
- package/dist/types/components/form/CepField/CepField.types.d.ts +16 -0
- package/dist/types/components/form/CepField/CepField.types.d.ts.map +1 -1
- package/dist/types/components/form/total-form/Fields/CepField.d.ts +6 -1
- package/dist/types/components/form/total-form/Fields/CepField.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/form/CepField/CepField.md +22 -0
- package/src/components/form/CepField/CepField.tsx +31 -5
- package/src/components/form/CepField/CepField.types.ts +16 -0
- package/src/components/form/total-form/Fields/CepField.tsx +9 -0
- package/dist/chunk-QCNUMOTL.js.map +0 -1
- package/dist/chunk-THD3N4BJ.js.map +0 -1
- /package/dist/{chunk-SMANO5OD.js.map → chunk-GJOU26UW.js.map} +0 -0
package/components-catalog.md
CHANGED
|
@@ -351,6 +351,13 @@ Components for user input and form interactions.
|
|
|
351
351
|
**Location**: `src/components/form/DateRangePicker`
|
|
352
352
|
**Docs**: `src/components/form/DateRangePicker/DateRangePicker.md`
|
|
353
353
|
|
|
354
|
+
### CepField
|
|
355
|
+
**Purpose**: CEP input that masks, looks the address up and hands it back to fill the form
|
|
356
|
+
**Use Cases**: Delivery/checkout address forms, supplier and store-location forms
|
|
357
|
+
**Features**: `#####-###` mask, debounced race-safe lookup injected via `onLookup`, live-region status, degrades to a plain input, `size` (the `Input` scale) and `labelPlacement` (`above` | `inside`) to sit level with `Input`s
|
|
358
|
+
**Location**: `src/components/form/CepField`
|
|
359
|
+
**Docs**: `src/components/form/CepField/CepField.md`
|
|
360
|
+
|
|
354
361
|
### Checkbox
|
|
355
362
|
**Purpose**: Toggle selection checkbox
|
|
356
363
|
**Use Cases**: Multi-select, boolean options, agreements
|
|
@@ -92,7 +92,7 @@ import "../../../chunk-IYC7NPX7.js";
|
|
|
92
92
|
import "../../../chunk-6XLMTOJ3.js";
|
|
93
93
|
import {
|
|
94
94
|
CepField
|
|
95
|
-
} from "../../../chunk-
|
|
95
|
+
} from "../../../chunk-EBEMNKTK.js";
|
|
96
96
|
import "../../../chunk-BTGXHQX3.js";
|
|
97
97
|
import "../../../chunk-IDMG332I.js";
|
|
98
98
|
import {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import {
|
|
3
3
|
CepField
|
|
4
|
-
} from "../../../../../chunk-
|
|
4
|
+
} from "../../../../../chunk-XNLJ2Z7T.js";
|
|
5
5
|
import "../../../../../chunk-4XK6JQ4D.js";
|
|
6
6
|
import "../../../../../chunk-6XLMTOJ3.js";
|
|
7
|
-
import "../../../../../chunk-
|
|
7
|
+
import "../../../../../chunk-EBEMNKTK.js";
|
|
8
8
|
import "../../../../../chunk-BTGXHQX3.js";
|
|
9
9
|
import "../../../../../chunk-MO4FFK62.js";
|
|
10
10
|
import "../../../../../chunk-4AJY7HMH.js";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import "../../../../../chunk-
|
|
1
|
+
import "../../../../../chunk-GJOU26UW.js";
|
|
2
2
|
import {
|
|
3
3
|
CategoryField
|
|
4
4
|
} from "../../../../../chunk-Y3TD7HOY.js";
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
} from "../../../../../chunk-6LMYQJZP.js";
|
|
14
14
|
import {
|
|
15
15
|
CepField
|
|
16
|
-
} from "../../../../../chunk-
|
|
16
|
+
} from "../../../../../chunk-XNLJ2Z7T.js";
|
|
17
17
|
import "../../../../../chunk-4XK6JQ4D.js";
|
|
18
18
|
import "../../../../../chunk-ONMSTW4B.js";
|
|
19
19
|
import "../../../../../chunk-TR7ZA67L.js";
|
|
@@ -24,7 +24,7 @@ import "../../../../../chunk-WNIL2KTB.js";
|
|
|
24
24
|
import "../../../../../chunk-3XZZSHE5.js";
|
|
25
25
|
import "../../../../../chunk-IYC7NPX7.js";
|
|
26
26
|
import "../../../../../chunk-6XLMTOJ3.js";
|
|
27
|
-
import "../../../../../chunk-
|
|
27
|
+
import "../../../../../chunk-EBEMNKTK.js";
|
|
28
28
|
import "../../../../../chunk-BTGXHQX3.js";
|
|
29
29
|
import "../../../../../chunk-QYKDY4EI.js";
|
|
30
30
|
import "../../../../../chunk-MWPD2BXO.js";
|
|
@@ -318,7 +318,7 @@ import "../../chunk-IYC7NPX7.js";
|
|
|
318
318
|
import "../../chunk-6XLMTOJ3.js";
|
|
319
319
|
import {
|
|
320
320
|
CepField
|
|
321
|
-
} from "../../chunk-
|
|
321
|
+
} from "../../chunk-EBEMNKTK.js";
|
|
322
322
|
import "../../chunk-BTGXHQX3.js";
|
|
323
323
|
import "../../chunk-IDMG332I.js";
|
|
324
324
|
import {
|
|
@@ -62,6 +62,17 @@ function CepStatus({
|
|
|
62
62
|
const visible = !suppressed && status !== "idle";
|
|
63
63
|
return /* @__PURE__ */ jsx("span", { id: `${testId}-status`, role: "status", "aria-live": "polite", children: visible && /* @__PURE__ */ jsx(FormMessage, { error: status === "notfound", dataTestId: `${testId}-${status}`, children: copy[status] }) });
|
|
64
64
|
}
|
|
65
|
+
function AboveLabel({
|
|
66
|
+
name,
|
|
67
|
+
label,
|
|
68
|
+
error
|
|
69
|
+
}) {
|
|
70
|
+
if (!label) return null;
|
|
71
|
+
return /* @__PURE__ */ jsx(FormLabel, { htmlFor: name, error: Boolean(error), children: label });
|
|
72
|
+
}
|
|
73
|
+
function insideLabel(label, placement) {
|
|
74
|
+
return placement === "inside" ? label : void 0;
|
|
75
|
+
}
|
|
65
76
|
function CepField({
|
|
66
77
|
value,
|
|
67
78
|
onChange,
|
|
@@ -75,7 +86,10 @@ function CepField({
|
|
|
75
86
|
debounceMs = 500,
|
|
76
87
|
name = "postalCode",
|
|
77
88
|
dataTestId,
|
|
78
|
-
copy
|
|
89
|
+
copy,
|
|
90
|
+
size,
|
|
91
|
+
// undefined reaches `Input`, whose own default is `md`
|
|
92
|
+
labelPlacement = "above"
|
|
79
93
|
}) {
|
|
80
94
|
const testId = dataTestId ?? `cep-field-${name}`;
|
|
81
95
|
const status = useCepLookup({
|
|
@@ -92,7 +106,7 @@ function CepField({
|
|
|
92
106
|
[onChange]
|
|
93
107
|
);
|
|
94
108
|
return /* @__PURE__ */ jsxs(FormControl, { fullWidth: true, children: [
|
|
95
|
-
|
|
109
|
+
labelPlacement === "above" && /* @__PURE__ */ jsx(AboveLabel, { name, label, error }),
|
|
96
110
|
/* @__PURE__ */ jsx(
|
|
97
111
|
Input,
|
|
98
112
|
{
|
|
@@ -101,6 +115,8 @@ function CepField({
|
|
|
101
115
|
type: "text",
|
|
102
116
|
inputMode: "numeric",
|
|
103
117
|
autoComplete: "postal-code",
|
|
118
|
+
size,
|
|
119
|
+
label: insideLabel(label, labelPlacement),
|
|
104
120
|
placeholder,
|
|
105
121
|
value,
|
|
106
122
|
error: Boolean(error),
|
|
@@ -122,4 +138,4 @@ export {
|
|
|
122
138
|
CepField,
|
|
123
139
|
CepField_default
|
|
124
140
|
};
|
|
125
|
-
//# sourceMappingURL=chunk-
|
|
141
|
+
//# sourceMappingURL=chunk-EBEMNKTK.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/form/CepField/CepField.tsx"],"sourcesContent":["'use client';\n\nimport type { CepFieldCopy } from \"../../../copy\";\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { cepDigits, formatCep, isValidCep } from '@12-apps/forms-core';\n\nimport { FormControl, FormLabel, FormMessage } from '../Form';\n// The outlined entry: CepField renders no variant, and the generic one would\n// ship MUI's FilledInput and Input to every screen with a CEP field (FUT-1054).\nimport { Input } from '../Input/outlined';\nimport type { CepAddress, CepFieldProps, CepLookupStatus } from './CepField.types';\n\n/** Everything {@link useCepLookup} needs, so the hook signature stays flat. */\ninterface LookupOptions {\n digits: string;\n complete: boolean;\n disabled: boolean;\n debounceMs: number;\n onLookup?: (cep: string) => Promise<CepAddress | null>;\n onResolved?: (address: CepAddress) => void;\n onNotFound?: () => void;\n}\n\n/**\n * Debounced, race-safe CEP resolution.\n *\n * Two guards earn their keep here: a monotonic sequence id, bumped\n * synchronously on every new query, so a slow response for a since-edited CEP\n * can never land; and a \"last queried\" marker, so re-rendering (or the caller\n * re-masking the same value) never re-hits the provider.\n */\nfunction useCepLookup({\n digits,\n complete,\n disabled,\n debounceMs,\n onLookup,\n onResolved,\n onNotFound,\n}: LookupOptions): CepLookupStatus {\n const [status, setStatus] = useState<CepLookupStatus>('idle');\n const lastQueriedRef = useRef('');\n const seqRef = useRef(0);\n // Latest callbacks, read at call time — keeps them out of the effect deps so\n // an inline arrow from the caller doesn't re-trigger the lookup every render.\n const lookupRef = useRef(onLookup);\n const resolvedRef = useRef(onResolved);\n const notFoundRef = useRef(onNotFound);\n lookupRef.current = onLookup;\n resolvedRef.current = onResolved;\n notFoundRef.current = onNotFound;\n\n useEffect(() => {\n if (!complete || disabled || !lookupRef.current) {\n // Editing away from a complete CEP drops any stale status and forgets the\n // last query, so re-typing the same CEP resolves again.\n seqRef.current += 1;\n setStatus('idle');\n lastQueriedRef.current = '';\n return;\n }\n if (lastQueriedRef.current === digits) return;\n\n const mySeq = (seqRef.current += 1);\n const handle = setTimeout(() => {\n lastQueriedRef.current = digits;\n setStatus('loading');\n void (async () => {\n // A failing lookup is treated exactly like \"not found\" — the user types\n // the address instead. Never surfaced as a crash. Going through\n // `Promise.resolve().then` rather than calling directly covers the two\n // failure modes an `await … .catch()` misses: a lookup that throws\n // SYNCHRONOUSLY, and one that returns something other than a promise.\n const address = await Promise.resolve()\n .then(() => lookupRef.current?.(digits))\n .catch(() => null);\n if (seqRef.current !== mySeq) return;\n setStatus(address ? 'found' : 'notfound');\n // A miss is reported too, so a caller that filled from a PREVIOUS CEP\n // can undo it rather than leaving that address under this CEP.\n if (address) resolvedRef.current?.(address);\n else notFoundRef.current?.();\n })();\n }, debounceMs);\n\n return () => clearTimeout(handle);\n }, [digits, complete, disabled, debounceMs]);\n\n return status;\n}\n\n/**\n * The lookup outcome as a live region, so screen readers announce it without\n * stealing focus. Silent while idle, and while the caller is showing its own\n * validation error (which would otherwise stack two messages).\n */\nfunction CepStatus({\n status,\n testId,\n suppressed,\n copy,\n}: {\n status: CepLookupStatus;\n testId: string;\n suppressed: boolean;\n copy: CepFieldCopy;\n}): React.ReactElement {\n const visible = !suppressed && status !== 'idle';\n return (\n <span id={`${testId}-status`} role=\"status\" aria-live=\"polite\">\n {visible && (\n <FormMessage error={status === 'notfound'} dataTestId={`${testId}-${status}`}>\n {copy[status as Exclude<CepLookupStatus, 'idle'>]}\n </FormMessage>\n )}\n </span>\n );\n}\n\n/** The stacked label over the box (`labelPlacement=\"above\"`), or nothing when there is no label. */\nfunction AboveLabel({\n name,\n label,\n error,\n}: {\n name: string;\n label?: string;\n error?: string;\n}): React.ReactElement | null {\n if (!label) return null;\n return (\n <FormLabel htmlFor={name} error={Boolean(error)}>\n {label}\n </FormLabel>\n );\n}\n\n/** The label handed to the `Input`, which draws it in the box: only for `inside`. */\nfunction insideLabel(label: string | undefined, placement: 'above' | 'inside'): string | undefined {\n return placement === 'inside' ? label : undefined;\n}\n\n/**\n * A CEP input that fills the rest of the address in.\n *\n * The pattern every Brazilian form has used for twenty years: type eight\n * digits, and logradouro/bairro/cidade/UF arrive on their own. This is the\n * reusable half — masking, validation, debouncing, race-safety and status —\n * with the actual lookup injected via `onLookup`, so the same component serves\n * a supplier form, a store-location form or a delivery checkout without\n * knowing anything about providers or auth.\n *\n * Resolution is best-effort by design. A provider outage, an unknown CEP or a\n * timeout leaves the field exactly as usable as a plain text input — the user\n * types the address themselves. Autofill never blocks.\n *\n * @example\n * ```tsx\n * <CepField\n * value={cep}\n * onChange={setCep}\n * onLookup={(c) => fetch(`/api/…/lookup/cep/${c}`).then((r) => r.json()).then((b) => b.data)}\n * onResolved={(a) => { setStreet(a.street ?? ''); setCity(a.city ?? ''); }}\n * />\n * ```\n */\nexport function CepField({\n value,\n onChange,\n onLookup,\n onResolved,\n onNotFound,\n label = 'CEP',\n placeholder = '00000-000',\n error,\n disabled = false,\n debounceMs = 500,\n name = 'postalCode',\n dataTestId,\n copy,\n size, // undefined reaches `Input`, whose own default is `md`\n labelPlacement = 'above',\n}: CepFieldProps): React.ReactElement {\n const testId = dataTestId ?? `cep-field-${name}`;\n\n const status = useCepLookup({\n digits: cepDigits(value),\n complete: isValidCep(value),\n disabled,\n debounceMs,\n onLookup,\n onResolved,\n onNotFound,\n });\n\n const handleChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => onChange(formatCep(event.target.value)),\n [onChange],\n );\n\n return (\n <FormControl fullWidth>\n {labelPlacement === 'above' && <AboveLabel name={name} label={label} error={error} />}\n <Input\n id={name}\n name={name}\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"postal-code\"\n size={size}\n // `inside`: the label is the Input's own, drawn in the box and floating\n // up on focus or a value — what every other `Input` in the form does.\n // `above` keeps the stacked `FormLabel`, for callers that never opted in.\n label={insideLabel(label, labelPlacement)}\n placeholder={placeholder}\n value={value}\n error={Boolean(error)}\n disabled={disabled}\n loading={status === 'loading'}\n onChange={handleChange}\n data-testid={testId}\n aria-describedby={`${testId}-status`}\n aria-busy={status === 'loading'}\n />\n {error && (\n <FormMessage error dataTestId={`${testId}-message`}>\n {error}\n </FormMessage>\n )}\n <CepStatus status={status} testId={testId} suppressed={Boolean(error)} copy={copy} />\n </FormControl>\n );\n}\n\nexport default CepField;\n"],"mappings":"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","names":[]}
|
|
@@ -12,7 +12,7 @@ import {
|
|
|
12
12
|
} from "./chunk-6LMYQJZP.js";
|
|
13
13
|
import {
|
|
14
14
|
CepField
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-XNLJ2Z7T.js";
|
|
16
16
|
import {
|
|
17
17
|
__export
|
|
18
18
|
} from "./chunk-PZ5AY32C.js";
|
|
@@ -30,4 +30,4 @@ __export(Fields_exports, {
|
|
|
30
30
|
export {
|
|
31
31
|
Fields_exports
|
|
32
32
|
};
|
|
33
|
-
//# sourceMappingURL=chunk-
|
|
33
|
+
//# sourceMappingURL=chunk-GJOU26UW.js.map
|
|
@@ -3,7 +3,7 @@ import {
|
|
|
3
3
|
} from "./chunk-4XK6JQ4D.js";
|
|
4
4
|
import {
|
|
5
5
|
CepField
|
|
6
|
-
} from "./chunk-
|
|
6
|
+
} from "./chunk-EBEMNKTK.js";
|
|
7
7
|
|
|
8
8
|
// src/components/form/total-form/Fields/CepField.tsx
|
|
9
9
|
import { useCallback, useMemo, useRef } from "react";
|
|
@@ -41,7 +41,9 @@ function CepField2({
|
|
|
41
41
|
lookup,
|
|
42
42
|
fills,
|
|
43
43
|
debounceMs,
|
|
44
|
-
copy
|
|
44
|
+
copy,
|
|
45
|
+
size,
|
|
46
|
+
labelPlacement
|
|
45
47
|
}) {
|
|
46
48
|
const { values, errors, setFieldValue } = useFormContext();
|
|
47
49
|
const { write, clearOwned } = useOwnedAutofill();
|
|
@@ -72,6 +74,8 @@ function CepField2({
|
|
|
72
74
|
onResolved: handleResolved,
|
|
73
75
|
onNotFound: clearOwned,
|
|
74
76
|
debounceMs,
|
|
77
|
+
size,
|
|
78
|
+
labelPlacement,
|
|
75
79
|
dataTestId: `total-form-field-${name}`
|
|
76
80
|
}
|
|
77
81
|
);
|
|
@@ -80,4 +84,4 @@ function CepField2({
|
|
|
80
84
|
export {
|
|
81
85
|
CepField2 as CepField
|
|
82
86
|
};
|
|
83
|
-
//# sourceMappingURL=chunk-
|
|
87
|
+
//# sourceMappingURL=chunk-XNLJ2Z7T.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/components/form/total-form/Fields/CepField.tsx"],"sourcesContent":["'use client';\n\nimport type { CepFieldCopy } from \"../../../../copy\";\nimport React, { useCallback, useMemo, useRef } from 'react';\n\nimport type { InputSize } from '../../Input/Input.base';\nimport { CepField as BaseCepField, type CepAddress } from '../../CepField';\nimport { useFormContext } from '../FormContext';\n\n/** The resolved address parts a form can receive. */\ntype CepPart = 'street' | 'neighborhood' | 'city' | 'state';\n\n/**\n * Which form field receives each resolved part. Omit a part to leave it alone —\n * a form with no bairro column simply doesn't map `neighborhood`.\n */\nexport type CepFills = Partial<Record<CepPart, string>>;\n\n/** Props for the `total-form` {@link CepField}. */\nexport interface CepFieldProps {\n /** The lookup status wording. REQUIRED — no default copy. */\n copy: CepFieldCopy;\n /** Field name; also the key into the form's values/errors. */\n name: string;\n /** Optional visible label. Defaults to `CEP`. */\n label?: string;\n /** Resolve a complete CEP. Omit for a masked input with no lookup. */\n lookup?: (cep: string) => Promise<CepAddress | null>;\n /** Maps each resolved address part onto a field in THIS form. */\n fills?: CepFills;\n /** Milliseconds to wait after the last keystroke before looking up. */\n debounceMs?: number;\n /** The input's size — the same scale, and box heights, as `Input`. Defaults to `md`. */\n size?: InputSize;\n /** `above` (default) stacks the label over the box; `inside` floats it in the box like `Input`. */\n labelPlacement?: 'above' | 'inside';\n}\n\nconst PARTS: readonly CepPart[] = ['street', 'neighborhood', 'city', 'state'];\n\n/** Ownership-aware writes into sibling form fields. */\ninterface OwnedAutofill {\n /** Write one field, where `''` means \"no value for this part\". */\n write: (field: string, value: string) => void;\n /** Blank every field this hook still owns. */\n clearOwned: () => void;\n}\n\n/**\n * The honesty rule, in one place: this hook remembers exactly what it wrote,\n * writes only into fields that are empty or still hold its own value, and drops\n * the ownership record when it blanks one. That single invariant is what lets\n * autofill replace itself freely while never touching the user's typing.\n */\nfunction useOwnedAutofill(): OwnedAutofill {\n const { values, setFieldValue } = useFormContext();\n // What we last auto-filled per target field, so a later lookup may replace\n // its own value while a manual edit is preserved.\n const autoFilledRef = useRef<Record<string, string>>({});\n // Read values at call time without making them a dependency of the callback.\n const valuesRef = useRef(values);\n valuesRef.current = values;\n\n const write = useCallback(\n (field: string, value: string) => {\n const current = (valuesRef.current[field] ?? '').trim();\n const ours = autoFilledRef.current[field] === current;\n if (current !== '' && !ours) return;\n setFieldValue(field, value);\n if (value) {\n autoFilledRef.current[field] = value;\n } else {\n delete autoFilledRef.current[field];\n }\n },\n [setFieldValue],\n );\n\n const clearOwned = useCallback(() => {\n // Object.keys snapshots, so `write` may delete entries as we iterate.\n for (const field of Object.keys(autoFilledRef.current)) {\n write(field, '');\n }\n }, [write]);\n\n return useMemo(() => ({ write, clearOwned }), [write, clearOwned]);\n}\n\n/**\n * CEP field bound to {@link useFormContext}: masks and validates the CEP, then\n * writes the resolved logradouro/bairro/cidade/UF straight into the sibling\n * fields named by `fills`.\n *\n * It will not overwrite work the user has done. A target field is only written\n * when it is empty, or when it still holds the exact value THIS component put\n * there — so correcting the CEP re-fills the address, but a hand-edited street\n * survives the next lookup.\n *\n * @example\n * ```tsx\n * <Fields.CepField\n * name=\"postalCode\"\n * lookup={lookupCep}\n * fills={{ street: 'addressLine1', neighborhood: 'neighborhood', city: 'city', state: 'state' }}\n * />\n * ```\n */\nexport function CepField({\n name,\n label,\n lookup,\n fills,\n debounceMs,\n copy,\n size,\n labelPlacement,\n}: CepFieldProps): React.ReactElement {\n const { values, errors, setFieldValue } = useFormContext();\n const { write, clearOwned } = useOwnedAutofill();\n\n const handleResolved = useCallback(\n (address: CepAddress) => {\n if (!fills) return;\n // Every mapped part is written on EVERY resolve, absent ones as ''. A\n // \"CEP geral\" (a small município's catch-all) carries no logradouro, so\n // skipping absent parts would leave the previous CEP's street here.\n for (const part of PARTS) {\n const field = fills[part];\n if (field) write(field, address[part] ?? '');\n }\n },\n [fills, write],\n );\n\n const handleChange = useCallback(\n (masked: string) => setFieldValue(name, masked),\n [name, setFieldValue],\n );\n\n return (\n <BaseCepField\n copy={copy}\n name={name}\n label={label}\n value={values[name] ?? ''}\n error={errors[name]}\n onChange={handleChange}\n onLookup={lookup}\n onResolved={handleResolved}\n onNotFound={clearOwned}\n debounceMs={debounceMs}\n size={size}\n labelPlacement={labelPlacement}\n dataTestId={`total-form-field-${name}`}\n />\n );\n}\n"],"mappings":";;;;;;;;AAGA,SAAgB,aAAa,SAAS,cAAc;AAyIhD;AAtGJ,IAAM,QAA4B,CAAC,UAAU,gBAAgB,QAAQ,OAAO;AAgB5E,SAAS,mBAAkC;AACzC,QAAM,EAAE,QAAQ,cAAc,IAAI,eAAe;AAGjD,QAAM,gBAAgB,OAA+B,CAAC,CAAC;AAEvD,QAAM,YAAY,OAAO,MAAM;AAC/B,YAAU,UAAU;AAEpB,QAAM,QAAQ;AAAA,IACZ,CAAC,OAAe,UAAkB;AAChC,YAAM,WAAW,UAAU,QAAQ,KAAK,KAAK,IAAI,KAAK;AACtD,YAAM,OAAO,cAAc,QAAQ,KAAK,MAAM;AAC9C,UAAI,YAAY,MAAM,CAAC,KAAM;AAC7B,oBAAc,OAAO,KAAK;AAC1B,UAAI,OAAO;AACT,sBAAc,QAAQ,KAAK,IAAI;AAAA,MACjC,OAAO;AACL,eAAO,cAAc,QAAQ,KAAK;AAAA,MACpC;AAAA,IACF;AAAA,IACA,CAAC,aAAa;AAAA,EAChB;AAEA,QAAM,aAAa,YAAY,MAAM;AAEnC,eAAW,SAAS,OAAO,KAAK,cAAc,OAAO,GAAG;AACtD,YAAM,OAAO,EAAE;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,KAAK,CAAC;AAEV,SAAO,QAAQ,OAAO,EAAE,OAAO,WAAW,IAAI,CAAC,OAAO,UAAU,CAAC;AACnE;AAqBO,SAASA,UAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAsC;AACpC,QAAM,EAAE,QAAQ,QAAQ,cAAc,IAAI,eAAe;AACzD,QAAM,EAAE,OAAO,WAAW,IAAI,iBAAiB;AAE/C,QAAM,iBAAiB;AAAA,IACrB,CAAC,YAAwB;AACvB,UAAI,CAAC,MAAO;AAIZ,iBAAW,QAAQ,OAAO;AACxB,cAAM,QAAQ,MAAM,IAAI;AACxB,YAAI,MAAO,OAAM,OAAO,QAAQ,IAAI,KAAK,EAAE;AAAA,MAC7C;AAAA,IACF;AAAA,IACA,CAAC,OAAO,KAAK;AAAA,EACf;AAEA,QAAM,eAAe;AAAA,IACnB,CAAC,WAAmB,cAAc,MAAM,MAAM;AAAA,IAC9C,CAAC,MAAM,aAAa;AAAA,EACtB;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,OAAO,IAAI,KAAK;AAAA,MACvB,OAAO,OAAO,IAAI;AAAA,MAClB,UAAU;AAAA,MACV,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ;AAAA,MACA;AAAA,MACA;AAAA,MACA,YAAY,oBAAoB,IAAI;AAAA;AAAA,EACtC;AAEJ;","names":["CepField"]}
|
package/dist/form/CepField.js
CHANGED
package/dist/form/total-form.js
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "../chunk-CPAJYJUD.js";
|
|
7
7
|
import {
|
|
8
8
|
Fields_exports
|
|
9
|
-
} from "../chunk-
|
|
9
|
+
} from "../chunk-GJOU26UW.js";
|
|
10
10
|
import {
|
|
11
11
|
CategoryField
|
|
12
12
|
} from "../chunk-Y3TD7HOY.js";
|
|
@@ -17,7 +17,7 @@ import {
|
|
|
17
17
|
import {
|
|
18
18
|
SelectField
|
|
19
19
|
} from "../chunk-6LMYQJZP.js";
|
|
20
|
-
import "../chunk-
|
|
20
|
+
import "../chunk-XNLJ2Z7T.js";
|
|
21
21
|
import {
|
|
22
22
|
SubmitButton
|
|
23
23
|
} from "../chunk-UERGWYGY.js";
|
|
@@ -34,7 +34,7 @@ import "../chunk-WNIL2KTB.js";
|
|
|
34
34
|
import "../chunk-3XZZSHE5.js";
|
|
35
35
|
import "../chunk-IYC7NPX7.js";
|
|
36
36
|
import "../chunk-6XLMTOJ3.js";
|
|
37
|
-
import "../chunk-
|
|
37
|
+
import "../chunk-EBEMNKTK.js";
|
|
38
38
|
import "../chunk-BTGXHQX3.js";
|
|
39
39
|
import "../chunk-JD5BVIM7.js";
|
|
40
40
|
import "../chunk-T77OVQEO.js";
|
|
@@ -24,6 +24,7 @@ import type { CepFieldProps } from './CepField.types';
|
|
|
24
24
|
* />
|
|
25
25
|
* ```
|
|
26
26
|
*/
|
|
27
|
-
export declare function CepField({ value, onChange, onLookup, onResolved, onNotFound, label, placeholder, error, disabled, debounceMs, name, dataTestId, copy,
|
|
27
|
+
export declare function CepField({ value, onChange, onLookup, onResolved, onNotFound, label, placeholder, error, disabled, debounceMs, name, dataTestId, copy, size, // undefined reaches `Input`, whose own default is `md`
|
|
28
|
+
labelPlacement, }: CepFieldProps): React.ReactElement;
|
|
28
29
|
export default CepField;
|
|
29
30
|
//# sourceMappingURL=CepField.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CepField.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/CepField/CepField.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAQxE,OAAO,KAAK,EAAc,aAAa,EAAmB,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"CepField.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/CepField/CepField.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAmD,MAAM,OAAO,CAAC;AAQxE,OAAO,KAAK,EAAc,aAAa,EAAmB,MAAM,kBAAkB,CAAC;AAoInF;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,wBAAgB,QAAQ,CAAC,EACvB,KAAK,EACL,QAAQ,EACR,QAAQ,EACR,UAAU,EACV,UAAU,EACV,KAAa,EACb,WAAyB,EACzB,KAAK,EACL,QAAgB,EAChB,UAAgB,EAChB,IAAmB,EACnB,UAAU,EACV,IAAI,EACJ,IAAI,EAAE,uDAAuD;AAC7D,cAAwB,GACzB,EAAE,aAAa,GAAG,KAAK,CAAC,YAAY,CAkDpC;AAED,eAAe,QAAQ,CAAC"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CepFieldCopy } from "../../../copy";
|
|
2
|
+
import type { InputSize } from "../Input/Input.base";
|
|
2
3
|
/**
|
|
3
4
|
* Types for {@link CepField} — the reusable CEP auto-completer.
|
|
4
5
|
*/
|
|
@@ -70,5 +71,20 @@ export interface CepFieldProps {
|
|
|
70
71
|
name?: string;
|
|
71
72
|
/** Test id for the input. Defaults to `cep-field-<name>`. */
|
|
72
73
|
dataTestId?: string;
|
|
74
|
+
/**
|
|
75
|
+
* A step of the house size scale — the same values, and the same box
|
|
76
|
+
* heights, as {@link Input}'s `size`. Defaults to `md`.
|
|
77
|
+
*/
|
|
78
|
+
size?: InputSize;
|
|
79
|
+
/**
|
|
80
|
+
* Where the label is drawn.
|
|
81
|
+
*
|
|
82
|
+
* - `above` (default): a stacked label over the box, as the field has always
|
|
83
|
+
* rendered. Every existing caller keeps it.
|
|
84
|
+
* - `inside`: the label sits in the box and floats up on focus or a value,
|
|
85
|
+
* exactly as `Input`'s label does. Use it beside `Input`s so the CEP block
|
|
86
|
+
* is the same height as its neighbours.
|
|
87
|
+
*/
|
|
88
|
+
labelPlacement?: 'above' | 'inside';
|
|
73
89
|
}
|
|
74
90
|
//# sourceMappingURL=CepField.types.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CepField.types.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/CepField/CepField.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD;;GAEG;AAEH;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,cAAc;IACd,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iBAAiB;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU;IACV,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,oEAAoE;AACpE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,UAAU,CAAC;AAExE,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,IAAI,EAAE,YAAY,CAAC;IACnB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACvD;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,CAAC;IAC3C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"CepField.types.d.ts","sourceRoot":"","sources":["../../../../../src/components/form/CepField/CepField.types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAClD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,qBAAqB,CAAC;AACrD;;GAEG;AAEH;;;;;;;;;GASG;AACH,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,GAAG,EAAE,MAAM,CAAC;IACZ,2CAA2C;IAC3C,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,cAAc;IACd,YAAY,EAAE,MAAM,GAAG,IAAI,CAAC;IAC5B,iBAAiB;IACjB,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IACpB,UAAU;IACV,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;CACtB;AAED,oEAAoE;AACpE,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,UAAU,CAAC;AAExE,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,IAAI,EAAE,YAAY,CAAC;IACnB,8EAA8E;IAC9E,KAAK,EAAE,MAAM,CAAC;IACd;;;OAGG;IACH,QAAQ,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACvD;;;OAGG;IACH,UAAU,CAAC,EAAE,CAAC,OAAO,EAAE,UAAU,KAAK,IAAI,CAAC;IAC3C;;;;;OAKG;IACH,UAAU,CAAC,EAAE,MAAM,IAAI,CAAC;IACxB,wCAAwC;IACxC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iDAAiD;IACjD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uDAAuD;IACvD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;OAGG;IACH,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB;;;;;;;;OAQG;IACH,cAAc,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CACrC"}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CepFieldCopy } from "../../../../copy";
|
|
2
2
|
import React from 'react';
|
|
3
|
+
import type { InputSize } from '../../Input/Input.base';
|
|
3
4
|
import { type CepAddress } from '../../CepField';
|
|
4
5
|
/** The resolved address parts a form can receive. */
|
|
5
6
|
type CepPart = 'street' | 'neighborhood' | 'city' | 'state';
|
|
@@ -22,6 +23,10 @@ export interface CepFieldProps {
|
|
|
22
23
|
fills?: CepFills;
|
|
23
24
|
/** Milliseconds to wait after the last keystroke before looking up. */
|
|
24
25
|
debounceMs?: number;
|
|
26
|
+
/** The input's size — the same scale, and box heights, as `Input`. Defaults to `md`. */
|
|
27
|
+
size?: InputSize;
|
|
28
|
+
/** `above` (default) stacks the label over the box; `inside` floats it in the box like `Input`. */
|
|
29
|
+
labelPlacement?: 'above' | 'inside';
|
|
25
30
|
}
|
|
26
31
|
/**
|
|
27
32
|
* CEP field bound to {@link useFormContext}: masks and validates the CEP, then
|
|
@@ -42,6 +47,6 @@ export interface CepFieldProps {
|
|
|
42
47
|
* />
|
|
43
48
|
* ```
|
|
44
49
|
*/
|
|
45
|
-
export declare function CepField({ name, label, lookup, fills, debounceMs, copy, }: CepFieldProps): React.ReactElement;
|
|
50
|
+
export declare function CepField({ name, label, lookup, fills, debounceMs, copy, size, labelPlacement, }: CepFieldProps): React.ReactElement;
|
|
46
51
|
export {};
|
|
47
52
|
//# sourceMappingURL=CepField.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"CepField.d.ts","sourceRoot":"","sources":["../../../../../../src/components/form/total-form/Fields/CepField.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAuC,MAAM,OAAO,CAAC;AAE5D,OAAO,EAA4B,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAG3E,qDAAqD;AACrD,KAAK,OAAO,GAAG,QAAQ,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;AAE5D;;;GAGG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;AAExD,mDAAmD;AACnD,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,IAAI,EAAE,YAAY,CAAC;IACnB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACrD,iEAAiE;IACjE,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"CepField.d.ts","sourceRoot":"","sources":["../../../../../../src/components/form/total-form/Fields/CepField.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,KAAuC,MAAM,OAAO,CAAC;AAE5D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACxD,OAAO,EAA4B,KAAK,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAG3E,qDAAqD;AACrD,KAAK,OAAO,GAAG,QAAQ,GAAG,cAAc,GAAG,MAAM,GAAG,OAAO,CAAC;AAE5D;;;GAGG;AACH,MAAM,MAAM,QAAQ,GAAG,OAAO,CAAC,MAAM,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC;AAExD,mDAAmD;AACnD,MAAM,WAAW,aAAa;IAC5B,6DAA6D;IAC7D,IAAI,EAAE,YAAY,CAAC;IACnB,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,iDAAiD;IACjD,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,sEAAsE;IACtE,MAAM,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,UAAU,GAAG,IAAI,CAAC,CAAC;IACrD,iEAAiE;IACjE,KAAK,CAAC,EAAE,QAAQ,CAAC;IACjB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,wFAAwF;IACxF,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,mGAAmG;IACnG,cAAc,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;CACrC;AAoDD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,QAAQ,CAAC,EACvB,IAAI,EACJ,KAAK,EACL,MAAM,EACN,KAAK,EACL,UAAU,EACV,IAAI,EACJ,IAAI,EACJ,cAAc,GACf,EAAE,aAAa,GAAG,KAAK,CAAC,YAAY,CAwCpC"}
|
package/package.json
CHANGED
|
@@ -20,6 +20,7 @@ serves a supplier form, a store-location form or a delivery checkout.
|
|
|
20
20
|
- Race-safe: a slow response for a since-edited CEP can never overwrite a newer one
|
|
21
21
|
- Degrades to a plain masked input when the lookup fails, throws, or is absent
|
|
22
22
|
- Accessible status via `role="status"` / `aria-live="polite"` and `aria-busy`
|
|
23
|
+
- Takes `Input`'s `size`, and can float its label inside the box like `Input`
|
|
23
24
|
- Works uncontrolled-by-a-form (props) or bound to `total-form` (`Fields.CepField`)
|
|
24
25
|
|
|
25
26
|
## Props Documentation
|
|
@@ -42,6 +43,11 @@ serves a supplier form, a store-location form or a delivery checkout.
|
|
|
42
43
|
- `placeholder`: Placeholder text (default `00000-000`)
|
|
43
44
|
- `name`: Field name / input id (default `postalCode`)
|
|
44
45
|
- `dataTestId`: Test id for the input (default `cep-field-<name>`)
|
|
46
|
+
- `size`: A step of the `Input` size scale (`xs` | `sm` | `md` | `lg` | `xl`, default
|
|
47
|
+
`md`). Same values and same box heights as `Input`, so a `CepField` at `size="sm"`
|
|
48
|
+
stands level with `sm` `Input`s
|
|
49
|
+
- `labelPlacement`: `above` (default) stacks the label over the box; `inside` draws it
|
|
50
|
+
in the box and floats it up on focus or a value, exactly as `Input`'s label does
|
|
45
51
|
|
|
46
52
|
### State Props
|
|
47
53
|
|
|
@@ -83,6 +89,20 @@ const [cep, setCep] = useState('');
|
|
|
83
89
|
/>
|
|
84
90
|
```
|
|
85
91
|
|
|
92
|
+
### Beside compact `Input`s
|
|
93
|
+
|
|
94
|
+
An address form made of `Input`s draws every label inside its box. Opt the CEP in to
|
|
95
|
+
the same treatment, and give it the form's `size`, so the CEP block is no taller than
|
|
96
|
+
its neighbours:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<CepField size="sm" labelPlacement="inside" value={cep} onChange={setCep} copy={copy} />
|
|
100
|
+
<Input size="sm" label="Rua" />
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
`labelPlacement` defaults to `above` so a form that never asked keeps the stacked label
|
|
104
|
+
it has always had. `Fields.CepField` takes the same two props.
|
|
105
|
+
|
|
86
106
|
### Bound to `total-form`
|
|
87
107
|
|
|
88
108
|
`Fields.CepField` wires the field to the surrounding `FormContainer` and writes
|
|
@@ -135,4 +155,6 @@ the lookup. The pure helpers (`formatCep`, `isValidCep`) live in
|
|
|
135
155
|
- `aria-busy` reflects the in-flight lookup
|
|
136
156
|
- `aria-describedby` links the input to its status region
|
|
137
157
|
- `inputMode="numeric"` brings up the numeric keypad on mobile
|
|
158
|
+
- With `labelPlacement="inside"` the label is the `Input`'s own, associated to the input
|
|
159
|
+
by `for`/`id`, so `getByLabelText` and screen readers name the field the same way
|
|
138
160
|
- `autoComplete="postal-code"` lets the browser's own autofill participate
|
|
@@ -118,6 +118,29 @@ function CepStatus({
|
|
|
118
118
|
);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
/** The stacked label over the box (`labelPlacement="above"`), or nothing when there is no label. */
|
|
122
|
+
function AboveLabel({
|
|
123
|
+
name,
|
|
124
|
+
label,
|
|
125
|
+
error,
|
|
126
|
+
}: {
|
|
127
|
+
name: string;
|
|
128
|
+
label?: string;
|
|
129
|
+
error?: string;
|
|
130
|
+
}): React.ReactElement | null {
|
|
131
|
+
if (!label) return null;
|
|
132
|
+
return (
|
|
133
|
+
<FormLabel htmlFor={name} error={Boolean(error)}>
|
|
134
|
+
{label}
|
|
135
|
+
</FormLabel>
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The label handed to the `Input`, which draws it in the box: only for `inside`. */
|
|
140
|
+
function insideLabel(label: string | undefined, placement: 'above' | 'inside'): string | undefined {
|
|
141
|
+
return placement === 'inside' ? label : undefined;
|
|
142
|
+
}
|
|
143
|
+
|
|
121
144
|
/**
|
|
122
145
|
* A CEP input that fills the rest of the address in.
|
|
123
146
|
*
|
|
@@ -156,6 +179,8 @@ export function CepField({
|
|
|
156
179
|
name = 'postalCode',
|
|
157
180
|
dataTestId,
|
|
158
181
|
copy,
|
|
182
|
+
size, // undefined reaches `Input`, whose own default is `md`
|
|
183
|
+
labelPlacement = 'above',
|
|
159
184
|
}: CepFieldProps): React.ReactElement {
|
|
160
185
|
const testId = dataTestId ?? `cep-field-${name}`;
|
|
161
186
|
|
|
@@ -176,17 +201,18 @@ export function CepField({
|
|
|
176
201
|
|
|
177
202
|
return (
|
|
178
203
|
<FormControl fullWidth>
|
|
179
|
-
{
|
|
180
|
-
<FormLabel htmlFor={name} error={Boolean(error)}>
|
|
181
|
-
{label}
|
|
182
|
-
</FormLabel>
|
|
183
|
-
)}
|
|
204
|
+
{labelPlacement === 'above' && <AboveLabel name={name} label={label} error={error} />}
|
|
184
205
|
<Input
|
|
185
206
|
id={name}
|
|
186
207
|
name={name}
|
|
187
208
|
type="text"
|
|
188
209
|
inputMode="numeric"
|
|
189
210
|
autoComplete="postal-code"
|
|
211
|
+
size={size}
|
|
212
|
+
// `inside`: the label is the Input's own, drawn in the box and floating
|
|
213
|
+
// up on focus or a value — what every other `Input` in the form does.
|
|
214
|
+
// `above` keeps the stacked `FormLabel`, for callers that never opted in.
|
|
215
|
+
label={insideLabel(label, labelPlacement)}
|
|
190
216
|
placeholder={placeholder}
|
|
191
217
|
value={value}
|
|
192
218
|
error={Boolean(error)}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CepFieldCopy } from "../../../copy";
|
|
2
|
+
import type { InputSize } from "../Input/Input.base";
|
|
2
3
|
/**
|
|
3
4
|
* Types for {@link CepField} — the reusable CEP auto-completer.
|
|
4
5
|
*/
|
|
@@ -73,4 +74,19 @@ export interface CepFieldProps {
|
|
|
73
74
|
name?: string;
|
|
74
75
|
/** Test id for the input. Defaults to `cep-field-<name>`. */
|
|
75
76
|
dataTestId?: string;
|
|
77
|
+
/**
|
|
78
|
+
* A step of the house size scale — the same values, and the same box
|
|
79
|
+
* heights, as {@link Input}'s `size`. Defaults to `md`.
|
|
80
|
+
*/
|
|
81
|
+
size?: InputSize;
|
|
82
|
+
/**
|
|
83
|
+
* Where the label is drawn.
|
|
84
|
+
*
|
|
85
|
+
* - `above` (default): a stacked label over the box, as the field has always
|
|
86
|
+
* rendered. Every existing caller keeps it.
|
|
87
|
+
* - `inside`: the label sits in the box and floats up on focus or a value,
|
|
88
|
+
* exactly as `Input`'s label does. Use it beside `Input`s so the CEP block
|
|
89
|
+
* is the same height as its neighbours.
|
|
90
|
+
*/
|
|
91
|
+
labelPlacement?: 'above' | 'inside';
|
|
76
92
|
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import type { CepFieldCopy } from "../../../../copy";
|
|
4
4
|
import React, { useCallback, useMemo, useRef } from 'react';
|
|
5
5
|
|
|
6
|
+
import type { InputSize } from '../../Input/Input.base';
|
|
6
7
|
import { CepField as BaseCepField, type CepAddress } from '../../CepField';
|
|
7
8
|
import { useFormContext } from '../FormContext';
|
|
8
9
|
|
|
@@ -29,6 +30,10 @@ export interface CepFieldProps {
|
|
|
29
30
|
fills?: CepFills;
|
|
30
31
|
/** Milliseconds to wait after the last keystroke before looking up. */
|
|
31
32
|
debounceMs?: number;
|
|
33
|
+
/** The input's size — the same scale, and box heights, as `Input`. Defaults to `md`. */
|
|
34
|
+
size?: InputSize;
|
|
35
|
+
/** `above` (default) stacks the label over the box; `inside` floats it in the box like `Input`. */
|
|
36
|
+
labelPlacement?: 'above' | 'inside';
|
|
32
37
|
}
|
|
33
38
|
|
|
34
39
|
const PARTS: readonly CepPart[] = ['street', 'neighborhood', 'city', 'state'];
|
|
@@ -107,6 +112,8 @@ export function CepField({
|
|
|
107
112
|
fills,
|
|
108
113
|
debounceMs,
|
|
109
114
|
copy,
|
|
115
|
+
size,
|
|
116
|
+
labelPlacement,
|
|
110
117
|
}: CepFieldProps): React.ReactElement {
|
|
111
118
|
const { values, errors, setFieldValue } = useFormContext();
|
|
112
119
|
const { write, clearOwned } = useOwnedAutofill();
|
|
@@ -142,6 +149,8 @@ export function CepField({
|
|
|
142
149
|
onResolved={handleResolved}
|
|
143
150
|
onNotFound={clearOwned}
|
|
144
151
|
debounceMs={debounceMs}
|
|
152
|
+
size={size}
|
|
153
|
+
labelPlacement={labelPlacement}
|
|
145
154
|
dataTestId={`total-form-field-${name}`}
|
|
146
155
|
/>
|
|
147
156
|
);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/form/total-form/Fields/CepField.tsx"],"sourcesContent":["'use client';\n\nimport type { CepFieldCopy } from \"../../../../copy\";\nimport React, { useCallback, useMemo, useRef } from 'react';\n\nimport { CepField as BaseCepField, type CepAddress } from '../../CepField';\nimport { useFormContext } from '../FormContext';\n\n/** The resolved address parts a form can receive. */\ntype CepPart = 'street' | 'neighborhood' | 'city' | 'state';\n\n/**\n * Which form field receives each resolved part. Omit a part to leave it alone —\n * a form with no bairro column simply doesn't map `neighborhood`.\n */\nexport type CepFills = Partial<Record<CepPart, string>>;\n\n/** Props for the `total-form` {@link CepField}. */\nexport interface CepFieldProps {\n /** The lookup status wording. REQUIRED — no default copy. */\n copy: CepFieldCopy;\n /** Field name; also the key into the form's values/errors. */\n name: string;\n /** Optional visible label. Defaults to `CEP`. */\n label?: string;\n /** Resolve a complete CEP. Omit for a masked input with no lookup. */\n lookup?: (cep: string) => Promise<CepAddress | null>;\n /** Maps each resolved address part onto a field in THIS form. */\n fills?: CepFills;\n /** Milliseconds to wait after the last keystroke before looking up. */\n debounceMs?: number;\n}\n\nconst PARTS: readonly CepPart[] = ['street', 'neighborhood', 'city', 'state'];\n\n/** Ownership-aware writes into sibling form fields. */\ninterface OwnedAutofill {\n /** Write one field, where `''` means \"no value for this part\". */\n write: (field: string, value: string) => void;\n /** Blank every field this hook still owns. */\n clearOwned: () => void;\n}\n\n/**\n * The honesty rule, in one place: this hook remembers exactly what it wrote,\n * writes only into fields that are empty or still hold its own value, and drops\n * the ownership record when it blanks one. That single invariant is what lets\n * autofill replace itself freely while never touching the user's typing.\n */\nfunction useOwnedAutofill(): OwnedAutofill {\n const { values, setFieldValue } = useFormContext();\n // What we last auto-filled per target field, so a later lookup may replace\n // its own value while a manual edit is preserved.\n const autoFilledRef = useRef<Record<string, string>>({});\n // Read values at call time without making them a dependency of the callback.\n const valuesRef = useRef(values);\n valuesRef.current = values;\n\n const write = useCallback(\n (field: string, value: string) => {\n const current = (valuesRef.current[field] ?? '').trim();\n const ours = autoFilledRef.current[field] === current;\n if (current !== '' && !ours) return;\n setFieldValue(field, value);\n if (value) {\n autoFilledRef.current[field] = value;\n } else {\n delete autoFilledRef.current[field];\n }\n },\n [setFieldValue],\n );\n\n const clearOwned = useCallback(() => {\n // Object.keys snapshots, so `write` may delete entries as we iterate.\n for (const field of Object.keys(autoFilledRef.current)) {\n write(field, '');\n }\n }, [write]);\n\n return useMemo(() => ({ write, clearOwned }), [write, clearOwned]);\n}\n\n/**\n * CEP field bound to {@link useFormContext}: masks and validates the CEP, then\n * writes the resolved logradouro/bairro/cidade/UF straight into the sibling\n * fields named by `fills`.\n *\n * It will not overwrite work the user has done. A target field is only written\n * when it is empty, or when it still holds the exact value THIS component put\n * there — so correcting the CEP re-fills the address, but a hand-edited street\n * survives the next lookup.\n *\n * @example\n * ```tsx\n * <Fields.CepField\n * name=\"postalCode\"\n * lookup={lookupCep}\n * fills={{ street: 'addressLine1', neighborhood: 'neighborhood', city: 'city', state: 'state' }}\n * />\n * ```\n */\nexport function CepField({\n name,\n label,\n lookup,\n fills,\n debounceMs,\n copy,\n}: CepFieldProps): React.ReactElement {\n const { values, errors, setFieldValue } = useFormContext();\n const { write, clearOwned } = useOwnedAutofill();\n\n const handleResolved = useCallback(\n (address: CepAddress) => {\n if (!fills) return;\n // Every mapped part is written on EVERY resolve, absent ones as ''. A\n // \"CEP geral\" (a small município's catch-all) carries no logradouro, so\n // skipping absent parts would leave the previous CEP's street here.\n for (const part of PARTS) {\n const field = fills[part];\n if (field) write(field, address[part] ?? '');\n }\n },\n [fills, write],\n );\n\n const handleChange = useCallback(\n (masked: string) => setFieldValue(name, masked),\n [name, setFieldValue],\n );\n\n return (\n <BaseCepField\n copy={copy}\n name={name}\n label={label}\n value={values[name] ?? ''}\n error={errors[name]}\n onChange={handleChange}\n onLookup={lookup}\n onResolved={handleResolved}\n onNotFound={clearOwned}\n debounceMs={debounceMs}\n dataTestId={`total-form-field-${name}`}\n />\n );\n}\n"],"mappings":";;;;;;;;AAGA,SAAgB,aAAa,SAAS,cAAc;AAkIhD;AApGJ,IAAM,QAA4B,CAAC,UAAU,gBAAgB,QAAQ,OAAO;AAgB5E,SAAS,mBAAkC;AACzC,QAAM,EAAE,QAAQ,cAAc,IAAI,eAAe;AAGjD,QAAM,gBAAgB,OAA+B,CAAC,CAAC;AAEvD,QAAM,YAAY,OAAO,MAAM;AAC/B,YAAU,UAAU;AAEpB,QAAM,QAAQ;AAAA,IACZ,CAAC,OAAe,UAAkB;AAChC,YAAM,WAAW,UAAU,QAAQ,KAAK,KAAK,IAAI,KAAK;AACtD,YAAM,OAAO,cAAc,QAAQ,KAAK,MAAM;AAC9C,UAAI,YAAY,MAAM,CAAC,KAAM;AAC7B,oBAAc,OAAO,KAAK;AAC1B,UAAI,OAAO;AACT,sBAAc,QAAQ,KAAK,IAAI;AAAA,MACjC,OAAO;AACL,eAAO,cAAc,QAAQ,KAAK;AAAA,MACpC;AAAA,IACF;AAAA,IACA,CAAC,aAAa;AAAA,EAChB;AAEA,QAAM,aAAa,YAAY,MAAM;AAEnC,eAAW,SAAS,OAAO,KAAK,cAAc,OAAO,GAAG;AACtD,YAAM,OAAO,EAAE;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,KAAK,CAAC;AAEV,SAAO,QAAQ,OAAO,EAAE,OAAO,WAAW,IAAI,CAAC,OAAO,UAAU,CAAC;AACnE;AAqBO,SAASA,UAAS;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAAsC;AACpC,QAAM,EAAE,QAAQ,QAAQ,cAAc,IAAI,eAAe;AACzD,QAAM,EAAE,OAAO,WAAW,IAAI,iBAAiB;AAE/C,QAAM,iBAAiB;AAAA,IACrB,CAAC,YAAwB;AACvB,UAAI,CAAC,MAAO;AAIZ,iBAAW,QAAQ,OAAO;AACxB,cAAM,QAAQ,MAAM,IAAI;AACxB,YAAI,MAAO,OAAM,OAAO,QAAQ,IAAI,KAAK,EAAE;AAAA,MAC7C;AAAA,IACF;AAAA,IACA,CAAC,OAAO,KAAK;AAAA,EACf;AAEA,QAAM,eAAe;AAAA,IACnB,CAAC,WAAmB,cAAc,MAAM,MAAM;AAAA,IAC9C,CAAC,MAAM,aAAa;AAAA,EACtB;AAEA,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA;AAAA,MACA,OAAO,OAAO,IAAI,KAAK;AAAA,MACvB,OAAO,OAAO,IAAI;AAAA,MAClB,UAAU;AAAA,MACV,UAAU;AAAA,MACV,YAAY;AAAA,MACZ,YAAY;AAAA,MACZ;AAAA,MACA,YAAY,oBAAoB,IAAI;AAAA;AAAA,EACtC;AAEJ;","names":["CepField"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/components/form/CepField/CepField.tsx"],"sourcesContent":["'use client';\n\nimport type { CepFieldCopy } from \"../../../copy\";\nimport React, { useCallback, useEffect, useRef, useState } from 'react';\n\nimport { cepDigits, formatCep, isValidCep } from '@12-apps/forms-core';\n\nimport { FormControl, FormLabel, FormMessage } from '../Form';\n// The outlined entry: CepField renders no variant, and the generic one would\n// ship MUI's FilledInput and Input to every screen with a CEP field (FUT-1054).\nimport { Input } from '../Input/outlined';\nimport type { CepAddress, CepFieldProps, CepLookupStatus } from './CepField.types';\n\n/** Everything {@link useCepLookup} needs, so the hook signature stays flat. */\ninterface LookupOptions {\n digits: string;\n complete: boolean;\n disabled: boolean;\n debounceMs: number;\n onLookup?: (cep: string) => Promise<CepAddress | null>;\n onResolved?: (address: CepAddress) => void;\n onNotFound?: () => void;\n}\n\n/**\n * Debounced, race-safe CEP resolution.\n *\n * Two guards earn their keep here: a monotonic sequence id, bumped\n * synchronously on every new query, so a slow response for a since-edited CEP\n * can never land; and a \"last queried\" marker, so re-rendering (or the caller\n * re-masking the same value) never re-hits the provider.\n */\nfunction useCepLookup({\n digits,\n complete,\n disabled,\n debounceMs,\n onLookup,\n onResolved,\n onNotFound,\n}: LookupOptions): CepLookupStatus {\n const [status, setStatus] = useState<CepLookupStatus>('idle');\n const lastQueriedRef = useRef('');\n const seqRef = useRef(0);\n // Latest callbacks, read at call time — keeps them out of the effect deps so\n // an inline arrow from the caller doesn't re-trigger the lookup every render.\n const lookupRef = useRef(onLookup);\n const resolvedRef = useRef(onResolved);\n const notFoundRef = useRef(onNotFound);\n lookupRef.current = onLookup;\n resolvedRef.current = onResolved;\n notFoundRef.current = onNotFound;\n\n useEffect(() => {\n if (!complete || disabled || !lookupRef.current) {\n // Editing away from a complete CEP drops any stale status and forgets the\n // last query, so re-typing the same CEP resolves again.\n seqRef.current += 1;\n setStatus('idle');\n lastQueriedRef.current = '';\n return;\n }\n if (lastQueriedRef.current === digits) return;\n\n const mySeq = (seqRef.current += 1);\n const handle = setTimeout(() => {\n lastQueriedRef.current = digits;\n setStatus('loading');\n void (async () => {\n // A failing lookup is treated exactly like \"not found\" — the user types\n // the address instead. Never surfaced as a crash. Going through\n // `Promise.resolve().then` rather than calling directly covers the two\n // failure modes an `await … .catch()` misses: a lookup that throws\n // SYNCHRONOUSLY, and one that returns something other than a promise.\n const address = await Promise.resolve()\n .then(() => lookupRef.current?.(digits))\n .catch(() => null);\n if (seqRef.current !== mySeq) return;\n setStatus(address ? 'found' : 'notfound');\n // A miss is reported too, so a caller that filled from a PREVIOUS CEP\n // can undo it rather than leaving that address under this CEP.\n if (address) resolvedRef.current?.(address);\n else notFoundRef.current?.();\n })();\n }, debounceMs);\n\n return () => clearTimeout(handle);\n }, [digits, complete, disabled, debounceMs]);\n\n return status;\n}\n\n/**\n * The lookup outcome as a live region, so screen readers announce it without\n * stealing focus. Silent while idle, and while the caller is showing its own\n * validation error (which would otherwise stack two messages).\n */\nfunction CepStatus({\n status,\n testId,\n suppressed,\n copy,\n}: {\n status: CepLookupStatus;\n testId: string;\n suppressed: boolean;\n copy: CepFieldCopy;\n}): React.ReactElement {\n const visible = !suppressed && status !== 'idle';\n return (\n <span id={`${testId}-status`} role=\"status\" aria-live=\"polite\">\n {visible && (\n <FormMessage error={status === 'notfound'} dataTestId={`${testId}-${status}`}>\n {copy[status as Exclude<CepLookupStatus, 'idle'>]}\n </FormMessage>\n )}\n </span>\n );\n}\n\n/**\n * A CEP input that fills the rest of the address in.\n *\n * The pattern every Brazilian form has used for twenty years: type eight\n * digits, and logradouro/bairro/cidade/UF arrive on their own. This is the\n * reusable half — masking, validation, debouncing, race-safety and status —\n * with the actual lookup injected via `onLookup`, so the same component serves\n * a supplier form, a store-location form or a delivery checkout without\n * knowing anything about providers or auth.\n *\n * Resolution is best-effort by design. A provider outage, an unknown CEP or a\n * timeout leaves the field exactly as usable as a plain text input — the user\n * types the address themselves. Autofill never blocks.\n *\n * @example\n * ```tsx\n * <CepField\n * value={cep}\n * onChange={setCep}\n * onLookup={(c) => fetch(`/api/…/lookup/cep/${c}`).then((r) => r.json()).then((b) => b.data)}\n * onResolved={(a) => { setStreet(a.street ?? ''); setCity(a.city ?? ''); }}\n * />\n * ```\n */\nexport function CepField({\n value,\n onChange,\n onLookup,\n onResolved,\n onNotFound,\n label = 'CEP',\n placeholder = '00000-000',\n error,\n disabled = false,\n debounceMs = 500,\n name = 'postalCode',\n dataTestId,\n copy,\n}: CepFieldProps): React.ReactElement {\n const testId = dataTestId ?? `cep-field-${name}`;\n\n const status = useCepLookup({\n digits: cepDigits(value),\n complete: isValidCep(value),\n disabled,\n debounceMs,\n onLookup,\n onResolved,\n onNotFound,\n });\n\n const handleChange = useCallback(\n (event: React.ChangeEvent<HTMLInputElement>) => onChange(formatCep(event.target.value)),\n [onChange],\n );\n\n return (\n <FormControl fullWidth>\n {label && (\n <FormLabel htmlFor={name} error={Boolean(error)}>\n {label}\n </FormLabel>\n )}\n <Input\n id={name}\n name={name}\n type=\"text\"\n inputMode=\"numeric\"\n autoComplete=\"postal-code\"\n placeholder={placeholder}\n value={value}\n error={Boolean(error)}\n disabled={disabled}\n loading={status === 'loading'}\n onChange={handleChange}\n data-testid={testId}\n aria-describedby={`${testId}-status`}\n aria-busy={status === 'loading'}\n />\n {error && (\n <FormMessage error dataTestId={`${testId}-message`}>\n {error}\n </FormMessage>\n )}\n <CepStatus status={status} testId={testId} suppressed={Boolean(error)} copy={copy} />\n </FormControl>\n );\n}\n\nexport default CepField;\n"],"mappings":"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","names":[]}
|
|
File without changes
|