@openzeppelin/ui-components 3.3.0 → 3.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -38,7 +38,7 @@ let sonner = require("sonner");
38
38
  let next_themes = require("next-themes");
39
39
 
40
40
  //#region src/version.ts
41
- const VERSION = "3.3.0";
41
+ const VERSION = "3.4.0";
42
42
 
43
43
  //#endregion
44
44
  //#region src/components/ui/accordion.tsx
@@ -131,6 +131,50 @@ const TooltipContent = react.forwardRef(({ className, sideOffset = 4, ...props }
131
131
  }) }));
132
132
  TooltipContent.displayName = _radix_ui_react_tooltip.Content.displayName;
133
133
 
134
+ //#endregion
135
+ //#region src/components/ui/address-display/address-avatar.tsx
136
+ /**
137
+ * Allowlist for name-owner-controlled avatar URLs at the render site.
138
+ * Only `https:` and `data:image/*` pass; `http:` (mixed content), `javascript:`,
139
+ * and other schemes are dropped. Does not rely on adapter sanitization.
140
+ */
141
+ function isAllowedAvatarSrc(src) {
142
+ try {
143
+ const url = new URL(src);
144
+ if (url.protocol === "https:") return true;
145
+ if (url.protocol === "data:") return /^data:image\/[a-z0-9.+-]+/i.test(src);
146
+ return false;
147
+ } catch {
148
+ return false;
149
+ }
150
+ }
151
+ /**
152
+ * @internal Avatar image for a resolved name, hiding itself on load error
153
+ * (never a broken-image icon) — the name renders as a complete, avatar-less
154
+ * row. Purely presentational; its only state is the per-instance load-error
155
+ * flag (INV-121).
156
+ *
157
+ * The error state is keyed to the current `src` rather than a bare boolean,
158
+ * so it resets automatically when `src` changes — a virtualized/reused list
159
+ * row that scrolls to a new address re-attempts its avatar instead of staying
160
+ * permanently hidden by a prior URL's failure (INV-59).
161
+ *
162
+ * @param props - {@link AddressAvatarProps}.
163
+ * @returns The avatar `<img>`, or `null` when the load failed for this `src`.
164
+ */
165
+ function AddressAvatar({ src, alt = "", variant = "fill", sizeClassName }) {
166
+ const [failedSrc, setFailedSrc] = react.useState(null);
167
+ if (!isAllowedAvatarSrc(src)) return null;
168
+ if (failedSrc === src) return null;
169
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("img", {
170
+ src,
171
+ alt,
172
+ className: (0, _openzeppelin_ui_utils.cn)("object-cover", sizeClassName ?? (variant === "fill" ? "absolute inset-0 h-full w-full rounded-none" : "h-4 w-4 shrink-0 rounded-full")),
173
+ referrerPolicy: "no-referrer",
174
+ onError: () => setFailedSrc(src)
175
+ });
176
+ }
177
+
134
178
  //#endregion
135
179
  //#region src/components/ui/address-display/context.ts
136
180
  /**
@@ -139,6 +183,14 @@ TooltipContent.displayName = _radix_ui_react_tooltip.Content.displayName;
139
183
  * only components (required by React Fast Refresh).
140
184
  */
141
185
  const AddressLabelContext = (0, react.createContext)(null);
186
+ /**
187
+ * @internal Shared context instance consumed by AddressDisplay,
188
+ * AddressNameProvider, and useAddressName. Sibling of AddressLabelContext
189
+ * (SF-4 INV-122): carries a synchronous, value-only reverse-name resolver.
190
+ * Defaults to `null` — no provider means no resolution and a byte-identical
191
+ * zero-injection render (INV-54).
192
+ */
193
+ const AddressNameContext = (0, react.createContext)(null);
142
194
 
143
195
  //#endregion
144
196
  //#region src/components/ui/address-display/address-display.tsx
@@ -155,16 +207,37 @@ function usePrefersHover() {
155
207
  }, []), () => typeof window !== "undefined" && typeof window.matchMedia !== "undefined" ? window.matchMedia("(hover: hover)").matches : true, () => true);
156
208
  }
157
209
  /**
210
+ * Strip C0/C1 controls, bidi embeds/isolates, and zero-width characters from a
211
+ * verified name at the display seam. ENSIP-15 normalization remains the
212
+ * adapter's job; this only removes invisible directionality/control glyphs so
213
+ * a raw render cannot spoof adjacent UI (homoglyph confusables are out of scope).
214
+ */
215
+ function sanitizeVerifiedNameForDisplay(name) {
216
+ return name.replace(/[\u0000-\u001F\u007F-\u009F\u200B-\u200F\u202A-\u202E\u2060-\u2064\u2066-\u206F\uFEFF]/g, "");
217
+ }
218
+ /**
158
219
  * Displays a blockchain address with optional truncation, copy button,
159
- * explorer link, tooltip, and human-readable label.
220
+ * explorer link, tooltip, human-readable label, and (when an already-resolved
221
+ * record is injected) a forward-verified name + avatar.
160
222
  *
161
- * Labels are resolved in priority order:
223
+ * Labels are resolved in priority order (INV-56):
162
224
  * 1. Explicit `label` prop
163
225
  * 2. `AddressLabelContext` resolver (via `AddressLabelProvider`)
164
- * 3. No label (renders address only, identical to previous behavior)
226
+ * 3. Forward-verified resolved name (`resolvedName` prop, else
227
+ * `AddressNameContext` via `AddressNameProvider`) — a record with
228
+ * `forwardVerified: false` is suppressed to hex (INV-52, LOCKED)
229
+ * 4. No label (renders address only, identical to previous behavior)
230
+ *
231
+ * The component is synchronous and capability-free (INV-121): it never
232
+ * resolves anything itself — it reads injected values. Hex renders on first
233
+ * commit; the name+avatar swap in when the react layer feeds an updated
234
+ * record (progressive enhancement, INV-61). With no `resolvedName` and no
235
+ * `AddressNameProvider`, output is byte-identical to the pre-enhancement
236
+ * component (INV-54, LOCKED).
165
237
  *
166
- * Pass `disableLabel` to suppress context-based resolution (e.g. when the
167
- * surrounding UI already shows a name, such as a contract selector).
238
+ * Pass `disableLabel` to render fully raw it suppresses both the context
239
+ * alias and any injected resolved name (e.g. when the surrounding UI already
240
+ * shows a name, such as a contract selector).
168
241
  *
169
242
  * @example
170
243
  * ```tsx
@@ -191,18 +264,30 @@ function usePrefersHover() {
191
264
  * // Inline variant (no chip background) — useful inside wallet bars
192
265
  * <AddressDisplay address="0x742d35Cc..." variant="inline" showTooltip showCopyButton />
193
266
  *
267
+ * // Reverse-resolved name + avatar via an injected value (react layer owns the async)
268
+ * <AddressDisplay address="0x742d35Cc..." resolvedName={record} showCopyButton />
269
+ *
270
+ * // Or subtree-wide via context (rows stay plain <AddressDisplay/>)
271
+ * <AddressNameProvider resolveAddressName={readFromResolutionCache}>
272
+ * <AddressDisplay address="0x742d35Cc..." />
273
+ * </AddressNameProvider>
274
+ *
194
275
  * // Truncate only when an alias/label is present (full address when unlabeled)
195
276
  * <AddressDisplay address="G..." truncateWhenLabeled />
196
277
  * ```
197
278
  */
198
- function AddressDisplay({ address, truncate: truncateProp, truncateWhenLabeled = false, untruncateOnHover = false, startChars = 6, endChars = 4, showCopyButton = false, showCopyButtonOnHover = false, explorerUrl, label: labelProp, onLabelEdit: onLabelEditProp, networkId, disableLabel = false, showTooltip = false, variant = "chip", className, onPointerEnter, onPointerLeave, onMouseEnter, onMouseLeave, onClick, ...props }) {
279
+ function AddressDisplay({ address, truncate: truncateProp, truncateWhenLabeled = false, untruncateOnHover = false, startChars = 6, endChars = 4, showCopyButton = false, showCopyButtonOnHover = false, explorerUrl, label: labelProp, onLabelEdit: onLabelEditProp, networkId, disableLabel = false, showTooltip = false, variant = "chip", avatar, avatarVariant = "fill", resolvedName, className, onPointerEnter, onPointerLeave, onMouseEnter, onMouseLeave, onClick, ...props }) {
199
280
  const [copied, setCopied] = react.useState(false);
200
281
  const [isHovered, setIsHovered] = react.useState(false);
201
282
  const copyTimeoutRef = react.useRef(null);
202
283
  const prefersHover = usePrefersHover();
203
284
  const resolver = react.useContext(AddressLabelContext);
285
+ const nameResolver = react.useContext(AddressNameContext);
204
286
  const resolvedLabel = disableLabel ? void 0 : labelProp ?? resolver?.resolveLabel(address, networkId);
205
- const effectiveTruncate = truncateProp !== void 0 ? truncateProp : truncateWhenLabeled ? Boolean(resolvedLabel) : true;
287
+ const record = disableLabel ? void 0 : resolvedName ?? nameResolver?.resolveAddressName(address, networkId);
288
+ const ensName = record?.forwardVerified === true ? sanitizeVerifiedNameForDisplay(record.name) : void 0;
289
+ const effectiveLabel = resolvedLabel ?? ensName;
290
+ const effectiveTruncate = truncateProp !== void 0 ? truncateProp : truncateWhenLabeled ? Boolean(effectiveLabel) : true;
206
291
  const contextEditHandler = react.useCallback(() => {
207
292
  resolver?.onEditLabel?.(address, networkId);
208
293
  }, [
@@ -276,6 +361,17 @@ function AddressDisplay({ address, truncate: truncateProp, truncateWhenLabeled =
276
361
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(lucide_react.Pencil, { className: "h-3.5 w-3.5" })
277
362
  })
278
363
  ] });
364
+ const ensAvatarUrl = ensName !== void 0 && resolvedLabel === void 0 ? record?.avatarUrl : void 0;
365
+ const effectiveAvatar = avatar ?? (ensAvatarUrl ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddressAvatar, {
366
+ src: ensAvatarUrl,
367
+ variant: avatarVariant
368
+ }) : void 0);
369
+ const isFillAvatar = avatarVariant === "fill" && Boolean(effectiveAvatar);
370
+ const fillAvatarChipClassName = isFillAvatar && isChip ? "overflow-hidden" : void 0;
371
+ const avatarSlot = effectiveAvatar ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
372
+ className: (0, _openzeppelin_ui_utils.cn)("flex shrink-0", isFillAvatar ? (0, _openzeppelin_ui_utils.cn)("relative w-9 self-stretch overflow-hidden mr-2", isChip && "-my-1 -ml-2") : "mr-1.5 items-center"),
373
+ children: effectiveAvatar
374
+ }) : null;
279
375
  const shouldShowTooltip = showTooltip && effectiveTruncate;
280
376
  const wrapWithTooltip = (content) => {
281
377
  if (!shouldShowTooltip) return content;
@@ -290,33 +386,43 @@ function AddressDisplay({ address, truncate: truncateProp, truncateWhenLabeled =
290
386
  })] })
291
387
  });
292
388
  };
293
- if (resolvedLabel) return wrapWithTooltip(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
294
- className: (0, _openzeppelin_ui_utils.cn)("group inline-flex max-w-full min-w-0 flex-col", isChip && "rounded-md bg-slate-100 px-2 py-1", "text-xs text-slate-700", expandInteractionClassName, className),
295
- onPointerEnter: handlePointerEnter,
296
- onPointerLeave: handlePointerLeave,
297
- onClick: handleUntruncateClick,
298
- ...props,
299
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
389
+ if (effectiveLabel) {
390
+ const labelColumn = /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
300
391
  className: "truncate font-sans font-medium text-slate-900 leading-snug",
301
- children: resolvedLabel
392
+ children: effectiveLabel
302
393
  }), /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
303
394
  className: "flex min-w-0 items-center font-mono text-[10px] text-slate-400 leading-snug",
304
395
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
305
396
  className: addressTextClassName,
306
397
  children: displayAddress
307
398
  }), actionButtons]
308
- })]
309
- }));
399
+ })] });
400
+ return wrapWithTooltip(/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
401
+ className: (0, _openzeppelin_ui_utils.cn)("group inline-flex max-w-full min-w-0", effectiveAvatar ? "items-center" : "flex-col", isChip && "rounded-md bg-slate-100 px-2 py-1", fillAvatarChipClassName, "text-xs text-slate-700", expandInteractionClassName, className),
402
+ onPointerEnter: handlePointerEnter,
403
+ onPointerLeave: handlePointerLeave,
404
+ onClick: handleUntruncateClick,
405
+ ...props,
406
+ children: effectiveAvatar ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [avatarSlot, /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
407
+ className: "inline-flex min-w-0 flex-col",
408
+ children: labelColumn
409
+ })] }) : labelColumn
410
+ }));
411
+ }
310
412
  return wrapWithTooltip(/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
311
- className: (0, _openzeppelin_ui_utils.cn)("group inline-flex max-w-full min-w-0 items-center", isChip && "rounded-md bg-slate-100 px-2 py-1", "text-xs font-mono text-slate-700", expandInteractionClassName, className),
413
+ className: (0, _openzeppelin_ui_utils.cn)("group inline-flex max-w-full min-w-0 items-center", isChip && "rounded-md bg-slate-100 px-2 py-1", fillAvatarChipClassName, "text-xs font-mono text-slate-700", expandInteractionClassName, className),
312
414
  onPointerEnter: handlePointerEnter,
313
415
  onPointerLeave: handlePointerLeave,
314
416
  onClick: handleUntruncateClick,
315
417
  ...props,
316
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
317
- className: addressTextClassName,
318
- children: displayAddress
319
- }), actionButtons]
418
+ children: [
419
+ avatarSlot,
420
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
421
+ className: addressTextClassName,
422
+ children: displayAddress
423
+ }),
424
+ actionButtons
425
+ ]
320
426
  }));
321
427
  }
322
428
 
@@ -414,6 +520,79 @@ function useAddressLabel(address, networkId) {
414
520
  };
415
521
  }
416
522
 
523
+ //#endregion
524
+ //#region src/components/ui/address-display/address-name-context.tsx
525
+ /**
526
+ * Address Name Context
527
+ *
528
+ * Provides a React context for surfacing already-resolved reverse name
529
+ * records (`ResolvedName`) for blockchain addresses. When an
530
+ * `AddressNameProvider` is mounted, every `AddressDisplay` in the subtree
531
+ * reads its record synchronously — no call-site changes — and renders the
532
+ * name/avatar only when the record is forward-verified (INV-52).
533
+ *
534
+ * Structural mirror of `AddressLabelProvider` (INV-122): synchronous,
535
+ * value-only, no-op by default. The resolver implementation lives in the
536
+ * react/renderer layer, which bridges async resolution state into this
537
+ * synchronous read; this file stays capability-free (INV-121).
538
+ *
539
+ * @example
540
+ * ```tsx
541
+ * import { AddressNameProvider } from '@openzeppelin/ui-components';
542
+ *
543
+ * <AddressNameProvider resolveAddressName={readFromResolutionCache}>
544
+ * <TxHistory /> {// rows are plain <AddressDisplay/> — verified names fill in }
545
+ * </AddressNameProvider>
546
+ * ```
547
+ */
548
+ /**
549
+ * Provides reverse-name resolution to all `AddressDisplay` instances in the
550
+ * subtree. `resolveAddressName` MUST be synchronous — it is called during
551
+ * render (INV-122). A resolver that returns `undefined` for an address is
552
+ * behaviorally identical to mounting no provider (INV-54).
553
+ *
554
+ * @param props - Resolver function and children
555
+ */
556
+ function AddressNameProvider({ children, resolveAddressName }) {
557
+ const value = react.useMemo(() => ({ resolveAddressName }), [resolveAddressName]);
558
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddressNameContext.Provider, {
559
+ value,
560
+ children
561
+ });
562
+ }
563
+
564
+ //#endregion
565
+ //#region src/components/ui/address-display/use-address-name.ts
566
+ /**
567
+ * Convenience hook for reading a reverse name record from the nearest
568
+ * `AddressNameProvider`.
569
+ *
570
+ * Kept in its own file so that `address-name-context.tsx` exports only
571
+ * components (required by React Fast Refresh) — mirrors `use-address-label.ts`.
572
+ */
573
+ /**
574
+ * Reads the `AddressNameContext` for one address, synchronously (INV-122).
575
+ * Returns `{ record: undefined }` when no provider is mounted — never throws,
576
+ * never suspends. Note the record is returned verbatim: callers rendering a
577
+ * name from it must apply the same `forwardVerified === true` gate that
578
+ * `AddressDisplay` applies (INV-52).
579
+ *
580
+ * @param address - The blockchain address to reverse-resolve
581
+ * @param networkId - Optional network identifier scoping the lookup
582
+ * @returns The current best-known record for the address
583
+ *
584
+ * @example
585
+ * ```tsx
586
+ * function EnsBadge({ address }: { address: string }) {
587
+ * const { record } = useAddressName(address);
588
+ * return record?.forwardVerified ? <Badge>{record.name}</Badge> : null;
589
+ * }
590
+ * ```
591
+ */
592
+ function useAddressName(address, networkId) {
593
+ return { record: react.useContext(AddressNameContext)?.resolveAddressName(address, networkId) };
594
+ }
595
+
417
596
  //#endregion
418
597
  //#region src/components/ui/alert.tsx
419
598
  const alertVariants = (0, class_variance_authority.cva)("relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", {
@@ -2551,6 +2730,55 @@ function WizardLayout(props) {
2551
2730
  });
2552
2731
  }
2553
2732
 
2733
+ //#endregion
2734
+ //#region src/components/fields/address-suggestion/AddressSuggestionList.tsx
2735
+ /**
2736
+ * Presentational ARIA listbox for address suggestions (SF-3, D9).
2737
+ *
2738
+ * Extracted from `AddressField`'s inline dropdown markup into a reusable
2739
+ * presentational listbox so the base `AddressField` renders a consistent
2740
+ * suggestion structure (INV-72). Purely presentational and capability-free: it
2741
+ * owns no state and no resolution — the consuming field supplies the resolved
2742
+ * list, the highlighted index, and the select / hover callbacks.
2743
+ *
2744
+ * @see INV-72 (byte-identical suggestion structure across both fields)
2745
+ * @see INV-94 (`role="listbox"` / `role="option"`, `aria-selected`, `onMouseDown`-preventDefault select)
2746
+ */
2747
+ /**
2748
+ * Render the suggestion dropdown. Returns `null` when there are no suggestions
2749
+ * (the consuming field also gates on `hasSuggestions`, so this is a safety net).
2750
+ *
2751
+ * @param props - {@link AddressSuggestionListProps}.
2752
+ */
2753
+ function AddressSuggestionList({ id, suggestions, highlightedIndex, onSelect, onHighlight }) {
2754
+ if (suggestions.length === 0) return null;
2755
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2756
+ id: `${id}-suggestions`,
2757
+ className: (0, _openzeppelin_ui_utils.cn)("absolute z-50 mt-1 w-full rounded-md border border-border bg-popover shadow-md", "max-h-48 overflow-auto"),
2758
+ role: "listbox",
2759
+ children: suggestions.map((s, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2760
+ id: `${id}-suggestion-${i}`,
2761
+ type: "button",
2762
+ role: "option",
2763
+ "aria-selected": i === highlightedIndex,
2764
+ className: (0, _openzeppelin_ui_utils.cn)("flex w-full flex-col px-3 py-2 text-left text-sm", "hover:bg-selected/10", i === highlightedIndex && "bg-selected/10"),
2765
+ onMouseDown: (e) => {
2766
+ e.preventDefault();
2767
+ onSelect(s);
2768
+ },
2769
+ onMouseEnter: () => onHighlight(i),
2770
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2771
+ className: "font-medium",
2772
+ children: s.label
2773
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2774
+ className: "truncate font-mono text-xs text-muted-foreground",
2775
+ children: s.value
2776
+ })]
2777
+ }, `${s.value}-${s.description ?? i}`))
2778
+ });
2779
+ }
2780
+ AddressSuggestionList.displayName = "AddressSuggestionList";
2781
+
2554
2782
  //#endregion
2555
2783
  //#region src/components/fields/address-suggestion/context.ts
2556
2784
  /**
@@ -2560,6 +2788,372 @@ function WizardLayout(props) {
2560
2788
  */
2561
2789
  const AddressSuggestionContext = (0, react.createContext)(null);
2562
2790
 
2791
+ //#endregion
2792
+ //#region src/components/fields/address-suggestion/useAddressSuggestionField.ts
2793
+ /**
2794
+ * Headless suggestion-dropdown behavior for address fields.
2795
+ *
2796
+ * Extracted from `AddressField`'s inline dropdown logic (SF-3, D9) into one
2797
+ * reusable headless hook the base `AddressField` consumes — no divergence, no
2798
+ * regression (INV-72). It owns:
2799
+ * - `AddressSuggestionContext` resolution (explicit `suggestions` prop overrides),
2800
+ * - the `MAX_SUGGESTIONS` (5) cap on context-resolved lists,
2801
+ * - the 200 ms suggestion debounce,
2802
+ * - keyboard highlight state with wrapping ArrowUp / ArrowDown navigation,
2803
+ * - click-outside-to-close.
2804
+ *
2805
+ * It is capability-free and chain-agnostic — nothing about ENS or any chain
2806
+ * leaks into `@openzeppelin/ui-components`. The consuming field owns the RHF
2807
+ * value write on selection and the Enter / Escape input handlers; this hook
2808
+ * exposes the state and primitives they need.
2809
+ *
2810
+ * Distinct from the pre-existing convenience hook `useAddressSuggestions(query,
2811
+ * networkId)`, which resolves a raw suggestion list from context with no
2812
+ * debounce / cap / keyboard state — that hook is unchanged and still exported.
2813
+ *
2814
+ * @see INV-72 (suggestion-dropdown behavior preserved with zero regression; SF-5 depends)
2815
+ * @see INV-94 (legacy a11y + suggestion keyboard contract preserved verbatim)
2816
+ */
2817
+ /** Suggestion debounce window (ms). Matches the legacy `AddressField`. */
2818
+ const DEBOUNCE_MS = 200;
2819
+ /** Cap on context-resolved suggestions. Explicit arrays are rendered as-is. */
2820
+ const MAX_SUGGESTIONS = 5;
2821
+ /**
2822
+ * Headless address-suggestion dropdown behavior. See the module docstring for
2823
+ * the full behavior contract.
2824
+ *
2825
+ * @param args - {@link UseAddressSuggestionFieldArgs}.
2826
+ * @returns {@link UseAddressSuggestionFieldResult}.
2827
+ */
2828
+ function useAddressSuggestionField({ inputValue, suggestions: suggestionsProp, networkId }) {
2829
+ const contextResolver = (0, react.useContext)(AddressSuggestionContext);
2830
+ const containerRef = (0, react.useRef)(null);
2831
+ const [debouncedQuery, setDebouncedQuery] = (0, react.useState)("");
2832
+ const [showSuggestions, setShowSuggestions] = (0, react.useState)(false);
2833
+ const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(-1);
2834
+ (0, react.useEffect)(() => {
2835
+ if (!inputValue.trim()) {
2836
+ setDebouncedQuery("");
2837
+ return;
2838
+ }
2839
+ const timer = setTimeout(() => setDebouncedQuery(inputValue), DEBOUNCE_MS);
2840
+ return () => clearTimeout(timer);
2841
+ }, [inputValue]);
2842
+ const suggestionsDisabled = suggestionsProp === false;
2843
+ const resolvedSuggestions = (0, react.useMemo)(() => {
2844
+ if (suggestionsDisabled) return [];
2845
+ if (Array.isArray(suggestionsProp)) return suggestionsProp;
2846
+ if (!contextResolver || !debouncedQuery.trim()) return [];
2847
+ return contextResolver.resolveSuggestions(debouncedQuery, networkId).slice(0, MAX_SUGGESTIONS);
2848
+ }, [
2849
+ suggestionsDisabled,
2850
+ suggestionsProp,
2851
+ contextResolver,
2852
+ debouncedQuery,
2853
+ networkId
2854
+ ]);
2855
+ const hasSuggestions = showSuggestions && resolvedSuggestions.length > 0;
2856
+ (0, react.useEffect)(() => {
2857
+ let active = true;
2858
+ const handleClickOutside = (e) => {
2859
+ if (active && containerRef.current && !containerRef.current.contains(e.target)) setShowSuggestions(false);
2860
+ };
2861
+ document.addEventListener("mousedown", handleClickOutside);
2862
+ return () => {
2863
+ active = false;
2864
+ document.removeEventListener("mousedown", handleClickOutside);
2865
+ };
2866
+ }, []);
2867
+ const onContainerKeyDown = (0, react.useCallback)((e) => {
2868
+ if (!hasSuggestions) return;
2869
+ if (e.key === "ArrowDown") {
2870
+ e.preventDefault();
2871
+ setHighlightedIndex((prev) => prev < resolvedSuggestions.length - 1 ? prev + 1 : 0);
2872
+ } else if (e.key === "ArrowUp") {
2873
+ e.preventDefault();
2874
+ setHighlightedIndex((prev) => prev > 0 ? prev - 1 : resolvedSuggestions.length - 1);
2875
+ }
2876
+ }, [hasSuggestions, resolvedSuggestions.length]);
2877
+ const onInputChange = (0, react.useCallback)((value) => {
2878
+ setShowSuggestions(value.length > 0);
2879
+ setHighlightedIndex(-1);
2880
+ }, []);
2881
+ return {
2882
+ containerRef,
2883
+ resolvedSuggestions,
2884
+ hasSuggestions,
2885
+ highlightedIndex,
2886
+ suggestionsDisabled,
2887
+ setHighlightedIndex,
2888
+ closeSuggestions: (0, react.useCallback)(() => {
2889
+ setShowSuggestions(false);
2890
+ setHighlightedIndex(-1);
2891
+ }, []),
2892
+ onInputChange,
2893
+ onContainerKeyDown
2894
+ };
2895
+ }
2896
+
2897
+ //#endregion
2898
+ //#region src/components/fields/name-resolution/useInjectedNameResolution.ts
2899
+ /**
2900
+ * Injected name-resolution machine (SF-3).
2901
+ *
2902
+ * A thin, capability-free async machine that `AddressField` consumes: given a
2903
+ * typed input and the injected `resolveName`, it debounces, dispatches, tracks
2904
+ * status, and applies the out-of-order name-drop guard (INV-117). It is the
2905
+ * component-boundary analogue of SF-2's `useResolveName`, with all hard
2906
+ * mechanics (cache, dedupe, retries, UNSUPPORTED synthesis) deliberately left
2907
+ * behind the injected function — this hook does ONLY debounce + status
2908
+ * derivation + the funds guards.
2909
+ *
2910
+ * The machine holds no resolved-hex cache of its own (INV-85): its single
2911
+ * `settled` record is valid only while it matches the current debounced input,
2912
+ * the current resolver function identity, and the current retry attempt — any
2913
+ * mismatch derives `loading`, never a stale `resolved`.
2914
+ *
2915
+ * Resolver-identity-churn backstop (INV-123/124/125). The dispatch effect keys
2916
+ * on the injected `resolveName` function identity, so an unstable (non-memoized)
2917
+ * integrator resolver re-fires it on every render. To keep that from becoming an
2918
+ * unbounded RPC loop (rate-ban + cost + funds-path DoS), the machine caps
2919
+ * dispatches per resolution intent — the pair (normalized debounced input × retry
2920
+ * attempt) — at {@link MAX_DISPATCHES_PER_INTENT}, regardless of render count and
2921
+ * regardless of how many times `resolveName` identity changes under that intent.
2922
+ * The budget resets only on intent change (a new typed name, or `retry()`) or when
2923
+ * the field leaves the active name-resolution path (clear / disable). It does
2924
+ * *not* reset on `settled.source` mismatch: that would let two settling sources
2925
+ * flap and refill the counter (settling churn), silently defeating INV-123. A
2926
+ * genuine network swap still re-dispatches within the shared intent budget
2927
+ * (INV-119 / acceptance #5) because a single identity change costs one unit.
2928
+ * Clear→retype of the same name gets a fresh budget via the leave-path reset, so
2929
+ * it cannot accumulate toward the cap across cycles and then strand a later swap.
2930
+ * A churning identity that exhausts the budget emits a one-shot, development-only
2931
+ * warning (INV-124) and degrades to the safe gated state (bounded RPC, RHF value
2932
+ * `''`, submit gated, never a wrong hex, never a throw — INV-125). This backstop
2933
+ * is entirely internal: no signature, param, or return-type change; `AddressField`
2934
+ * consumes the machine unchanged.
2935
+ */
2936
+ /**
2937
+ * Default debounce window (ms) for typed names — mirrors SF-2's
2938
+ * `forwardDebounceMs` default so the field and the bare hook feel identical.
2939
+ */
2940
+ const NAME_RESOLUTION_DEBOUNCE_MS = 300;
2941
+ /**
2942
+ * Cap on resolver dispatches per resolution intent — the pair (normalized
2943
+ * debounced input × retry attempt) — regardless of how many times the field
2944
+ * re-renders or how many times `resolveName` identity changes under that intent
2945
+ * (INV-123). Chosen to sit comfortably above the dispatches a single displayed
2946
+ * input legitimately incurs (1 initial call + any human-initiated genuine network
2947
+ * swaps of that same name, each ≤1 unit — INV-119) and far below per-render
2948
+ * identity churn (hundreds/sec): a user does not switch networks eight times
2949
+ * while one unedited name sits in the field, whereas churn blows past eight
2950
+ * within milliseconds. The budget resets when the intent changes or when the
2951
+ * field leaves the name-resolution path (clear / disable) — never on
2952
+ * `settled.source` mismatch, so settling-source oscillation cannot refill it.
2953
+ */
2954
+ const MAX_DISPATCHES_PER_INTENT = 8;
2955
+ /**
2956
+ * Debounce + dispatch + status derivation over an injected `resolveName`.
2957
+ *
2958
+ * Guarantees:
2959
+ * - INV-83: no call unless `enabled` (name-candidate + injected method), and
2960
+ * never for a debounced copy that lags the current normalized input.
2961
+ * - INV-117: a settled result whose requested name ≠ the current debounced
2962
+ * input is discarded (last-write-wins at the component boundary).
2963
+ * - INV-87: a rejecting resolver (contract violation) is mapped to a typed
2964
+ * `ADAPTER_ERROR` — nothing throws into the render tree.
2965
+ * - INV-123: resolver dispatches are bounded per resolution intent (normalized
2966
+ * debounced input × retry attempt) at {@link MAX_DISPATCHES_PER_INTENT},
2967
+ * regardless of render count or resolver-identity oscillation; the budget
2968
+ * resets on intent change or when leaving the name-resolution path — never on
2969
+ * `settled.source` mismatch; `retry()` never consumes an existing budget.
2970
+ * - INV-124: an identity-churning resolver that exhausts the budget triggers a
2971
+ * one-shot, development-only `logger.warn` (silent in production).
2972
+ * - INV-125: under sustained churn the field stays in the safe gated state — the
2973
+ * perpetually-changing identity keeps failing the `settled.source` check, so
2974
+ * the machine derives `loading`, never `resolved`; combined with INV-87 it
2975
+ * never throws and never writes a hex.
2976
+ * - No `setState` after unmount: every dispatch is cancelled on cleanup.
2977
+ *
2978
+ * @param params - {@link UseInjectedNameResolutionParams}
2979
+ * @returns The current {@link InjectedNameResolutionResult}.
2980
+ */
2981
+ function useInjectedNameResolution({ input, enabled, resolveName, debounceMs = NAME_RESOLUTION_DEBOUNCE_MS }) {
2982
+ const normalized = input.trim().toLowerCase();
2983
+ const [debounced, setDebounced] = (0, react.useState)(normalized);
2984
+ const seededRef = (0, react.useRef)(false);
2985
+ (0, react.useEffect)(() => {
2986
+ if (!seededRef.current) {
2987
+ seededRef.current = true;
2988
+ return;
2989
+ }
2990
+ if (debounceMs <= 0) {
2991
+ setDebounced(normalized);
2992
+ return;
2993
+ }
2994
+ const timer = setTimeout(() => setDebounced(normalized), debounceMs);
2995
+ return () => clearTimeout(timer);
2996
+ }, [normalized, debounceMs]);
2997
+ const [attempt, setAttempt] = (0, react.useState)(0);
2998
+ const [settled, setSettled] = (0, react.useState)(null);
2999
+ const currentTargetRef = (0, react.useRef)("");
3000
+ currentTargetRef.current = enabled && resolveName ? debounced : "";
3001
+ const intentKeyRef = (0, react.useRef)("");
3002
+ const dispatchCountRef = (0, react.useRef)(0);
3003
+ const churnWarnedRef = (0, react.useRef)(false);
3004
+ const warnOnceOnChurn = (0, react.useCallback)(() => {
3005
+ if (process.env.NODE_ENV === "production" || churnWarnedRef.current) return;
3006
+ churnWarnedRef.current = true;
3007
+ _openzeppelin_ui_utils.logger.warn("AddressField", "The injected name resolver changes identity on every render, so inline name resolution was bounded to prevent an unbounded RPC loop. Memoize the resolver (e.g. wrap it in useMemo, or use useRuntimeNameResolver from @openzeppelin/ui-react) so its function identity is stable across renders. See the ens-address-input integration guide.");
3008
+ }, []);
3009
+ (0, react.useEffect)(() => {
3010
+ if (!enabled || !resolveName || debounced === "") {
3011
+ intentKeyRef.current = "";
3012
+ dispatchCountRef.current = 0;
3013
+ return;
3014
+ }
3015
+ if (debounced !== normalized) return;
3016
+ const intentKey = `${debounced}\u0000${attempt}`;
3017
+ if (intentKeyRef.current !== intentKey) {
3018
+ intentKeyRef.current = intentKey;
3019
+ dispatchCountRef.current = 0;
3020
+ }
3021
+ if (dispatchCountRef.current >= MAX_DISPATCHES_PER_INTENT) {
3022
+ warnOnceOnChurn();
3023
+ return;
3024
+ }
3025
+ dispatchCountRef.current += 1;
3026
+ let cancelled = false;
3027
+ const requestedName = debounced;
3028
+ const requestedAttempt = attempt;
3029
+ const source = resolveName;
3030
+ const settle = (result) => {
3031
+ if (cancelled) return;
3032
+ if (requestedName !== currentTargetRef.current) return;
3033
+ setSettled({
3034
+ name: requestedName,
3035
+ result,
3036
+ source,
3037
+ attempt: requestedAttempt
3038
+ });
3039
+ };
3040
+ source(requestedName).then(settle, (cause) => {
3041
+ settle({
3042
+ ok: false,
3043
+ error: {
3044
+ code: "ADAPTER_ERROR",
3045
+ message: cause instanceof Error ? cause.message : String(cause),
3046
+ cause
3047
+ }
3048
+ });
3049
+ });
3050
+ return () => {
3051
+ cancelled = true;
3052
+ };
3053
+ }, [
3054
+ enabled,
3055
+ resolveName,
3056
+ debounced,
3057
+ normalized,
3058
+ attempt,
3059
+ warnOnceOnChurn
3060
+ ]);
3061
+ const retry = (0, react.useCallback)(() => {
3062
+ setAttempt((current) => current + 1);
3063
+ }, []);
3064
+ if (!enabled || !resolveName || normalized === "") return { status: "idle" };
3065
+ if (debounced !== normalized) return {
3066
+ status: "debouncing",
3067
+ name: normalized
3068
+ };
3069
+ if (settled !== null && settled.name === debounced && settled.source === resolveName && settled.attempt === attempt) {
3070
+ if (settled.result.ok) return {
3071
+ status: "resolved",
3072
+ name: settled.name,
3073
+ data: settled.result.value
3074
+ };
3075
+ return {
3076
+ status: "error",
3077
+ name: settled.name,
3078
+ error: settled.result.error,
3079
+ retry
3080
+ };
3081
+ }
3082
+ return {
3083
+ status: "loading",
3084
+ name: normalized
3085
+ };
3086
+ }
3087
+
3088
+ //#endregion
3089
+ //#region src/components/fields/name-resolution/context.ts
3090
+ /**
3091
+ * Process-global key for the context object. Uses `Symbol.for` so all duplicated
3092
+ * module instances share ONE React context — the same cross-bundle-singleton
3093
+ * pattern as `@openzeppelin/ui-react`'s `NameResolutionContext` /
3094
+ * `WalletStateContext` (bundler pre-bundling / npm-installed consumers).
3095
+ */
3096
+ const CONTEXT_KEY = Symbol.for("@openzeppelin/ui-components/NameResolverContext");
3097
+ function getOrCreateSharedContext() {
3098
+ const globalScope = globalThis;
3099
+ if (!globalScope[CONTEXT_KEY]) globalScope[CONTEXT_KEY] = (0, react.createContext)(null);
3100
+ return globalScope[CONTEXT_KEY];
3101
+ }
3102
+ /**
3103
+ * @internal Shared context instance consumed by both AddressField and
3104
+ * NameResolverProvider (INV-118). Kept in its own file so component files
3105
+ * export only components (required by React Fast Refresh).
3106
+ *
3107
+ * `null` means no provider is mounted — every ENS branch in `AddressField` is
3108
+ * then dead code and the field is byte-identical to its pre-ENS behavior
3109
+ * (INV-82).
3110
+ */
3111
+ const NameResolverContext = getOrCreateSharedContext();
3112
+
3113
+ //#endregion
3114
+ //#region src/components/fields/name-resolution/useNameResolver.ts
3115
+ /**
3116
+ * Read the injected resolver context. Returns `null` when no
3117
+ * `NameResolverProvider` is mounted — the consuming field must then treat
3118
+ * every ENS branch as dead code (INV-82).
3119
+ *
3120
+ * @returns The extended context value, or `null` when no provider is mounted.
3121
+ */
3122
+ function useNameResolver() {
3123
+ return (0, react.useContext)(NameResolverContext);
3124
+ }
3125
+
3126
+ //#endregion
3127
+ //#region src/components/fields/name-resolution/useResolvingAnnouncerCopy.ts
3128
+ /** Default escalation threshold for long-latency resolves (INV-129/130). */
3129
+ const RESOLVING_ANNOUNCER_ESCALATE_AFTER_MS = 3e3;
3130
+ const PHASE_1_COPY = "Resolving…";
3131
+ const PHASE_2_COPY = "Still resolving…";
3132
+ /**
3133
+ * Returns the announcer string for debouncing/loading arms.
3134
+ * Phase 1: "Resolving…" (SF-3 default, unchanged for first N ms).
3135
+ * Phase 2: "Still resolving…" after threshold — reduces frozen perception
3136
+ * during CCIP-scale latency without naming gateway/CCIP/v2.
3137
+ */
3138
+ function useResolvingAnnouncerCopy({ isPending, escalateAfterMs = RESOLVING_ANNOUNCER_ESCALATE_AFTER_MS }) {
3139
+ const [escalated, setEscalated] = (0, react.useState)(false);
3140
+ (0, react.useEffect)(() => {
3141
+ if (!isPending) {
3142
+ setEscalated(false);
3143
+ return;
3144
+ }
3145
+ setEscalated(false);
3146
+ const timeoutId = window.setTimeout(() => {
3147
+ setEscalated(true);
3148
+ }, escalateAfterMs);
3149
+ return () => {
3150
+ window.clearTimeout(timeoutId);
3151
+ };
3152
+ }, [isPending, escalateAfterMs]);
3153
+ if (!isPending) return PHASE_1_COPY;
3154
+ return escalated ? PHASE_2_COPY : PHASE_1_COPY;
3155
+ }
3156
+
2563
3157
  //#endregion
2564
3158
  //#region src/components/fields/utils/accessibility.ts
2565
3159
  /**
@@ -2751,8 +3345,26 @@ function getWidthClasses(width) {
2751
3345
 
2752
3346
  //#endregion
2753
3347
  //#region src/components/fields/AddressField.tsx
2754
- const DEBOUNCE_MS = 200;
2755
- const MAX_SUGGESTIONS = 5;
3348
+ /**
3349
+ * Non-user-facing gate string returned by the sync validator while a typed name
3350
+ * is resolving, so `formState.isValid` is `false` even for an optional field
3351
+ * (INV-84). The visible message comes from the aria-live region, never this string.
3352
+ */
3353
+ const NAME_RESOLUTION_PENDING_GATE = "__ens_name_resolution_pending__";
3354
+ /** Codes for which a `retry()` affordance is offered — transient only (INV-90). */
3355
+ const TRANSIENT_ERROR_CODES = new Set([
3356
+ "RESOLUTION_TIMEOUT",
3357
+ "EXTERNAL_GATEWAY_ERROR",
3358
+ "ADAPTER_ERROR"
3359
+ ]);
3360
+ /**
3361
+ * Normalize a typed name for the resolved-write match guard: trim then lowercase,
3362
+ * mirroring the SF-2 engine (whose echoed `result.name` is already normalized).
3363
+ * Comparing the raw typed value would spuriously fail on case/whitespace (INV-79).
3364
+ */
3365
+ function normalizeResolvedName(value) {
3366
+ return value.trim().toLowerCase();
3367
+ }
2756
3368
  /**
2757
3369
  * Address input field component specifically designed for blockchain addresses via React Hook Form integration.
2758
3370
  *
@@ -2780,19 +3392,35 @@ const MAX_SUGGESTIONS = 5;
2780
3392
  * Pass `suggestions={false}` to opt out when a provider is mounted.
2781
3393
  *
2782
3394
  * The suggestion dropdown includes built-in debouncing, keyboard navigation (Arrow keys,
2783
- * Enter, Escape), click-outside dismissal, and ARIA listbox semantics.
3395
+ * Enter, Escape), click-outside dismissal, and ARIA listbox semantics — all provided by
3396
+ * the shared headless `useAddressSuggestionField` hook + `AddressSuggestionList`.
3397
+ *
3398
+ * **Inline name resolution (SF-3, opt-in via context).** When a
3399
+ * `NameResolverProvider` is mounted, the field also accepts a name (e.g.
3400
+ * `alice.eth`): the name is resolved inline through the injected `resolveName`
3401
+ * and the RHF form value becomes the resolved hex — never the name, never a
3402
+ * silently-coerced value. The correctness spine is a shadow-state model:
3403
+ * the `<input>` always shows the typed string (`inputValue`, INV-69); the RHF
3404
+ * value is `''` for every unresolved name state so submit stays gated with no
3405
+ * async validator (INV-75); the single name→hex write fires only on
3406
+ * `resolved` + normalized name-match (INV-79/80). With **no** provider mounted
3407
+ * every resolution branch below is dead code and the field is byte-identical
3408
+ * to its pre-ENS behavior (INV-82 — the LOCKED backward-compat guarantee).
3409
+ * The component stays capability-free: it never reads a runtime or wallet
3410
+ * state; everything arrives through the injected context (INV-118).
2784
3411
  */
2785
- function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, addressing, readOnly, suggestions: suggestionsProp, onSuggestionSelect }) {
3412
+ function AddressField({ id, label, placeholder, helperText, control, name, width = "full", validation, addressing, readOnly, suggestions: suggestionsProp, onSuggestionSelect, onResolvedNameChange }) {
2786
3413
  const isRequired = !!validation?.required;
2787
3414
  const errorId = `${id}-error`;
2788
3415
  const descriptionId = `${id}-description`;
2789
- const contextResolver = (0, react.useContext)(AddressSuggestionContext);
2790
- const containerRef = (0, react.useRef)(null);
3416
+ const resolutionRegionId = `${id}-resolution`;
3417
+ const resolver = useNameResolver();
3418
+ const resolveName = resolver?.resolveName;
3419
+ const isValidName = resolver?.isValidName;
3420
+ const activeNetworkId = resolver?.activeNetworkId;
3421
+ const activeNetworkName = resolver?.activeNetworkName;
2791
3422
  const lastSetValueRef = (0, react.useRef)("");
2792
3423
  const [inputValue, setInputValue] = (0, react.useState)("");
2793
- const [debouncedQuery, setDebouncedQuery] = (0, react.useState)("");
2794
- const [showSuggestions, setShowSuggestions] = (0, react.useState)(false);
2795
- const [highlightedIndex, setHighlightedIndex] = (0, react.useState)(-1);
2796
3424
  const watchedFieldValue = (0, react_hook_form.useWatch)({
2797
3425
  control,
2798
3426
  name
@@ -2804,173 +3432,271 @@ function AddressField({ id, label, placeholder, helperText, control, name, width
2804
3432
  setInputValue(currentFieldValue);
2805
3433
  }
2806
3434
  }, [watchedFieldValue]);
3435
+ const isValidAddress = (0, react.useMemo)(() => addressing ? (v) => addressing.isValidAddress(v) : void 0, [addressing]);
3436
+ const classification = (0, react.useMemo)(() => (0, _openzeppelin_ui_utils.classifyAddressInput)(inputValue, {
3437
+ isValidAddress,
3438
+ isValidName
3439
+ }), [
3440
+ inputValue,
3441
+ isValidAddress,
3442
+ isValidName
3443
+ ]);
3444
+ const resolverActive = resolver !== null && classification === "name-candidate";
3445
+ const nameEnabled = resolverActive && resolveName != null;
3446
+ const forwardUnsupported = resolverActive && resolveName == null;
3447
+ const resolution = useInjectedNameResolution({
3448
+ input: inputValue,
3449
+ enabled: nameEnabled,
3450
+ resolveName
3451
+ });
3452
+ const chainScopeBlocked = resolution.status === "resolved" && (0, _openzeppelin_ui_utils.isChainScopeMismatch)(resolution.data.provenance, activeNetworkId);
3453
+ const resolvingAnnouncerCopy = useResolvingAnnouncerCopy({ isPending: !forwardUnsupported && (resolution.status === "debouncing" || resolution.status === "loading") });
3454
+ const resolverPresentRef = (0, react.useRef)(false);
3455
+ const classificationRef = (0, react.useRef)("empty");
3456
+ const statusRef = (0, react.useRef)("idle");
3457
+ const resolvedNameRef = (0, react.useRef)(void 0);
3458
+ const chainScopeBlockedRef = (0, react.useRef)(false);
3459
+ const validationRef = (0, react.useRef)(validation);
3460
+ const addressingRef = (0, react.useRef)(addressing);
3461
+ resolverPresentRef.current = resolver !== null;
3462
+ classificationRef.current = classification;
3463
+ statusRef.current = resolution.status;
3464
+ resolvedNameRef.current = resolution.status === "resolved" ? resolution.name : void 0;
3465
+ chainScopeBlockedRef.current = chainScopeBlocked;
3466
+ validationRef.current = validation;
3467
+ addressingRef.current = addressing;
3468
+ const { field, fieldState } = (0, react_hook_form.useController)({
3469
+ control,
3470
+ name,
3471
+ rules: { validate: (0, react.useCallback)((value) => {
3472
+ if (resolverPresentRef.current && classificationRef.current === "name-candidate" && (statusRef.current !== "resolved" || chainScopeBlockedRef.current)) return NAME_RESOLUTION_PENDING_GATE;
3473
+ if (value === void 0 || value === null || value === "") return validationRef.current?.required ? "This field is required" : true;
3474
+ const standardValidationResult = require_ErrorMessage.validateField(value, validationRef.current);
3475
+ if (standardValidationResult !== true) return standardValidationResult;
3476
+ if (addressingRef.current && typeof value === "string") {
3477
+ if (!addressingRef.current.isValidAddress(value)) return "Invalid address format for the selected chain";
3478
+ }
3479
+ return true;
3480
+ }, []) },
3481
+ disabled: readOnly
3482
+ });
3483
+ const fieldOnChange = field.onChange;
3484
+ const gateFormState = (0, react_hook_form.useFormState)({
3485
+ control,
3486
+ disabled: resolver === null
3487
+ });
3488
+ const observedIsValid = resolver !== null && gateFormState.isValid;
3489
+ const pendingNameGate = resolver !== null && classification === "name-candidate" && (resolution.status !== "resolved" || chainScopeBlocked);
2807
3490
  (0, react.useEffect)(() => {
2808
- if (!inputValue.trim()) {
2809
- setDebouncedQuery("");
2810
- return;
3491
+ if (pendingNameGate && observedIsValid) control.setError(name, {
3492
+ type: "validate",
3493
+ message: NAME_RESOLUTION_PENDING_GATE
3494
+ });
3495
+ }, [
3496
+ pendingNameGate,
3497
+ observedIsValid,
3498
+ control,
3499
+ name
3500
+ ]);
3501
+ const resolvedAddress = resolution.status === "resolved" ? resolution.data.address : void 0;
3502
+ const resolvedName = resolution.status === "resolved" ? resolution.name : void 0;
3503
+ const isResolvedNameMatch = resolvedName !== void 0 && normalizeResolvedName(inputValue) === resolvedName;
3504
+ (0, react.useEffect)(() => {
3505
+ if (isResolvedNameMatch && resolvedAddress !== void 0 && !chainScopeBlocked) {
3506
+ lastSetValueRef.current = resolvedAddress;
3507
+ fieldOnChange(resolvedAddress);
2811
3508
  }
2812
- const timer = setTimeout(() => setDebouncedQuery(inputValue), DEBOUNCE_MS);
2813
- return () => clearTimeout(timer);
2814
- }, [inputValue]);
2815
- const suggestionsDisabled = suggestionsProp === false;
2816
- const resolvedSuggestions = (0, react.useMemo)(() => {
2817
- if (suggestionsDisabled) return [];
2818
- if (Array.isArray(suggestionsProp)) return suggestionsProp;
2819
- if (!contextResolver || !debouncedQuery.trim()) return [];
2820
- return contextResolver.resolveSuggestions(debouncedQuery).slice(0, MAX_SUGGESTIONS);
2821
3509
  }, [
2822
- suggestionsDisabled,
2823
- suggestionsProp,
2824
- contextResolver,
2825
- debouncedQuery
3510
+ isResolvedNameMatch,
3511
+ resolvedAddress,
3512
+ chainScopeBlocked,
3513
+ fieldOnChange
2826
3514
  ]);
2827
- const hasSuggestions = showSuggestions && resolvedSuggestions.length > 0;
3515
+ const matchedName = isResolvedNameMatch ? resolvedName : void 0;
2828
3516
  (0, react.useEffect)(() => {
2829
- let active = true;
2830
- const handleClickOutside = (e) => {
2831
- if (active && containerRef.current && !containerRef.current.contains(e.target)) setShowSuggestions(false);
2832
- };
2833
- document.addEventListener("mousedown", handleClickOutside);
2834
- return () => {
2835
- active = false;
2836
- document.removeEventListener("mousedown", handleClickOutside);
2837
- };
2838
- }, []);
2839
- const handleSuggestionKeyDown = (0, react.useCallback)((e) => {
2840
- if (!hasSuggestions) return;
2841
- if (e.key === "ArrowDown") {
2842
- e.preventDefault();
2843
- setHighlightedIndex((prev) => prev < resolvedSuggestions.length - 1 ? prev + 1 : 0);
2844
- } else if (e.key === "ArrowUp") {
3517
+ onResolvedNameChange?.(matchedName);
3518
+ }, [matchedName, onResolvedNameChange]);
3519
+ const { containerRef, resolvedSuggestions, hasSuggestions, highlightedIndex, suggestionsDisabled, setHighlightedIndex, closeSuggestions, onInputChange, onContainerKeyDown } = useAddressSuggestionField({
3520
+ inputValue,
3521
+ suggestions: suggestionsProp
3522
+ });
3523
+ /**
3524
+ * The single site for the typed/raw write (the name→hex write lives in the
3525
+ * effect above). With no resolver this is the legacy write, verbatim (INV-82);
3526
+ * with a resolver, only the `name-candidate` classification diverges:
3527
+ * - hex / malformed / empty → the raw value (legacy sync validate applies)
3528
+ * - name-candidate → `''` — gates submit and synchronously invalidates any
3529
+ * prior resolved hex before re-resolution (INV-75 / INV-81)
3530
+ */
3531
+ const commitTypedValue = (0, react.useCallback)((value) => {
3532
+ setInputValue(value);
3533
+ if (!resolverPresentRef.current) {
3534
+ lastSetValueRef.current = value;
3535
+ fieldOnChange(value);
3536
+ return;
3537
+ }
3538
+ const c = (0, _openzeppelin_ui_utils.classifyAddressInput)(value, {
3539
+ isValidAddress,
3540
+ isValidName
3541
+ });
3542
+ classificationRef.current = c;
3543
+ if (c === "name-candidate") {
3544
+ const normalized = normalizeResolvedName(value);
3545
+ if (statusRef.current === "resolved" && resolvedNameRef.current === normalized && !chainScopeBlockedRef.current) return;
3546
+ statusRef.current = "debouncing";
3547
+ lastSetValueRef.current = "";
3548
+ fieldOnChange("");
3549
+ } else {
3550
+ lastSetValueRef.current = value;
3551
+ fieldOnChange(value);
3552
+ }
3553
+ }, [
3554
+ fieldOnChange,
3555
+ isValidAddress,
3556
+ isValidName
3557
+ ]);
3558
+ const handleInputChange = (e) => {
3559
+ commitTypedValue(e.target.value);
3560
+ onInputChange(e.target.value);
3561
+ };
3562
+ const applySuggestion = (suggestion) => {
3563
+ commitTypedValue(suggestion.value);
3564
+ onSuggestionSelect?.(suggestion);
3565
+ closeSuggestions();
3566
+ };
3567
+ const handleKeyDown = (e) => {
3568
+ if (hasSuggestions && e.key === "Enter" && highlightedIndex >= 0) {
2845
3569
  e.preventDefault();
2846
- setHighlightedIndex((prev) => prev > 0 ? prev - 1 : resolvedSuggestions.length - 1);
3570
+ applySuggestion(resolvedSuggestions[highlightedIndex]);
3571
+ return;
2847
3572
  }
2848
- }, [hasSuggestions, resolvedSuggestions.length]);
3573
+ if (e.key === "Escape") {
3574
+ if (hasSuggestions) {
3575
+ closeSuggestions();
3576
+ return;
3577
+ }
3578
+ handleEscapeKey(commitTypedValue, inputValue)(e);
3579
+ }
3580
+ };
3581
+ const hasError = !!fieldState.error;
3582
+ const isPendingGate = fieldState.error?.message === NAME_RESOLUTION_PENDING_GATE;
3583
+ const hasRealError = hasError && !isPendingGate;
3584
+ const shouldShowError = hasRealError && fieldState.isTouched;
3585
+ const validationClasses = require_ErrorMessage.getValidationStateClasses(hasRealError ? fieldState.error : void 0, isPendingGate ? false : fieldState.isTouched);
3586
+ const patternErrorMessage = validation?.pattern && typeof validation.pattern === "object" && "message" in validation.pattern ? validation.pattern.message : void 0;
3587
+ const accessibilityProps = getAccessibilityProps({
3588
+ id,
3589
+ hasError: hasRealError,
3590
+ isRequired,
3591
+ hasHelperText: !!helperText
3592
+ });
3593
+ const renderOutcome = () => {
3594
+ if (forwardUnsupported) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3595
+ role: "alert",
3596
+ className: "text-destructive text-sm",
3597
+ children: (0, _openzeppelin_ui_utils.nameResolutionMessageForCode)("UNSUPPORTED_NETWORK")
3598
+ });
3599
+ switch (resolution.status) {
3600
+ case "idle": return null;
3601
+ case "debouncing":
3602
+ case "loading": return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3603
+ className: "text-muted-foreground text-sm",
3604
+ children: resolvingAnnouncerCopy
3605
+ });
3606
+ case "resolved":
3607
+ if (chainScopeBlocked) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3608
+ role: "alert",
3609
+ className: "text-destructive text-sm",
3610
+ children: (0, _openzeppelin_ui_utils.nameResolutionChainScopeMismatchMessage)({ activeNetworkName })
3611
+ });
3612
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3613
+ className: "text-sm",
3614
+ children: ["Resolved to ", /* @__PURE__ */ (0, react_jsx_runtime.jsx)("code", {
3615
+ className: "font-mono",
3616
+ children: resolution.data.address
3617
+ })]
3618
+ });
3619
+ case "error": {
3620
+ const networkName = resolution.error.code === "UNSUPPORTED_NETWORK" ? resolution.error.networkId || void 0 : void 0;
3621
+ const message = (0, _openzeppelin_ui_utils.nameResolutionMessageForCode)(resolution.error.code, { networkName });
3622
+ const isTransient = TRANSIENT_ERROR_CODES.has(resolution.error.code);
3623
+ const retry = resolution.retry;
3624
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
3625
+ role: "alert",
3626
+ className: "text-destructive text-sm",
3627
+ children: [message, isTransient ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
3628
+ type: "button",
3629
+ className: "ml-2 underline",
3630
+ onClick: retry,
3631
+ children: "Retry"
3632
+ }) : null]
3633
+ });
3634
+ }
3635
+ }
3636
+ };
2849
3637
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2850
3638
  className: `flex flex-col gap-2 ${width === "full" ? "w-full" : width === "half" ? "w-1/2" : "w-1/3"}`,
2851
- children: [label && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Label, {
2852
- htmlFor: id,
2853
- children: [
2854
- label,
2855
- " ",
2856
- isRequired && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2857
- className: "text-destructive",
2858
- children: "*"
2859
- })
2860
- ]
2861
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_hook_form.Controller, {
2862
- control,
2863
- name,
2864
- rules: { validate: (value) => {
2865
- if (value === void 0 || value === null || value === "") return validation?.required ? "This field is required" : true;
2866
- const standardValidationResult = require_ErrorMessage.validateField(value, validation);
2867
- if (standardValidationResult !== true) return standardValidationResult;
2868
- if (addressing && typeof value === "string") {
2869
- if (!addressing.isValidAddress(value)) return "Invalid address format for the selected chain";
2870
- }
2871
- return true;
2872
- } },
2873
- disabled: readOnly,
2874
- render: ({ fieldState: { error, isTouched }, field }) => {
2875
- const hasError = !!error;
2876
- const shouldShowError = hasError && isTouched;
2877
- const validationClasses = require_ErrorMessage.getValidationStateClasses(error, isTouched);
2878
- const patternErrorMessage = validation?.pattern && typeof validation.pattern === "object" && "message" in validation.pattern ? validation.pattern.message : void 0;
2879
- const handleInputChange = (e) => {
2880
- const value = e.target.value;
2881
- field.onChange(value);
2882
- lastSetValueRef.current = value;
2883
- setInputValue(value);
2884
- setShowSuggestions(value.length > 0);
2885
- setHighlightedIndex(-1);
2886
- };
2887
- const applySuggestion = (suggestion) => {
2888
- field.onChange(suggestion.value);
2889
- onSuggestionSelect?.(suggestion);
2890
- lastSetValueRef.current = suggestion.value;
2891
- setInputValue(suggestion.value);
2892
- setShowSuggestions(false);
2893
- setHighlightedIndex(-1);
2894
- };
2895
- const handleKeyDown = (e) => {
2896
- if (hasSuggestions && e.key === "Enter" && highlightedIndex >= 0) {
2897
- e.preventDefault();
2898
- applySuggestion(resolvedSuggestions[highlightedIndex]);
2899
- return;
2900
- }
2901
- if (e.key === "Escape") {
2902
- if (hasSuggestions) {
2903
- setShowSuggestions(false);
2904
- return;
2905
- }
2906
- handleEscapeKey(field.onChange, field.value)(e);
2907
- }
2908
- };
2909
- const accessibilityProps = getAccessibilityProps({
2910
- id,
2911
- hasError,
2912
- isRequired,
2913
- hasHelperText: !!helperText
2914
- });
2915
- return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(react_jsx_runtime.Fragment, { children: [
2916
- /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2917
- ref: containerRef,
2918
- className: "relative",
2919
- onKeyDown: handleSuggestionKeyDown,
2920
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
2921
- ...field,
2922
- id,
2923
- placeholder: placeholder || "0x...",
2924
- className: validationClasses,
2925
- onChange: handleInputChange,
2926
- onKeyDown: handleKeyDown,
2927
- "data-slot": "input",
2928
- value: field.value ?? "",
2929
- ...accessibilityProps,
2930
- "aria-describedby": `${helperText ? descriptionId : ""} ${hasError ? errorId : ""}`,
2931
- "aria-expanded": hasSuggestions,
2932
- "aria-autocomplete": suggestionsDisabled ? void 0 : "list",
2933
- "aria-controls": hasSuggestions ? `${id}-suggestions` : void 0,
2934
- "aria-activedescendant": hasSuggestions && highlightedIndex >= 0 ? `${id}-suggestion-${highlightedIndex}` : void 0,
2935
- disabled: readOnly
2936
- }), hasSuggestions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2937
- id: `${id}-suggestions`,
2938
- className: (0, _openzeppelin_ui_utils.cn)("absolute z-50 mt-1 w-full rounded-md border border-border bg-popover shadow-md", "max-h-48 overflow-auto"),
2939
- role: "listbox",
2940
- children: resolvedSuggestions.map((s, i) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("button", {
2941
- id: `${id}-suggestion-${i}`,
2942
- type: "button",
2943
- role: "option",
2944
- "aria-selected": i === highlightedIndex,
2945
- className: (0, _openzeppelin_ui_utils.cn)("flex w-full flex-col px-3 py-2 text-left text-sm", "hover:bg-selected/10", i === highlightedIndex && "bg-selected/10"),
2946
- onMouseDown: (e) => {
2947
- e.preventDefault();
2948
- applySuggestion(s);
2949
- },
2950
- onMouseEnter: () => setHighlightedIndex(i),
2951
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2952
- className: "font-medium",
2953
- children: s.label
2954
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2955
- className: "truncate font-mono text-xs text-muted-foreground",
2956
- children: s.value
2957
- })]
2958
- }, `${s.value}-${s.description ?? i}`))
2959
- })]
2960
- }),
2961
- helperText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2962
- id: descriptionId,
2963
- className: "text-muted-foreground text-sm",
2964
- children: helperText
2965
- }),
2966
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ErrorMessage.ErrorMessage, {
2967
- error,
2968
- id: errorId,
2969
- message: shouldShowError ? error?.message || patternErrorMessage : void 0
3639
+ children: [
3640
+ label && /* @__PURE__ */ (0, react_jsx_runtime.jsxs)(Label, {
3641
+ htmlFor: id,
3642
+ children: [
3643
+ label,
3644
+ " ",
3645
+ isRequired && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
3646
+ className: "text-destructive",
3647
+ children: "*"
2970
3648
  })
2971
- ] });
2972
- }
2973
- })]
3649
+ ]
3650
+ }),
3651
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
3652
+ ref: containerRef,
3653
+ className: "relative",
3654
+ onKeyDown: onContainerKeyDown,
3655
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)(Input, {
3656
+ ...field,
3657
+ id,
3658
+ placeholder: placeholder || (resolver !== null ? "0x... or name" : "0x..."),
3659
+ className: validationClasses,
3660
+ onChange: handleInputChange,
3661
+ onKeyDown: handleKeyDown,
3662
+ "data-slot": "input",
3663
+ value: inputValue,
3664
+ ...accessibilityProps,
3665
+ "aria-describedby": resolver === null ? `${helperText ? descriptionId : ""} ${hasRealError ? errorId : ""}` : [
3666
+ helperText ? descriptionId : "",
3667
+ hasRealError ? errorId : "",
3668
+ resolutionRegionId
3669
+ ].filter(Boolean).join(" ") || void 0,
3670
+ "aria-expanded": hasSuggestions,
3671
+ "aria-autocomplete": suggestionsDisabled ? void 0 : "list",
3672
+ "aria-controls": hasSuggestions ? `${id}-suggestions` : void 0,
3673
+ "aria-activedescendant": hasSuggestions && highlightedIndex >= 0 ? `${id}-suggestion-${highlightedIndex}` : void 0,
3674
+ disabled: readOnly
3675
+ }), hasSuggestions && /* @__PURE__ */ (0, react_jsx_runtime.jsx)(AddressSuggestionList, {
3676
+ id,
3677
+ suggestions: resolvedSuggestions,
3678
+ highlightedIndex,
3679
+ onSelect: applySuggestion,
3680
+ onHighlight: setHighlightedIndex
3681
+ })]
3682
+ }),
3683
+ helperText && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3684
+ id: descriptionId,
3685
+ className: "text-muted-foreground text-sm",
3686
+ children: helperText
3687
+ }),
3688
+ /* @__PURE__ */ (0, react_jsx_runtime.jsx)(require_ErrorMessage.ErrorMessage, {
3689
+ error: hasRealError ? fieldState.error : void 0,
3690
+ id: errorId,
3691
+ message: shouldShowError ? fieldState.error?.message || patternErrorMessage : void 0
3692
+ }),
3693
+ resolver !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
3694
+ id: resolutionRegionId,
3695
+ "aria-live": "polite",
3696
+ className: "min-h-5",
3697
+ children: renderOutcome()
3698
+ })
3699
+ ]
2974
3700
  });
2975
3701
  }
2976
3702
  AddressField.displayName = "AddressField";
@@ -3510,6 +4236,82 @@ function useAddressSuggestions(query, networkId) {
3510
4236
  ]) };
3511
4237
  }
3512
4238
 
4239
+ //#endregion
4240
+ //#region src/components/fields/name-resolution/name-resolver-context.tsx
4241
+ /**
4242
+ * Name Resolver Context (SF-3)
4243
+ *
4244
+ * Provides a React context for forward name resolution (name → address).
4245
+ * When a `NameResolverProvider` is mounted, every `AddressField` in the
4246
+ * subtree resolves typed names inline through the injected `resolveName`
4247
+ * and submits the resolved hex — never the name (SC-004).
4248
+ *
4249
+ * The provider is deliberately **dumb** (INV-118): it memoizes the injected
4250
+ * functions into the context value and nothing else — no hook state, no
4251
+ * capability, no chain dependency. The smart runtime wiring lives upstream in
4252
+ * `@openzeppelin/ui-react` (`useRuntimeNameResolver`), which an app or the
4253
+ * renderer mounts ambiently:
4254
+ *
4255
+ * @example
4256
+ * ```tsx
4257
+ * import { NameResolverProvider } from '@openzeppelin/ui-components';
4258
+ * import { useRuntimeNameResolver } from '@openzeppelin/ui-react';
4259
+ *
4260
+ * function FormRoot() {
4261
+ * const resolver = useRuntimeNameResolver();
4262
+ * return (
4263
+ * <NameResolverProvider {...resolver}>
4264
+ * <MyForm />
4265
+ * </NameResolverProvider>
4266
+ * );
4267
+ * }
4268
+ * ```
4269
+ */
4270
+ /**
4271
+ * Provides forward name resolution to all `AddressField` instances in the
4272
+ * subtree (zero call-site wiring, SC-001). Both functions are optional: an
4273
+ * absent `resolveName` means forward resolution is unsupported and a typed
4274
+ * name surfaces `UNSUPPORTED_NETWORK` (INV-119 / SC-006).
4275
+ *
4276
+ * ## Stable resolver identity (integrator contract)
4277
+ *
4278
+ * The injected resolver **must be referentially stable across renders** —
4279
+ * memoize it (e.g. `useMemo`) or use `useRuntimeNameResolver` from
4280
+ * `@openzeppelin/ui-react`, which is stable by construction. A resolver whose
4281
+ * function identity changes on every render (e.g. a fresh inline function) is a
4282
+ * misconfiguration: the field keys inline resolution on that identity, so a
4283
+ * churning identity would otherwise re-dispatch on every render — an unbounded
4284
+ * RPC loop on a funds path.
4285
+ *
4286
+ * The base component **detects and withstands** this (it is not merely a caveat):
4287
+ * dispatch is bounded per resolution intent so a churning resolver can never drive
4288
+ * an unbounded loop (INV-123), a one-shot development-only warning names the
4289
+ * misconfiguration (INV-124), and the field degrades to a **safe gated state**
4290
+ * (empty value, submit blocked — never a wrong address, never a throw; INV-125)
4291
+ * until the resolver is memoized. A **genuine** resolver/network swap — a single
4292
+ * identity change for a given input — is still honored and re-resolves within
4293
+ * budget (INV-119). The bound is a backstop; the contract is a stable identity.
4294
+ *
4295
+ * @param props - Injected resolver functions and children
4296
+ */
4297
+ function NameResolverProvider({ children, isValidName, resolveName, activeNetworkId, activeNetworkName }) {
4298
+ const value = react.useMemo(() => ({
4299
+ isValidName,
4300
+ resolveName,
4301
+ activeNetworkId,
4302
+ activeNetworkName
4303
+ }), [
4304
+ isValidName,
4305
+ resolveName,
4306
+ activeNetworkId,
4307
+ activeNetworkName
4308
+ ]);
4309
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NameResolverContext.Provider, {
4310
+ value,
4311
+ children
4312
+ });
4313
+ }
4314
+
3513
4315
  //#endregion
3514
4316
  //#region src/components/fields/AmountField.tsx
3515
4317
  /**
@@ -6238,7 +7040,7 @@ SelectGroupedField.displayName = "SelectGroupedField";
6238
7040
  * - Full accessibility support with ARIA attributes
6239
7041
  * - Keyboard navigation
6240
7042
  */
6241
- function TextField({ id, label, placeholder, helperText, control, name, width = "full", validation, readOnly }) {
7043
+ function TextField({ id, label, placeholder, helperText, control, name, width = "full", validation, readOnly, onUserEdit }) {
6242
7044
  const isRequired = !!validation?.required;
6243
7045
  const errorId = `${id}-error`;
6244
7046
  const descriptionId = `${id}-description`;
@@ -6274,9 +7076,13 @@ function TextField({ id, label, placeholder, helperText, control, name, width =
6274
7076
  const handleInputChange = (e) => {
6275
7077
  const value = e.target.value;
6276
7078
  field.onChange(value);
7079
+ onUserEdit?.(value);
6277
7080
  };
6278
7081
  const handleKeyDown = (e) => {
6279
- if (e.key === "Escape") handleEscapeKey(field.onChange, field.value)(e);
7082
+ if (e.key === "Escape") handleEscapeKey((value) => {
7083
+ field.onChange(value);
7084
+ onUserEdit?.(value);
7085
+ }, field.value)(e);
6280
7086
  };
6281
7087
  const accessibilityProps = getAccessibilityProps({
6282
7088
  id,
@@ -6817,6 +7623,8 @@ exports.AddressDisplay = AddressDisplay;
6817
7623
  exports.AddressField = AddressField;
6818
7624
  exports.AddressLabelProvider = AddressLabelProvider;
6819
7625
  exports.AddressListField = AddressListField;
7626
+ exports.AddressNameProvider = AddressNameProvider;
7627
+ exports.AddressSuggestionList = AddressSuggestionList;
6820
7628
  exports.AddressSuggestionProvider = AddressSuggestionProvider;
6821
7629
  exports.Alert = Alert;
6822
7630
  exports.AlertDescription = AlertDescription;
@@ -6891,6 +7699,9 @@ exports.LoadingButton = LoadingButton;
6891
7699
  exports.MapEntryRow = MapEntryRow;
6892
7700
  exports.MapField = MapField;
6893
7701
  exports.MidnightIcon = MidnightIcon;
7702
+ exports.NAME_RESOLUTION_DEBOUNCE_MS = NAME_RESOLUTION_DEBOUNCE_MS;
7703
+ exports.NameResolverContext = NameResolverContext;
7704
+ exports.NameResolverProvider = NameResolverProvider;
6894
7705
  exports.NetworkAvailabilityNotice = NetworkAvailabilityNotice;
6895
7706
  exports.NetworkErrorNotificationProvider = NetworkErrorNotificationProvider;
6896
7707
  exports.NetworkIcon = NetworkIcon;
@@ -6965,9 +7776,13 @@ exports.hasFieldError = require_ErrorMessage.hasFieldError;
6965
7776
  exports.isDuplicateMapKey = require_ErrorMessage.isDuplicateMapKey;
6966
7777
  exports.resolveAddressListFieldLabels = resolveAddressListFieldLabels;
6967
7778
  exports.useAddressLabel = useAddressLabel;
7779
+ exports.useAddressName = useAddressName;
7780
+ exports.useAddressSuggestionField = useAddressSuggestionField;
6968
7781
  exports.useAddressSuggestions = useAddressSuggestions;
6969
7782
  exports.useDuplicateKeyIndexes = useDuplicateKeyIndexes;
7783
+ exports.useInjectedNameResolution = useInjectedNameResolution;
6970
7784
  exports.useMapFieldSync = useMapFieldSync;
7785
+ exports.useNameResolver = useNameResolver;
6971
7786
  exports.useNetworkErrorAwareAdapter = useNetworkErrorAwareAdapter;
6972
7787
  exports.useNetworkErrorReporter = useNetworkErrorReporter;
6973
7788
  exports.useNetworkErrors = useNetworkErrors;