@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/README.md +23 -5
- package/dist/index.cjs +249 -257
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +97 -45
- package/dist/index.d.ts +97 -45
- package/dist/index.js +249 -257
- package/dist/index.js.map +1 -1
- package/dist/styles.css +23 -26
- package/docs/README.md +16 -0
- package/docs/address-autocomplete.md +208 -0
- package/docs/address-field-group.md +152 -0
- package/docs/address-form-field.md +198 -0
- package/docs/forward-geocode-input.md +115 -0
- package/docs/reverse-geocode-input.md +124 -0
- package/package.json +9 -2
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|