shoc-frontend-new/src/components/common/address-autocomplete-field.tsx
Alexandre Brandizzi 3f3a3701c3 feat(sites): suggest addresses as Street Address is typed
SH-331 requires Street Address suggestions after 3 typed characters,
with a pick filling Street, City and State and unpicked typing kept as
free text. SH-272, which SH-331 consolidates, specifies those
suggestions as mocked, and the shared AddressAutocompleteField that
Vendors already uses (SH-271) was built for Sites to reuse. No Places
key is involved.

The Add Site modal and the detail panel's edit mode now render that
field. A pick writes all three values through their own controllers,
so the form marks them dirty and Save changes enables in edit mode.
The field gains an opt-in requiredMarker prop so Sites keeps its label
asterisk while Vendors stays without one.
2026-09-25 13:10:03 -03:00

94 lines
3.2 KiB
TypeScript

import { Autocomplete, TextField } from "@mui/material";
import {
suggestAddresses,
type AddressParts,
type AddressSuggestion,
} from "@/lib/address/vendor-address";
type AddressAutocompleteFieldProps = {
value: string;
onInputChange: (value: string) => void;
/** Fired only when a candidate row is chosen, never while typing. */
onSelect: (parts: AddressParts) => void;
label?: string;
placeholder?: string;
required?: boolean;
/**
* Show MUI's required asterisk on the label. Sites marks required fields that
* way; Vendors spells "(required)" in the label and asserts no asterisk.
*/
requiredMarker?: boolean;
error?: boolean;
helperText?: string;
disabled?: boolean;
};
/**
* Street Address input with mocked City/State completion.
*
* `freeSolo` is the whole point: typing without picking a row is a valid way to
* finish the field. The candidates are a convenience for filling City and State
* in one action, not a constraint on what the address may be.
*
* Shared rather than local to Vendors because Sites needs the identical control
* when it is built (SH-273).
*/
export function AddressAutocompleteField({
value,
onInputChange,
onSelect,
label = "Street Address",
placeholder = "Start typing the street address…",
required = false,
requiredMarker = false,
error = false,
helperText,
disabled = false,
}: AddressAutocompleteFieldProps) {
return (
<Autocomplete<AddressSuggestion, false, false, true>
freeSolo
disabled={disabled}
options={suggestAddresses(value)}
// The options are already derived from the input; letting MUI filter them
// again would drop every row whose label does not literally contain the
// typed text.
filterOptions={(options) => options}
inputValue={value}
onInputChange={(_event, next, reason) => {
// Genuine typing and the clear control update the street. MUI also
// fires this callback with reason "reset" after a selection, carrying
// the option's full "<street>, <city>, <state>" label — forwarding
// that would overwrite the just-picked street with the label.
if (reason !== "input" && reason !== "clear") return;
onInputChange(next);
}}
onChange={(_event, selected) => {
if (selected && typeof selected !== "string") {
onSelect({ street: selected.street, city: selected.city, state: selected.state });
return;
}
if (selected === null) onSelect({ street: "", city: "", state: "" });
}}
getOptionLabel={(option) => (typeof option === "string" ? option : option.label)}
renderInput={(params) => (
<TextField
{...params}
label={label}
placeholder={placeholder}
error={error}
helperText={helperText}
fullWidth
required={requiredMarker}
// Required is set on the input itself; MUI's `required` prop only
// adds the label asterisk, which Vendors must not render.
slotProps={{
...params.slotProps,
htmlInput: { ...params.slotProps?.htmlInput, required },
}}
/>
)}
/>
);
}