@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
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wherabouts/react-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
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
6
|
"license": "UNLICENSED",
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
},
|
|
18
18
|
"files": [
|
|
19
19
|
"dist",
|
|
20
|
+
"docs",
|
|
20
21
|
"README.md",
|
|
21
22
|
"CHANGELOG.md"
|
|
22
23
|
],
|
|
@@ -25,7 +26,9 @@
|
|
|
25
26
|
"dev": "tsup --watch",
|
|
26
27
|
"test": "vitest run",
|
|
27
28
|
"test:watch": "vitest",
|
|
28
|
-
"prepublishOnly": "pnpm build"
|
|
29
|
+
"prepublishOnly": "pnpm build",
|
|
30
|
+
"storybook": "storybook dev -p 6006 --no-open",
|
|
31
|
+
"build-storybook": "storybook build"
|
|
29
32
|
},
|
|
30
33
|
"peerDependencies": {
|
|
31
34
|
"react": ">=19.0.0",
|
|
@@ -34,6 +37,8 @@
|
|
|
34
37
|
"@wherabouts/react": ">=0.2.0"
|
|
35
38
|
},
|
|
36
39
|
"devDependencies": {
|
|
40
|
+
"@storybook/addon-a11y": "^9.0.0",
|
|
41
|
+
"@storybook/react-vite": "^9.0.0",
|
|
37
42
|
"@testing-library/jest-dom": "^6.1.5",
|
|
38
43
|
"@testing-library/react": "^15.0.0",
|
|
39
44
|
"@testing-library/user-event": "^14.5.1",
|
|
@@ -46,9 +51,11 @@
|
|
|
46
51
|
"jsdom": "^23.0.1",
|
|
47
52
|
"react": "catalog:",
|
|
48
53
|
"react-dom": "catalog:",
|
|
54
|
+
"storybook": "^9.0.0",
|
|
49
55
|
"tailwind-merge": "^2.3.0",
|
|
50
56
|
"tsup": "^8.0.2",
|
|
51
57
|
"typescript": "^5.4.4",
|
|
58
|
+
"vite": "^7.0.2",
|
|
52
59
|
"vitest": "^1.2.0"
|
|
53
60
|
}
|
|
54
61
|
}
|