@wherabouts/react-ui 0.1.0 → 0.1.1

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/styles.css CHANGED
@@ -76,14 +76,14 @@
76
76
  display: block;
77
77
  width: 100%;
78
78
  padding: var(--wherabouts-spacing-sm) var(--wherabouts-spacing-md);
79
- border: var(--wherabouts-border-width) solid
80
- var(--wherabouts-color-border);
81
- border-radius: var(--wherabouts-border-radius);
82
- background-color: var(--wherabouts-color-input-bg);
83
- color: var(--wherabouts-color-input-text);
84
79
  font-size: var(--wherabouts-font-size-base);
85
80
  font-weight: var(--wherabouts-font-weight-normal);
86
- transition: border-color var(--wherabouts-transition-fast),
81
+ color: var(--wherabouts-color-input-text);
82
+ background-color: var(--wherabouts-color-input-bg);
83
+ border: var(--wherabouts-border-width) solid var(--wherabouts-color-border);
84
+ border-radius: var(--wherabouts-border-radius);
85
+ transition:
86
+ border-color var(--wherabouts-transition-fast),
87
87
  box-shadow var(--wherabouts-transition-fast);
88
88
  }
89
89
 
@@ -94,14 +94,13 @@
94
94
  [data-slot="address-input"]:focus {
95
95
  outline: none;
96
96
  border-color: var(--wherabouts-color-focus-ring);
97
- box-shadow: 0 0 0 3px
98
- hsl(221.2 83.2% 53.3% / 0.1);
97
+ box-shadow: 0 0 0 3px hsl(221.2 83.2% 53.3% / 0.1);
99
98
  }
100
99
 
101
100
  [data-slot="address-input"]:disabled {
102
- opacity: 0.5;
103
101
  cursor: not-allowed;
104
102
  background-color: hsl(0 0% 95% / 0.5);
103
+ opacity: 0.5;
105
104
  }
106
105
 
107
106
  [data-slot="address-input"][aria-invalid="true"] {
@@ -109,8 +108,7 @@
109
108
  }
110
109
 
111
110
  [data-slot="address-input"][aria-invalid="true"]:focus {
112
- box-shadow: 0 0 0 3px
113
- hsl(0 84.2% 60.2% / 0.1);
111
+ box-shadow: 0 0 0 3px hsl(0 84.2% 60.2% / 0.1);
114
112
  }
115
113
 
116
114
  /* ── Dropdown Listbox ──────────────────────────────────────── */
@@ -148,17 +146,17 @@
148
146
 
149
147
  /* ── Suggestion Item ───────────────────────────────────────── */
150
148
  [data-slot="address-item"] {
149
+ display: flex;
150
+ align-items: center;
151
+ min-height: 44px;
151
152
  padding: var(--wherabouts-spacing-sm) var(--wherabouts-spacing-md);
152
153
  margin: 2px 0;
153
- border-radius: var(--wherabouts-border-radius-sm);
154
- background-color: var(--wherabouts-color-item-bg);
154
+ font-size: var(--wherabouts-font-size-base);
155
155
  color: var(--wherabouts-color-item-text);
156
156
  cursor: pointer;
157
- font-size: var(--wherabouts-font-size-base);
157
+ background-color: var(--wherabouts-color-item-bg);
158
+ border-radius: var(--wherabouts-border-radius-sm);
158
159
  transition: background-color var(--wherabouts-transition-fast);
159
- min-height: 44px;
160
- display: flex;
161
- align-items: center;
162
160
  }
163
161
 
164
162
  [data-slot="address-item"]:hover {
@@ -173,11 +171,11 @@
173
171
  [data-slot="address-item"][data-status="loading"],
174
172
  [data-slot="address-item"][data-status="error"],
175
173
  [data-slot="address-item"][data-status="empty"] {
176
- cursor: default;
177
174
  justify-content: center;
175
+ min-height: 40px;
178
176
  font-size: var(--wherabouts-font-size-sm);
179
177
  color: var(--wherabouts-color-text-muted);
180
- min-height: 40px;
178
+ cursor: default;
181
179
  }
182
180
 
183
181
  [data-slot="address-item"][data-status="error"] {
@@ -199,15 +197,15 @@
199
197
  }
200
198
 
201
199
  [data-slot="address-form-field"] label [aria-hidden="true"] {
202
- color: var(--wherabouts-color-error);
203
200
  margin-left: 2px;
201
+ color: var(--wherabouts-color-error);
204
202
  }
205
203
 
206
204
  [data-slot="address-form-field"] [role="alert"] {
207
205
  display: block;
206
+ margin-top: 2px;
208
207
  font-size: var(--wherabouts-font-size-sm);
209
208
  color: var(--wherabouts-color-error);
210
- margin-top: 2px;
211
209
  }
212
210
 
213
211
  /* ── Field Group ───────────────────────────────────────────── */
@@ -246,13 +244,12 @@
246
244
  display: block;
247
245
  width: 100%;
248
246
  padding: var(--wherabouts-spacing-sm) var(--wherabouts-spacing-md);
249
- border: var(--wherabouts-border-width) solid
250
- var(--wherabouts-color-border);
251
- border-radius: var(--wherabouts-border-radius);
252
- background-color: hsl(0 0% 97%);
253
- color: var(--wherabouts-color-input-text);
254
247
  font-size: var(--wherabouts-font-size-base);
248
+ color: var(--wherabouts-color-input-text);
255
249
  cursor: default;
250
+ background-color: hsl(0 0% 97%);
251
+ border: var(--wherabouts-border-width) solid var(--wherabouts-color-border);
252
+ border-radius: var(--wherabouts-border-radius);
256
253
  }
257
254
 
258
255
  .dark [data-slot="geocode-input"] {
package/docs/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @wherabouts/react-ui — Component documentation
2
+
3
+ Detailed, per-component guides with worked examples, full prop tables,
4
+ accessibility notes, and recipes. For a quick overview and install steps, see
5
+ the [package README](../README.md).
6
+
7
+ These docs mirror the package's [Storybook](../README.md#interactive-docs-storybook),
8
+ where every component has interactive, live examples.
9
+
10
+ ## Components
11
+
12
+ - [AddressAutocomplete](./address-autocomplete.md) — accessible, debounced address search.
13
+ - [AddressFormField](./address-form-field.md) — labelled form wrapper around the autocomplete.
14
+ - [ForwardGeocodeInput](./forward-geocode-input.md) — resolve free text to coordinates.
15
+ - [ReverseGeocodeInput](./reverse-geocode-input.md) — resolve coordinates to an address.
16
+ - [AddressFieldGroup](./address-field-group.md) — controlled street/suburb/state/postcode group.
@@ -0,0 +1,208 @@
1
+ # AddressAutocomplete
2
+
3
+ ## Summary
4
+
5
+ Accessible (WAI-ARIA combobox), debounced address search with keyboard navigation, proximity bias, session tokens, i18n strings, and customizable render slots. Provide a `client` created with `createWheraboutsClient`.
6
+
7
+ ## When to use / when not
8
+
9
+ **Use it when:**
10
+
11
+ - You need a freeform "start typing an address" search box backed by the Wherabouts
12
+ autocomplete API (checkout forms, shipping address capture, location search).
13
+ - You want built-in keyboard navigation (arrow keys, enter, escape) and ARIA semantics
14
+ without wiring up a combobox yourself.
15
+ - You want to bias results toward the user's location (geolocation or explicit lat/lng).
16
+
17
+ **Don't use it when:**
18
+
19
+ - You need a labelled form field with built-in error styling — use `AddressFormField`,
20
+ which wraps this component with a `<label>` and error text.
21
+ - You're editing an already-known, structured address (street/suburb/state/postcode) —
22
+ use `AddressFieldGroup` instead.
23
+ - You only need to resolve free text to coordinates without a suggestion dropdown — use
24
+ `ForwardGeocodeInput`.
25
+
26
+ ## Import & minimal example
27
+
28
+ ```tsx
29
+ import { createWheraboutsClient } from "@wherabouts/sdk";
30
+ import { AddressAutocomplete } from "@wherabouts/react-ui";
31
+ import "@wherabouts/react-ui/styles.css";
32
+
33
+ const client = createWheraboutsClient({ apiKey: import.meta.env.VITE_WHERABOUTS_KEY });
34
+
35
+ export function Checkout() {
36
+ return (
37
+ <AddressAutocomplete
38
+ client={client}
39
+ placeholder="Start typing an address…"
40
+ onSelect={(address) => console.log(address.formattedAddress)}
41
+ />
42
+ );
43
+ }
44
+ ```
45
+
46
+ ## Worked examples
47
+
48
+ ### 1. Controlled selection state
49
+
50
+ `AddressAutocomplete` manages its own input text internally; there is no `value` prop.
51
+ To keep the *selected* address as state in your component, store it from `onSelect` and
52
+ track the raw query text (if you need it) via `onQueryChange`:
53
+
54
+ ```tsx
55
+ function DeliveryAddress() {
56
+ const [selected, setSelected] = useState<AddressWithParsed | null>(null);
57
+ const [query, setQuery] = useState("");
58
+
59
+ return (
60
+ <div>
61
+ <AddressAutocomplete
62
+ client={client}
63
+ onQueryChange={setQuery}
64
+ onSelect={(address) => setSelected(address)}
65
+ placeholder="Delivery address"
66
+ />
67
+ {selected && (
68
+ <p>
69
+ Selected: {selected.streetAddress}, {selected.suburb} {selected.state}{" "}
70
+ {selected.postcode}
71
+ </p>
72
+ )}
73
+ </div>
74
+ );
75
+ }
76
+ ```
77
+
78
+ ### 2. TanStack Form wiring
79
+
80
+ Wire `onSelect` to a TanStack Form field's `setValue`, and surface field validation
81
+ errors via the `error` prop:
82
+
83
+ ```tsx
84
+ import { useForm } from "@tanstack/react-form";
85
+
86
+ function AddressFormExample() {
87
+ const form = useForm({
88
+ defaultValues: { address: null as AddressWithParsed | null },
89
+ });
90
+
91
+ return (
92
+ <form.Field name="address">
93
+ {(field) => (
94
+ <AddressAutocomplete
95
+ client={client}
96
+ placeholder="Shipping address"
97
+ onSelect={(address) => field.handleChange(address)}
98
+ error={field.state.meta.errors?.[0]}
99
+ required
100
+ />
101
+ )}
102
+ </form.Field>
103
+ );
104
+ }
105
+ ```
106
+
107
+ ### 3. Geolocation / proximity bias
108
+
109
+ Enable browser geolocation to bias suggestions toward the user's current position, or
110
+ pass explicit coordinates (e.g. from a previously selected map pin) instead:
111
+
112
+ ```tsx
113
+ // Browser geolocation
114
+ <AddressAutocomplete client={client} enableGeolocation onSelect={setAddress} />
115
+
116
+ // Explicit proximity (e.g. a map center), skips the geolocation prompt
117
+ <AddressAutocomplete
118
+ client={client}
119
+ userLat={-33.8688}
120
+ userLng={151.2093}
121
+ onSelect={setAddress}
122
+ />
123
+ ```
124
+
125
+ `userLat`/`userLng` take precedence over `enableGeolocation` when both are provided.
126
+
127
+ ### 4. Custom suggestion renderer
128
+
129
+ Replace the default street/suburb row with your own markup. `renderSuggestion` receives
130
+ the parsed `AddressWithParsed` result and whether it's the currently keyboard-active row:
131
+
132
+ ```tsx
133
+ <AddressAutocomplete
134
+ client={client}
135
+ onSelect={setAddress}
136
+ renderSuggestion={(address, isActive) => (
137
+ <span style={{ fontWeight: isActive ? 700 : 400 }}>
138
+ 📍 {address.formattedAddress}
139
+ </span>
140
+ )}
141
+ />
142
+ ```
143
+
144
+ ## Props
145
+
146
+ | Prop | Type | Default | Description |
147
+ |---|---|---|---|
148
+ | `client` | `WheraboutsClient` | — | **Required.** SDK client created with `createWheraboutsClient`. |
149
+ | `onSelect` | `(address: AddressWithParsed) => void` | — | Called when a suggestion is selected. |
150
+ | `onQueryChange` | `(query: string) => void` | — | Called as the input text changes. |
151
+ | `placeholder` | `string` | — | Input placeholder text. |
152
+ | `debounceMs` | `number` | `300` | Debounce in ms before querying the API. |
153
+ | `minCharsToSearch` | `number` | `2` | Minimum characters typed before searching. |
154
+ | `maxSuggestions` | `number` | `5` | Maximum number of suggestions to show. |
155
+ | `enableGeolocation` | `boolean` | `false` | Use the browser's geolocation to bias results by proximity. |
156
+ | `userLat` | `number` | — | Explicit latitude for proximity bias (instead of geolocation). |
157
+ | `userLng` | `number` | — | Explicit longitude for proximity bias (instead of geolocation). |
158
+ | `sessionToken` | `string` | — | Group a run of keystrokes into one billable search (see SDK `newSessionToken()`). |
159
+ | `disabled` | `boolean` | — | Disable the input. |
160
+ | `required` | `boolean` | — | Mark the input as required. |
161
+ | `error` | `string` | — | External error message to display. |
162
+ | `id` | `string` | `"wherabouts-autocomplete"` | id forwarded to the input element. |
163
+ | `className` | `string` | — | Class applied to the root container. |
164
+ | `i18nStrings` | `Partial<AddressI18nStrings>` | — | Override built-in UI strings (no results, retry, etc.). |
165
+ | `renderSuggestion` | `(address: AddressWithParsed, isActive: boolean) => ReactNode` | — | Render a custom suggestion row. |
166
+ | `renderEmpty` | `() => ReactNode` | — | Render a custom empty state. |
167
+ | `renderLoading` | `() => ReactNode` | — | Render a custom loading state. |
168
+ | `renderError` | `(error: Error \| null) => ReactNode` | — | Render a custom error state. |
169
+
170
+ ## Accessibility
171
+
172
+ - The input and suggestion list implement the WAI-ARIA 1.2 **combobox** pattern: the
173
+ input has `role="combobox"` semantics (via the `useCombobox` hook), is associated with
174
+ a listbox via `aria-controls`, and exposes `aria-expanded` for open/closed state.
175
+ - The currently keyboard-highlighted suggestion is tracked with `aria-activedescendant`
176
+ on the input, and `aria-selected` on the active `<li>` — this drives the `isActive`
177
+ flag passed to `renderSuggestion`.
178
+ - **Keyboard support:** Arrow Down/Up move the active suggestion, Enter selects the
179
+ active suggestion (calling `onSelect` and clearing the query), and Escape closes the
180
+ dropdown.
181
+ - The component does **not** render its own `<label>`. When using `AddressAutocomplete`
182
+ directly, pair it with a `<label htmlFor={id}>` (matching the `id` prop, default
183
+ `"wherabouts-autocomplete"`), or use `AddressFormField`, which does this for you.
184
+ - Loading, error, and empty states are announced as plain list items inside the same
185
+ listbox region so screen readers traversing the list encounter them in context.
186
+
187
+ ## Recipes & edge cases
188
+
189
+ - **Tuning `debounceMs` / `minCharsToSearch`:** The defaults (`debounceMs={300}`,
190
+ `minCharsToSearch={2}`) suit most forms. For high-traffic search-as-you-type UIs where
191
+ API cost matters, raise `debounceMs` (e.g. `400`) and `minCharsToSearch` (e.g. `4`) to
192
+ reduce request volume — see the `TunedSearch` Storybook story.
193
+ - **`sessionToken` for billing:** Each keystroke debounce fires a billable autocomplete
194
+ request. Generate a session token once per "search session" (e.g. when the user
195
+ focuses the field) with the SDK's `newSessionToken()` and pass it as `sessionToken` so
196
+ the API can group the whole keystroke sequence into a single billable session instead
197
+ of billing per request.
198
+ - **Error / empty / loading slots:** Use `renderError`, `renderEmpty`, and `renderLoading`
199
+ to match your app's design system instead of the built-in text rows. `renderError`
200
+ receives the underlying `Error` (or `null` if the error came from the external `error`
201
+ prop rather than the API call) so you can render API-specific messaging.
202
+ - **External vs. API errors:** The `error` prop (e.g. from form validation) and API
203
+ request failures both surface through the same error slot — the external `error`
204
+ prop takes precedence when both are present.
205
+ - **Geolocation denial:** If `enableGeolocation` is set and the user denies the browser
206
+ permission prompt, results simply fall back to unbiased search; there's no separate
207
+ error state for this today, so don't rely on `i18nStrings.geolocationError` being
208
+ surfaced automatically by this component.
@@ -0,0 +1,152 @@
1
+ # AddressFieldGroup
2
+
3
+ ## Summary
4
+
5
+ A controlled group of structured inputs (street, suburb, state, postcode) for editing a full address. Provide `value` and `onChange`.
6
+
7
+ ## When to use / when not
8
+
9
+ **Use it when:**
10
+
11
+ - You need the user to enter or confirm a full postal address split across discrete fields (street, suburb, state, postcode).
12
+ - You want to pre-fill address fields from an autocomplete selection and then let the user correct individual fields.
13
+ - You are building a checkout, registration, or profile form where a structured address is required for storage or validation.
14
+
15
+ **Don't use it when:**
16
+
17
+ - You only need the user to search for and select an address as a single value — use `AddressAutocomplete` or `AddressFormField` instead.
18
+ - You need reverse geocoding (coordinates → address display) — use `ReverseGeocodeInput` instead.
19
+ - You need forward geocoding (free text → coordinates) — use `ForwardGeocodeInput` instead.
20
+
21
+ ## Import & minimal example
22
+
23
+ ```tsx
24
+ import { useState } from "react";
25
+ import { createWheraboutsClient } from "@wherabouts/sdk";
26
+ import {
27
+ AddressFieldGroup,
28
+ type AddressFieldGroupValue,
29
+ } from "@wherabouts/react-ui";
30
+
31
+ const client = createWheraboutsClient({ apiKey: "..." });
32
+
33
+ const EMPTY: AddressFieldGroupValue = {
34
+ street: "",
35
+ suburb: "",
36
+ state: "",
37
+ postcode: "",
38
+ };
39
+
40
+ function MyForm() {
41
+ const [address, setAddress] = useState<AddressFieldGroupValue>(EMPTY);
42
+
43
+ return (
44
+ <AddressFieldGroup
45
+ client={client}
46
+ value={address}
47
+ onChange={setAddress}
48
+ />
49
+ );
50
+ }
51
+ ```
52
+
53
+ ## Worked examples
54
+
55
+ ### 1. Pre-fill from autocomplete, then allow manual correction
56
+
57
+ `AddressFieldGroup` renders an `AddressAutocomplete` at the top of the group. Selecting a suggestion fills all four fields automatically. The user can then edit individual fields:
58
+
59
+ ```tsx
60
+ const [address, setAddress] = useState<AddressFieldGroupValue>(EMPTY);
61
+
62
+ <AddressFieldGroup
63
+ client={client}
64
+ value={address}
65
+ onChange={setAddress}
66
+ />
67
+
68
+ <pre>{JSON.stringify(address, null, 2)}</pre>
69
+ ```
70
+
71
+ ### 2. Custom field labels (international / regional terminology)
72
+
73
+ Override any label to match your audience:
74
+
75
+ ```tsx
76
+ <AddressFieldGroup
77
+ client={client}
78
+ value={address}
79
+ onChange={setAddress}
80
+ streetLabel="Street address"
81
+ suburbLabel="City"
82
+ stateLabel="Region"
83
+ postcodeLabel="ZIP"
84
+ />
85
+ ```
86
+
87
+ ### 3. Disable all fields while submitting
88
+
89
+ Pass `disabled` to prevent edits during async operations:
90
+
91
+ ```tsx
92
+ const [submitting, setSubmitting] = useState(false);
93
+
94
+ <AddressFieldGroup
95
+ client={client}
96
+ value={address}
97
+ onChange={setAddress}
98
+ disabled={submitting}
99
+ />
100
+ ```
101
+
102
+ ### 4. Persist only when all fields are filled
103
+
104
+ Gate your submit handler on completeness:
105
+
106
+ ```tsx
107
+ const isComplete = Object.values(address).every((v) => v.trim().length > 0);
108
+
109
+ <button disabled={!isComplete} onClick={handleSubmit}>
110
+ Save address
111
+ </button>
112
+ ```
113
+
114
+ ## Props
115
+
116
+ | Prop | Type | Default | Description |
117
+ |---|---|---|---|
118
+ | `client` | `WheraboutsClient` | **Required** | SDK client created with `createWheraboutsClient`. |
119
+ | `value` | `AddressFieldGroupValue` | **Required** | Controlled value for the field group. |
120
+ | `onChange` | `(value: AddressFieldGroupValue) => void` | **Required** | Change handler called with the updated value on any field edit. |
121
+ | `streetLabel` | `string` | `"Street Address"` | Override the street address field label. |
122
+ | `suburbLabel` | `string` | `"Suburb"` | Override the suburb field label. |
123
+ | `stateLabel` | `string` | `"State"` | Override the state field label. |
124
+ | `postcodeLabel` | `string` | `"Postcode"` | Override the postcode field label. |
125
+ | `disabled` | `boolean` | `false` | Disables all fields. |
126
+ | `className` | `string` | — | Additional CSS class applied to the root container. |
127
+
128
+ ### AddressFieldGroupValue
129
+
130
+ ```ts
131
+ interface AddressFieldGroupValue {
132
+ street: string;
133
+ suburb: string;
134
+ state: string;
135
+ postcode: string;
136
+ }
137
+ ```
138
+
139
+ ## Accessibility
140
+
141
+ - Each field renders a native `<input>` with an explicit `<label>` wired via `htmlFor` — screen readers announce the field name correctly without additional markup.
142
+ - The embedded `AddressAutocomplete` at the top of the group provides its own accessible combobox behaviour; selecting a suggestion fills all four fields and moves focus back to the form flow.
143
+ - The `disabled` prop sets the HTML `disabled` attribute on every input, which is communicated to assistive technologies automatically.
144
+ - Field IDs are static (`field-street`, `field-suburb`, `field-state`, `field-postcode`). If you render multiple `AddressFieldGroup` instances on the same page, use a wrapping `<fieldset>` with a `<legend>` to distinguish them.
145
+
146
+ ## Recipes & edge cases
147
+
148
+ - **Autocomplete fills all four fields:** When the user selects a suggestion from the embedded `AddressAutocomplete`, `onChange` is called once with all four fields populated from the matched address. The user can then edit individual fields as needed.
149
+ - **Uncontrolled initialisation:** Initialise `value` with the `EMPTY` constant (or your own empty object) rather than leaving fields undefined — all four keys must be strings for controlled inputs to behave correctly.
150
+ - **Validation:** The component does not validate field content. Apply your own validation logic in `onChange` or before form submission.
151
+ - **Layout:** The street field spans the full width; suburb, state, and postcode share a two-column grid. Apply `className` to the root container to constrain width or add spacing.
152
+ - **Multiple instances on one page:** Wrap each instance in a `<fieldset>` + `<legend>` so screen readers distinguish between shipping and billing address groups, as static field IDs are shared.
@@ -0,0 +1,198 @@
1
+ # AddressFormField
2
+
3
+ ## Summary
4
+
5
+ `AddressAutocomplete` wrapped with a `<label>` and error styling — a drop-in form field. Accepts every `AddressAutocomplete` prop plus `label`, `labelClassName`, and `errorClassName`.
6
+
7
+ ## When to use / when not
8
+
9
+ **Use it when:**
10
+
11
+ - You need a labelled address-search field inside a form — this is the right default
12
+ for checkout forms, shipping address capture, and any other form where the label and
13
+ error text should be co-located with the input.
14
+ - You want required-field marking (`*`) and accessible error text (`role="alert"`,
15
+ `aria-live="polite"`) without wiring them up yourself.
16
+ - You're already using `AddressAutocomplete` props and just want a label + error wrapper
17
+ around them with no extra setup.
18
+
19
+ **Don't use it when:**
20
+
21
+ - You need the raw autocomplete without any label or error chrome — use
22
+ `AddressAutocomplete` directly and bring your own `<label>`.
23
+ - You're editing an already-known, structured address (street/suburb/state/postcode) —
24
+ use `AddressFieldGroup` instead.
25
+ - You only need to resolve free text to coordinates without a suggestion dropdown — use
26
+ `ForwardGeocodeInput`.
27
+
28
+ ## Import & minimal example
29
+
30
+ ```tsx
31
+ import { createWheraboutsClient } from "@wherabouts/sdk";
32
+ import { AddressFormField } from "@wherabouts/react-ui";
33
+ import "@wherabouts/react-ui/styles.css";
34
+
35
+ const client = createWheraboutsClient({ apiKey: import.meta.env.VITE_WHERABOUTS_KEY });
36
+
37
+ export function Checkout() {
38
+ return (
39
+ <AddressFormField client={client} label="Delivery address" required onSelect={setAddress} />
40
+ );
41
+ }
42
+ ```
43
+
44
+ ## Worked examples
45
+
46
+ ### 1. Controlled selection state
47
+
48
+ Store the selected address from `onSelect`; surface it elsewhere in the form:
49
+
50
+ ```tsx
51
+ function DeliveryAddress() {
52
+ const [selected, setSelected] = useState<AddressWithParsed | null>(null);
53
+
54
+ return (
55
+ <div>
56
+ <AddressFormField
57
+ client={client}
58
+ label="Delivery address"
59
+ placeholder="Start typing…"
60
+ onSelect={(address) => setSelected(address)}
61
+ required
62
+ />
63
+ {selected && (
64
+ <p>
65
+ Selected: {selected.streetAddress}, {selected.suburb} {selected.state}{" "}
66
+ {selected.postcode}
67
+ </p>
68
+ )}
69
+ </div>
70
+ );
71
+ }
72
+ ```
73
+
74
+ ### 2. TanStack Form wiring
75
+
76
+ Wire `onSelect` to a TanStack Form field's `handleChange`, and pass the field's
77
+ validation error to `error`:
78
+
79
+ ```tsx
80
+ import { useForm } from "@tanstack/react-form";
81
+
82
+ function CheckoutForm() {
83
+ const form = useForm({
84
+ defaultValues: { address: null as AddressWithParsed | null },
85
+ });
86
+
87
+ return (
88
+ <form.Field name="address">
89
+ {(field) => (
90
+ <AddressFormField
91
+ client={client}
92
+ label="Shipping address"
93
+ onSelect={(address) => field.handleChange(address)}
94
+ error={field.state.meta.errors?.[0]}
95
+ required
96
+ />
97
+ )}
98
+ </form.Field>
99
+ );
100
+ }
101
+ ```
102
+
103
+ ### 3. Custom label and error styles
104
+
105
+ Override label and error appearance via `labelClassName` and `errorClassName`:
106
+
107
+ ```tsx
108
+ <AddressFormField
109
+ client={client}
110
+ label="Pickup location"
111
+ labelClassName="text-lg font-semibold text-brand-900"
112
+ errorClassName="font-medium"
113
+ error={validationError}
114
+ onSelect={setAddress}
115
+ />
116
+ ```
117
+
118
+ ### 4. Geolocation / proximity bias
119
+
120
+ All `AddressAutocomplete` props are forwarded — including geolocation:
121
+
122
+ ```tsx
123
+ <AddressFormField
124
+ client={client}
125
+ label="Delivery address"
126
+ enableGeolocation
127
+ onSelect={setAddress}
128
+ required
129
+ />
130
+ ```
131
+
132
+ ## Props
133
+
134
+ `AddressFormField` extends every prop from `AddressAutocomplete` (see
135
+ [AddressAutocomplete docs](./address-autocomplete.md)) and adds:
136
+
137
+ | Prop | Type | Default | Description |
138
+ |---|---|---|---|
139
+ | `label` | `string` | — | **Required.** Text content of the `<label>` element. |
140
+ | `labelClassName` | `string` | — | Additional class(es) applied to the `<label>` element. |
141
+ | `errorClassName` | `string` | — | Additional class(es) applied to the error text `<p>` element. |
142
+
143
+ All `AddressAutocomplete` props are forwarded unchanged. Key inherited props:
144
+
145
+ | Prop | Type | Default | Description |
146
+ |---|---|---|---|
147
+ | `client` | `WheraboutsClient` | — | **Required.** SDK client created with `createWheraboutsClient`. |
148
+ | `onSelect` | `(address: AddressWithParsed) => void` | — | Called when a suggestion is selected. |
149
+ | `onQueryChange` | `(query: string) => void` | — | Called as the input text changes. |
150
+ | `placeholder` | `string` | — | Input placeholder text. |
151
+ | `debounceMs` | `number` | `300` | Debounce in ms before querying the API. |
152
+ | `minCharsToSearch` | `number` | `2` | Minimum characters typed before searching. |
153
+ | `maxSuggestions` | `number` | `5` | Maximum number of suggestions to show. |
154
+ | `enableGeolocation` | `boolean` | `false` | Use the browser's geolocation to bias results by proximity. |
155
+ | `userLat` | `number` | — | Explicit latitude for proximity bias. |
156
+ | `userLng` | `number` | — | Explicit longitude for proximity bias. |
157
+ | `disabled` | `boolean` | — | Disable the input. |
158
+ | `required` | `boolean` | — | Mark the input as required (also renders `*` next to the label). |
159
+ | `error` | `string` | — | External error message; renders below the input with `role="alert"`. |
160
+ | `id` | `string` | `"wherabouts-field"` | Forwarded to the input element and linked to the label via `htmlFor`. |
161
+ | `className` | `string` | — | Class applied to the `AddressAutocomplete` root container. |
162
+
163
+ ## Accessibility
164
+
165
+ - The component renders a `<label>` with `htmlFor` set to the same `id` as the
166
+ underlying `AddressAutocomplete` input (default `"wherabouts-field"`). This
167
+ association satisfies WCAG 2.1 SC 1.3.1 and ensures screen readers announce the
168
+ label when the input is focused.
169
+ - Pass a custom `id` when you render multiple `AddressFormField` instances on the
170
+ same page to keep `htmlFor`/`id` pairs unique.
171
+ - When `required` is `true`, a `*` character is rendered `aria-hidden="true"` (so it
172
+ is not read by screen readers) alongside the visible label text; the `required`
173
+ attribute is forwarded to the input so assistive technology announces it natively.
174
+ - When `error` is set, a `<p role="alert" aria-live="polite">` is rendered below the
175
+ input. Screen readers will announce the error text without the user having to navigate
176
+ to it. The `errorClassName` prop lets you style this element to match your design
177
+ system.
178
+ - All keyboard navigation and ARIA combobox semantics are inherited from
179
+ `AddressAutocomplete` — see the [AddressAutocomplete accessibility section](./address-autocomplete.md#accessibility).
180
+
181
+ ## Recipes & edge cases
182
+
183
+ - **Multiple fields on one page:** Always supply a unique `id` (e.g. `id="billing-address"`,
184
+ `id="shipping-address"`) when rendering more than one `AddressFormField` on the same
185
+ page. Without it, both fields default to `id="wherabouts-field"`, which breaks the
186
+ `htmlFor` association and is invalid HTML.
187
+ - **Passing `error` from form validation vs. API errors:** The `error` prop accepts an
188
+ external string (e.g. from TanStack Form's `field.state.meta.errors`) and renders it
189
+ below the input. API-level errors from the autocomplete itself surface inside the
190
+ dropdown via `AddressAutocomplete`'s built-in error slot, not through this prop.
191
+ - **Debounce / min-chars tuning:** Inherits `AddressAutocomplete` defaults
192
+ (`debounceMs={300}`, `minCharsToSearch={2}`). For high-traffic forms, raise both to
193
+ reduce API call volume.
194
+ - **`sessionToken` for billing:** Pass `newSessionToken()` from the SDK as `sessionToken`
195
+ to group a whole keystroke sequence into a single billable autocomplete session.
196
+ - **Clearing the field:** There is no imperative `clear()` API. If you need to clear
197
+ the field programmatically (e.g. after form submit), unmount and remount the component
198
+ or use a React `key` change.