@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.
@@ -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.
@@ -0,0 +1,115 @@
1
+ # ForwardGeocodeInput
2
+
3
+ ## Summary
4
+
5
+ Resolves a free-text address string to coordinates (forward geocoding) as the `query` prop changes. Controlled: the parent owns `query` and receives geocode results via `onResult`. Renders a read-only display input showing the resolved `latitude, longitude` pair.
6
+
7
+ ## When to use / when not
8
+
9
+ **Use it when:**
10
+
11
+ - You have a text query (from a search box, form field, or programmatic source) and need the corresponding coordinates without building your own geocode loop.
12
+ - You want a lightweight read-only coordinate display driven by an external input.
13
+ - You need to react to resolved coordinates (e.g. drop a map pin, store lat/lng alongside a form submission).
14
+
15
+ **Don't use it when:**
16
+
17
+ - You need the user to type and search interactively in a single input — use `AddressAutocomplete` instead (it handles debounce, suggestions, and selection in one component).
18
+ - You have coordinates and want the human-readable address — use `ReverseGeocodeInput` instead.
19
+ - You need editable lat/lng fields — wire your own inputs to `useForwardGeocode` directly.
20
+
21
+ ## Import & minimal example
22
+
23
+ ```tsx
24
+ import { createWheraboutsClient } from "@wherabouts/sdk";
25
+ import { ForwardGeocodeInput } from "@wherabouts/react-ui";
26
+
27
+ const client = createWheraboutsClient({ apiKey: "..." });
28
+
29
+ function MyComponent() {
30
+ const [query, setQuery] = React.useState("");
31
+
32
+ return (
33
+ <>
34
+ <input value={query} onChange={(e) => setQuery(e.target.value)} />
35
+ <ForwardGeocodeInput
36
+ client={client}
37
+ query={query}
38
+ onResult={(r) => console.log(r.latitude, r.longitude)}
39
+ />
40
+ </>
41
+ );
42
+ }
43
+ ```
44
+
45
+ ## Worked examples
46
+
47
+ ### 1. Store resolved coordinates on form submit
48
+
49
+ ```tsx
50
+ const [query, setQuery] = React.useState("");
51
+ const [coords, setCoords] = React.useState<{
52
+ lat: number | null;
53
+ lng: number | null;
54
+ }>({ lat: null, lng: null });
55
+
56
+ <input value={query} onChange={(e) => setQuery(e.target.value)} placeholder="Enter address" />
57
+ <ForwardGeocodeInput
58
+ client={client}
59
+ query={query}
60
+ onResult={(r) => setCoords({ lat: r.latitude, lng: r.longitude })}
61
+ placeholder="Resolved coordinates"
62
+ />
63
+ ```
64
+
65
+ ### 2. Debounce the query to reduce API requests
66
+
67
+ The component fires a geocode request on every `query` change. If `query` comes from a fast-changing input (e.g. keystrokes), debounce before passing to `query`:
68
+
69
+ ```tsx
70
+ // Replace with your preferred debounce hook (e.g. `use-debounce`).
71
+ import { useDebouncedValue } from "your-debounce-hook";
72
+
73
+ const [raw, setRaw] = React.useState("");
74
+ const debouncedQuery = useDebouncedValue(raw, 300);
75
+
76
+ <input value={raw} onChange={(e) => setRaw(e.target.value)} />
77
+ <ForwardGeocodeInput client={client} query={debouncedQuery} onResult={handleResult} />
78
+ ```
79
+
80
+ ### 3. Skip geocoding when query is empty
81
+
82
+ Pass `query={null}` (or an empty string) to suppress the network request entirely:
83
+
84
+ ```tsx
85
+ <ForwardGeocodeInput client={client} query={query || null} onResult={handleResult} />
86
+ ```
87
+
88
+ ## Props
89
+
90
+ | Prop | Type | Default | Description |
91
+ |---|---|---|---|
92
+ | `client` | `WheraboutsClient` | **Required** | SDK client created with `createWheraboutsClient`. |
93
+ | `query` | `string \| null` | **Required** | Address text to geocode. `null` or empty string skips the request. |
94
+ | `onResult` | `(r: { latitude: number \| null; longitude: number \| null; formattedAddress: string \| null }) => void` | — | Called whenever the resolved result changes. All fields are `null` when no result is available. |
95
+ | `placeholder` | `string` | `"Coordinates will appear here"` | Placeholder text for the read-only display input. |
96
+ | `id` | `string` | — | `id` forwarded to the input element. |
97
+ | `className` | `string` | — | Additional CSS class applied to the input element. |
98
+ | `disabled` | `boolean` | `false` | Disables the input. |
99
+
100
+ > **Controlled component:** `ForwardGeocodeInput` does not own `query`. Your component must supply and update it. The component only renders the resolved coordinate string; it does not accept user typing.
101
+
102
+ ## Accessibility
103
+
104
+ - The component renders a native `<input type="text" readOnly>` — screen readers announce it as a text field with the current value.
105
+ - Supply a visible `<label>` or `aria-label` / `aria-labelledby` targeting the `id` prop so assistive technologies identify the field.
106
+ - The `disabled` prop produces the standard disabled input state, which is communicated to screen readers automatically.
107
+ - Because the input is read-only, keyboard users cannot accidentally modify the displayed value.
108
+
109
+ ## Recipes & edge cases
110
+
111
+ - **`null` query = no request:** Pass `query={null}` to explicitly suppress geocoding (e.g. before the user has typed anything). An empty string `""` also produces no result.
112
+ - **Debouncing the query:** The component fires on every `query` change. Debounce upstream (e.g. 300 ms) if `query` comes from keystroke events to avoid excessive API calls.
113
+ - **`onResult` on every render cycle:** `onResult` is called inside a `useEffect` that runs whenever the resolved `data` changes. If you pass an inline callback, wrap it in `useCallback` to avoid firing on every parent render.
114
+ - **All-null result:** When the geocode fails or returns no match, `onResult` is still called with `{ latitude: null, longitude: null, formattedAddress: null }`. Guard against nulls before using the values.
115
+ - **Display format:** The component renders `"lat.toFixed(4), lng.toFixed(4)"` in the input. If you need raw numeric values, consume them from the `onResult` callback instead.
@@ -0,0 +1,124 @@
1
+ # ReverseGeocodeInput
2
+
3
+ ## Summary
4
+
5
+ Resolves a `latitude`/`longitude` pair to the nearest address (reverse geocoding). No request is made until both coordinates are non-null.
6
+
7
+ ## When to use / when not
8
+
9
+ **Use it when:**
10
+
11
+ - You have coordinates (e.g. from a GPS fix, map click, or device location) and need the human-readable address for display or storage.
12
+ - You want a lightweight read-only address display that reacts to changing coordinates.
13
+ - You need to react to the resolved address (e.g. prefill a form field, log the location label, show the nearest street address).
14
+
15
+ **Don't use it when:**
16
+
17
+ - You need the user to type and search interactively in a single input — use `AddressAutocomplete` instead (it handles debounce, suggestions, and selection in one component).
18
+ - You have a text query and want coordinates — use `ForwardGeocodeInput` instead.
19
+ - You need editable address fields — wire your own inputs to `useReverseGeocode` directly.
20
+
21
+ ## Import & minimal example
22
+
23
+ ```tsx
24
+ import { createWheraboutsClient } from "@wherabouts/sdk";
25
+ import { ReverseGeocodeInput } from "@wherabouts/react-ui";
26
+
27
+ const client = createWheraboutsClient({ apiKey: "..." });
28
+
29
+ function MyComponent() {
30
+ const [coords, setCoords] = React.useState<{
31
+ lat: number | null;
32
+ lng: number | null;
33
+ }>({ lat: null, lng: null });
34
+
35
+ return (
36
+ <ReverseGeocodeInput
37
+ client={client}
38
+ latitude={coords.lat}
39
+ longitude={coords.lng}
40
+ onResult={(r) => console.log(r.address, r.distance)}
41
+ />
42
+ );
43
+ }
44
+ ```
45
+
46
+ ## Worked examples
47
+
48
+ ### 1. Show address when user clicks a map
49
+
50
+ ```tsx
51
+ const [lat, setLat] = React.useState<number | null>(null);
52
+ const [lng, setLng] = React.useState<number | null>(null);
53
+ const [label, setLabel] = React.useState<string>("");
54
+
55
+ // Imagine mapInstance.on("click", ...) sets lat/lng
56
+ <ReverseGeocodeInput
57
+ client={client}
58
+ latitude={lat}
59
+ longitude={lng}
60
+ onResult={(r) => setLabel(r.address ?? "Unknown location")}
61
+ placeholder="Click the map to resolve an address"
62
+ />
63
+ <p>Selected: {label || "—"}</p>
64
+ ```
65
+
66
+ ### 2. Display distance alongside the resolved address
67
+
68
+ ```tsx
69
+ const [distance, setDistance] = React.useState<number | null>(null);
70
+
71
+ <ReverseGeocodeInput
72
+ client={client}
73
+ latitude={-27.4698}
74
+ longitude={153.0251}
75
+ onResult={(r) => setDistance(r.distance)}
76
+ />
77
+ {distance !== null && (
78
+ <p>Distance from nearest address: {distance.toFixed(1)} m</p>
79
+ )}
80
+ ```
81
+
82
+ ### 3. Hold the request until both coordinates are available
83
+
84
+ Pass `null` for either coordinate to suppress the network request entirely. The component renders its placeholder until a valid pair arrives:
85
+
86
+ ```tsx
87
+ <ReverseGeocodeInput
88
+ client={client}
89
+ latitude={gpsReady ? lat : null}
90
+ longitude={gpsReady ? lng : null}
91
+ onResult={handleResult}
92
+ placeholder="Waiting for GPS fix…"
93
+ />
94
+ ```
95
+
96
+ ## Props
97
+
98
+ | Prop | Type | Default | Description |
99
+ |---|---|---|---|
100
+ | `client` | `WheraboutsClient` | **Required** | SDK client created with `createWheraboutsClient`. |
101
+ | `latitude` | `number \| null` | **Required** | Latitude to reverse-geocode. `null` suppresses the request. |
102
+ | `longitude` | `number \| null` | **Required** | Longitude to reverse-geocode. `null` suppresses the request. |
103
+ | `onResult` | `(r: { address: string \| null; distance: number \| null }) => void` | — | Called whenever the resolved address changes. Both fields are `null` when no result is available. |
104
+ | `placeholder` | `string` | `"Address will appear here"` | Placeholder text for the read-only display input. |
105
+ | `id` | `string` | — | `id` forwarded to the input element. |
106
+ | `className` | `string` | — | Additional CSS class applied to the input element. |
107
+ | `disabled` | `boolean` | `false` | Disables the input. |
108
+
109
+ > **Null-coordinate handling:** No geocode request is made until **both** `latitude` and `longitude` are non-null. The component renders its placeholder while either coordinate is `null`.
110
+
111
+ ## Accessibility
112
+
113
+ - The component renders a native `<input type="text" readOnly>` — screen readers announce it as a text field with the current value.
114
+ - Supply a visible `<label>` or `aria-label` / `aria-labelledby` targeting the `id` prop so assistive technologies identify the field.
115
+ - The `disabled` prop produces the standard disabled input state, which is communicated to screen readers automatically.
116
+ - Because the input is read-only, keyboard users cannot accidentally modify the displayed value.
117
+
118
+ ## Recipes & edge cases
119
+
120
+ - **Both coordinates must be non-null:** If either `latitude` or `longitude` is `null`, no request is fired and the input shows the placeholder. Use this deliberately to gate the request on GPS availability or user interaction.
121
+ - **`onResult` on every render cycle:** `onResult` is called inside a `useEffect` that runs whenever the resolved data changes. If you pass an inline callback, wrap it in `useCallback` to avoid firing on every parent render.
122
+ - **All-null result:** When reverse geocoding returns no match, `onResult` is still called with `{ address: null, distance: null }`. Guard against nulls before using the values.
123
+ - **`address` is `formattedAddress` from the API:** The `address` field in `onResult` is the formatted address string from the nearest result (or `null` if unavailable). It is also what the read-only input displays.
124
+ - **`distance` is in metres:** The `distance` field reports how far the resolved address is from the supplied coordinates, in metres.
package/package.json CHANGED
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "name": "@wherabouts/react-ui",
3
- "version": "0.1.0",
3
+ "version": "0.3.0",
4
4
  "type": "module",
5
5
  "description": "Production-ready React components for Wherabouts SDK — address autocomplete, geocoding, and form fields with accessibility and customization.",
6
- "license": "UNLICENSED",
6
+ "license": "MIT",
7
7
  "publishConfig": {
8
8
  "access": "public"
9
9
  },
@@ -17,38 +17,45 @@
17
17
  },
18
18
  "files": [
19
19
  "dist",
20
+ "docs",
20
21
  "README.md",
21
- "CHANGELOG.md"
22
+ "CHANGELOG.md",
23
+ "LICENSE"
22
24
  ],
23
- "scripts": {
24
- "build": "tsup && node scripts/build-css.mjs",
25
- "dev": "tsup --watch",
26
- "test": "vitest run",
27
- "test:watch": "vitest",
28
- "prepublishOnly": "pnpm build"
29
- },
30
25
  "peerDependencies": {
31
26
  "react": ">=19.0.0",
32
27
  "react-dom": ">=19.0.0",
33
- "@wherabouts/sdk": ">=0.4.2",
28
+ "@wherabouts/sdk": ">=0.5.0",
34
29
  "@wherabouts/react": ">=0.2.0"
35
30
  },
36
31
  "devDependencies": {
32
+ "@storybook/addon-a11y": "^9.0.0",
33
+ "@storybook/react-vite": "^9.0.0",
37
34
  "@testing-library/jest-dom": "^6.1.5",
38
35
  "@testing-library/react": "^15.0.0",
39
36
  "@testing-library/user-event": "^14.5.1",
40
- "@types/react": "catalog:",
41
- "@types/react-dom": "catalog:",
42
- "@wherabouts/react": "workspace:*",
43
- "@wherabouts/sdk": "workspace:*",
37
+ "@types/react": "^19.2.10",
38
+ "@types/react-dom": "^19.2.3",
44
39
  "class-variance-authority": "^0.7.0",
45
40
  "clsx": "^2.1.1",
46
41
  "jsdom": "^23.0.1",
47
- "react": "catalog:",
48
- "react-dom": "catalog:",
42
+ "react": "^19.2.3",
43
+ "react-dom": "^19.2.3",
44
+ "storybook": "^9.0.0",
49
45
  "tailwind-merge": "^2.3.0",
50
46
  "tsup": "^8.0.2",
51
47
  "typescript": "^5.4.4",
52
- "vitest": "^1.2.0"
48
+ "vite": "^7.0.2",
49
+ "vitest": "^1.2.0",
50
+ "@wherabouts/sdk": "0.6.0",
51
+ "@wherabouts/react": "0.3.0"
52
+ },
53
+ "scripts": {
54
+ "build": "tsup && node scripts/build-css.mjs",
55
+ "dev": "tsup --watch",
56
+ "test": "vitest run",
57
+ "test:watch": "vitest",
58
+ "storybook": "storybook dev -p 6006 --no-open",
59
+ "build-storybook": "storybook build"
53
60
  }
54
- }
61
+ }