@wherabouts/react-ui 0.1.0 → 0.3.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/LICENSE +21 -0
- package/README.md +27 -9
- package/dist/index.cjs +246 -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 +246 -257
- package/dist/index.js.map +1 -1
- package/dist/styles.css +52 -40
- 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 +26 -19
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,30 +108,35 @@
|
|
|
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
|
-
/* ── Dropdown
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
right: 0;
|
|
122
|
-
margin-top: var(--wherabouts-spacing-xs);
|
|
123
|
-
max-height: 300px;
|
|
124
|
-
overflow-y: auto;
|
|
114
|
+
/* ── Dropdown Container ────────────────────────────────────── */
|
|
115
|
+
/* The component positions this (position: fixed) inline and portals it to
|
|
116
|
+
<body>, so no ancestor overflow can clip it. This rule supplies the visual
|
|
117
|
+
chrome and clips the inner scroll area to the rounded corners. */
|
|
118
|
+
[data-slot="address-dropdown"] {
|
|
125
119
|
z-index: var(--wherabouts-z-dropdown);
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
margin: var(--wherabouts-spacing-xs) 0 0 0;
|
|
120
|
+
overflow: hidden;
|
|
121
|
+
background-color: var(--wherabouts-color-dropdown-bg);
|
|
129
122
|
border: var(--wherabouts-border-width) solid
|
|
130
123
|
var(--wherabouts-color-dropdown-border);
|
|
131
124
|
border-radius: var(--wherabouts-border-radius);
|
|
132
|
-
background-color: var(--wherabouts-color-dropdown-bg);
|
|
133
125
|
box-shadow: var(--wherabouts-color-dropdown-shadow);
|
|
134
126
|
}
|
|
135
127
|
|
|
128
|
+
/* ── Dropdown Listbox ──────────────────────────────────────── */
|
|
129
|
+
/* The scroll area itself. It lives in normal flow inside the fixed container
|
|
130
|
+
above (NOT absolutely positioned), so a long suggestion list scrolls here at
|
|
131
|
+
max-height instead of extending past the container and the viewport. */
|
|
132
|
+
[data-slot="address-listbox"] {
|
|
133
|
+
max-height: 300px;
|
|
134
|
+
padding: var(--wherabouts-spacing-sm);
|
|
135
|
+
margin: 0;
|
|
136
|
+
overflow-y: auto;
|
|
137
|
+
list-style: none;
|
|
138
|
+
}
|
|
139
|
+
|
|
136
140
|
[data-slot="address-listbox"]::-webkit-scrollbar {
|
|
137
141
|
width: 8px;
|
|
138
142
|
}
|
|
@@ -146,19 +150,28 @@
|
|
|
146
150
|
border-radius: 4px;
|
|
147
151
|
}
|
|
148
152
|
|
|
153
|
+
/* Branding footer — pinned below the scroll area, inside the dropdown border. */
|
|
154
|
+
[data-slot="address-powered-by"] {
|
|
155
|
+
padding: var(--wherabouts-spacing-sm) var(--wherabouts-spacing-md);
|
|
156
|
+
font-size: var(--wherabouts-font-size-sm);
|
|
157
|
+
color: var(--wherabouts-color-text-muted);
|
|
158
|
+
border-top: var(--wherabouts-border-width) solid
|
|
159
|
+
var(--wherabouts-color-dropdown-border);
|
|
160
|
+
}
|
|
161
|
+
|
|
149
162
|
/* ── Suggestion Item ───────────────────────────────────────── */
|
|
150
163
|
[data-slot="address-item"] {
|
|
164
|
+
display: flex;
|
|
165
|
+
align-items: center;
|
|
166
|
+
min-height: 44px;
|
|
151
167
|
padding: var(--wherabouts-spacing-sm) var(--wherabouts-spacing-md);
|
|
152
168
|
margin: 2px 0;
|
|
153
|
-
|
|
154
|
-
background-color: var(--wherabouts-color-item-bg);
|
|
169
|
+
font-size: var(--wherabouts-font-size-base);
|
|
155
170
|
color: var(--wherabouts-color-item-text);
|
|
156
171
|
cursor: pointer;
|
|
157
|
-
|
|
172
|
+
background-color: var(--wherabouts-color-item-bg);
|
|
173
|
+
border-radius: var(--wherabouts-border-radius-sm);
|
|
158
174
|
transition: background-color var(--wherabouts-transition-fast);
|
|
159
|
-
min-height: 44px;
|
|
160
|
-
display: flex;
|
|
161
|
-
align-items: center;
|
|
162
175
|
}
|
|
163
176
|
|
|
164
177
|
[data-slot="address-item"]:hover {
|
|
@@ -173,11 +186,11 @@
|
|
|
173
186
|
[data-slot="address-item"][data-status="loading"],
|
|
174
187
|
[data-slot="address-item"][data-status="error"],
|
|
175
188
|
[data-slot="address-item"][data-status="empty"] {
|
|
176
|
-
cursor: default;
|
|
177
189
|
justify-content: center;
|
|
190
|
+
min-height: 40px;
|
|
178
191
|
font-size: var(--wherabouts-font-size-sm);
|
|
179
192
|
color: var(--wherabouts-color-text-muted);
|
|
180
|
-
|
|
193
|
+
cursor: default;
|
|
181
194
|
}
|
|
182
195
|
|
|
183
196
|
[data-slot="address-item"][data-status="error"] {
|
|
@@ -199,15 +212,15 @@
|
|
|
199
212
|
}
|
|
200
213
|
|
|
201
214
|
[data-slot="address-form-field"] label [aria-hidden="true"] {
|
|
202
|
-
color: var(--wherabouts-color-error);
|
|
203
215
|
margin-left: 2px;
|
|
216
|
+
color: var(--wherabouts-color-error);
|
|
204
217
|
}
|
|
205
218
|
|
|
206
219
|
[data-slot="address-form-field"] [role="alert"] {
|
|
207
220
|
display: block;
|
|
221
|
+
margin-top: 2px;
|
|
208
222
|
font-size: var(--wherabouts-font-size-sm);
|
|
209
223
|
color: var(--wherabouts-color-error);
|
|
210
|
-
margin-top: 2px;
|
|
211
224
|
}
|
|
212
225
|
|
|
213
226
|
/* ── Field Group ───────────────────────────────────────────── */
|
|
@@ -246,13 +259,12 @@
|
|
|
246
259
|
display: block;
|
|
247
260
|
width: 100%;
|
|
248
261
|
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
262
|
font-size: var(--wherabouts-font-size-base);
|
|
263
|
+
color: var(--wherabouts-color-input-text);
|
|
255
264
|
cursor: default;
|
|
265
|
+
background-color: hsl(0 0% 97%);
|
|
266
|
+
border: var(--wherabouts-border-width) solid var(--wherabouts-color-border);
|
|
267
|
+
border-radius: var(--wherabouts-border-radius);
|
|
256
268
|
}
|
|
257
269
|
|
|
258
270
|
.dark [data-slot="geocode-input"] {
|
|
@@ -278,7 +290,7 @@
|
|
|
278
290
|
/* High contrast mode support */
|
|
279
291
|
@media (prefers-contrast: more) {
|
|
280
292
|
[data-slot="address-input"],
|
|
281
|
-
[data-slot="address-
|
|
293
|
+
[data-slot="address-dropdown"] {
|
|
282
294
|
border-width: 2px;
|
|
283
295
|
}
|
|
284
296
|
|
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.
|