@jmruthers/pace-core 0.5.186 → 0.5.187
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{DataTable-Z9NLVJh0.d.ts → DataTable-IVYljGJ6.d.ts} +1 -1
- package/dist/{DataTable-IX2NBUTP.js → DataTable-K3RJRSOX.js} +7 -7
- package/dist/{PublicPageProvider-DIzEzwKl.d.ts → PublicPageProvider-DrLDztHt.d.ts} +211 -106
- package/dist/{UnifiedAuthProvider-A4BCQRJY.js → UnifiedAuthProvider-B76OWOAT.js} +2 -2
- package/dist/{api-BMFCXVQX.js → api-YP7XD5L6.js} +3 -3
- package/dist/{audit-WRS3KJKI.js → audit-B5P6FFIR.js} +2 -2
- package/dist/{chunk-445GEP27.js → chunk-3IC5WCMO.js} +33 -8
- package/dist/chunk-3IC5WCMO.js.map +1 -0
- package/dist/{chunk-OALXJH4Y.js → chunk-3NFNJOO7.js} +8 -8
- package/dist/chunk-3NFNJOO7.js.map +1 -0
- package/dist/{chunk-FSFQFJCU.js → chunk-63FOKYGO.js} +174 -6
- package/dist/chunk-63FOKYGO.js.map +1 -0
- package/dist/{chunk-TC7D3CR3.js → chunk-C4OYJOV4.js} +556 -101
- package/dist/chunk-C4OYJOV4.js.map +1 -0
- package/dist/{chunk-HGPQUCBC.js → chunk-FMTK4XNN.js} +3 -3
- package/dist/{chunk-U6WNSFX5.js → chunk-HEHYGYOX.js} +279 -44
- package/dist/chunk-HEHYGYOX.js.map +1 -0
- package/dist/{chunk-XAUHJD3L.js → chunk-K2JGDXGU.js} +2 -2
- package/dist/{chunk-HDCUMOOI.js → chunk-LBBUPSSC.js} +792 -559
- package/dist/chunk-LBBUPSSC.js.map +1 -0
- package/dist/{chunk-UQWSHFVX.js → chunk-SAUPYVLF.js} +1 -1
- package/dist/{chunk-UQWSHFVX.js.map → chunk-SAUPYVLF.js.map} +1 -1
- package/dist/{chunk-GRIQLQ52.js → chunk-T6ZJVI3A.js} +27 -23
- package/dist/chunk-T6ZJVI3A.js.map +1 -0
- package/dist/{chunk-DAGICKHT.js → chunk-ULX5FYEM.js} +3 -3
- package/dist/{chunk-FXFJRTKI.js → chunk-WK2Y6TGA.js} +3 -3
- package/dist/chunk-WK2Y6TGA.js.map +1 -0
- package/dist/chunk-YHCN776L.js +447 -0
- package/dist/chunk-YHCN776L.js.map +1 -0
- package/dist/components.d.ts +4 -4
- package/dist/components.js +12 -10
- package/dist/components.js.map +1 -1
- package/dist/{file-reference-PRTSLxKx.d.ts → file-reference-D037xOFK.d.ts} +0 -1
- package/dist/hooks.d.ts +221 -6
- package/dist/hooks.js +146 -49
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts +24 -9
- package/dist/index.js +62 -28
- package/dist/index.js.map +1 -1
- package/dist/providers.js +1 -1
- package/dist/rbac/index.d.ts +124 -7
- package/dist/rbac/index.js +27 -7
- package/dist/{types-DUyCRSTj.d.ts → types-Bwgl--Xo.d.ts} +162 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.js +1 -1
- package/dist/{usePublicRouteParams-D71QLlg4.d.ts → usePublicRouteParams-CTDELQ7H.d.ts} +2 -2
- package/dist/utils.d.ts +213 -3
- package/dist/utils.js +22 -2
- package/dist/utils.js.map +1 -1
- package/docs/api/classes/ColumnFactory.md +1 -1
- package/docs/api/classes/ErrorBoundary.md +1 -1
- package/docs/api/classes/InvalidScopeError.md +1 -1
- package/docs/api/classes/Logger.md +1 -1
- package/docs/api/classes/MissingUserContextError.md +1 -1
- package/docs/api/classes/OrganisationContextRequiredError.md +1 -1
- package/docs/api/classes/PermissionDeniedError.md +1 -1
- package/docs/api/classes/RBACAuditManager.md +21 -17
- package/docs/api/classes/RBACCache.md +31 -23
- package/docs/api/classes/RBACEngine.md +5 -5
- package/docs/api/classes/RBACError.md +1 -1
- package/docs/api/classes/RBACNotInitializedError.md +1 -1
- package/docs/api/classes/SecureSupabaseClient.md +1 -1
- package/docs/api/classes/StorageUtils.md +1 -1
- package/docs/api/enums/FileCategory.md +1 -1
- package/docs/api/enums/LogLevel.md +1 -1
- package/docs/api/enums/RBACErrorCode.md +1 -1
- package/docs/api/enums/RPCFunction.md +1 -1
- package/docs/api/interfaces/AddressFieldProps.md +241 -0
- package/docs/api/interfaces/AddressFieldRef.md +94 -0
- package/docs/api/interfaces/AggregateConfig.md +1 -1
- package/docs/api/interfaces/AutocompleteOptions.md +75 -0
- package/docs/api/interfaces/BadgeProps.md +1 -1
- package/docs/api/interfaces/ButtonProps.md +1 -1
- package/docs/api/interfaces/CalendarProps.md +1 -1
- package/docs/api/interfaces/CardProps.md +1 -1
- package/docs/api/interfaces/ColorPalette.md +1 -1
- package/docs/api/interfaces/ColorShade.md +1 -1
- package/docs/api/interfaces/ComplianceResult.md +1 -1
- package/docs/api/interfaces/DataAccessRecord.md +1 -1
- package/docs/api/interfaces/DataRecord.md +1 -1
- package/docs/api/interfaces/DataTableAction.md +1 -1
- package/docs/api/interfaces/DataTableColumn.md +1 -1
- package/docs/api/interfaces/DataTableProps.md +1 -1
- package/docs/api/interfaces/DataTableToolbarButton.md +1 -1
- package/docs/api/interfaces/DatabaseComplianceResult.md +1 -1
- package/docs/api/interfaces/DatabaseIssue.md +1 -1
- package/docs/api/interfaces/EmptyStateConfig.md +1 -1
- package/docs/api/interfaces/EnhancedNavigationMenuProps.md +1 -1
- package/docs/api/interfaces/EventAppRoleData.md +1 -1
- package/docs/api/interfaces/ExportColumn.md +1 -1
- package/docs/api/interfaces/ExportOptions.md +1 -1
- package/docs/api/interfaces/FileDisplayProps.md +15 -15
- package/docs/api/interfaces/FileMetadata.md +1 -1
- package/docs/api/interfaces/FileReference.md +1 -1
- package/docs/api/interfaces/FileSizeLimits.md +1 -1
- package/docs/api/interfaces/FileUploadOptions.md +1 -1
- package/docs/api/interfaces/FileUploadProps.md +1 -1
- package/docs/api/interfaces/FooterProps.md +1 -1
- package/docs/api/interfaces/FormFieldProps.md +1 -1
- package/docs/api/interfaces/FormProps.md +1 -1
- package/docs/api/interfaces/GrantEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/InactivityWarningModalProps.md +1 -1
- package/docs/api/interfaces/InputProps.md +1 -1
- package/docs/api/interfaces/LabelProps.md +1 -1
- package/docs/api/interfaces/LoggerConfig.md +1 -1
- package/docs/api/interfaces/LoginFormProps.md +1 -1
- package/docs/api/interfaces/NavigationAccessRecord.md +1 -1
- package/docs/api/interfaces/NavigationContextType.md +1 -1
- package/docs/api/interfaces/NavigationGuardProps.md +1 -1
- package/docs/api/interfaces/NavigationItem.md +1 -1
- package/docs/api/interfaces/NavigationMenuProps.md +1 -1
- package/docs/api/interfaces/NavigationProviderProps.md +1 -1
- package/docs/api/interfaces/Organisation.md +1 -1
- package/docs/api/interfaces/OrganisationContextType.md +1 -1
- package/docs/api/interfaces/OrganisationMembership.md +1 -1
- package/docs/api/interfaces/OrganisationProviderProps.md +1 -1
- package/docs/api/interfaces/OrganisationSecurityError.md +1 -1
- package/docs/api/interfaces/PaceAppLayoutProps.md +1 -1
- package/docs/api/interfaces/PaceLoginPageProps.md +1 -1
- package/docs/api/interfaces/PageAccessRecord.md +1 -1
- package/docs/api/interfaces/PagePermissionContextType.md +1 -1
- package/docs/api/interfaces/PagePermissionGuardProps.md +11 -11
- package/docs/api/interfaces/PagePermissionProviderProps.md +1 -1
- package/docs/api/interfaces/PaletteData.md +1 -1
- package/docs/api/interfaces/ParsedAddress.md +120 -0
- package/docs/api/interfaces/PermissionEnforcerProps.md +1 -1
- package/docs/api/interfaces/ProgressProps.md +1 -1
- package/docs/api/interfaces/ProtectedRouteProps.md +1 -1
- package/docs/api/interfaces/PublicPageFooterProps.md +1 -1
- package/docs/api/interfaces/PublicPageHeaderProps.md +1 -1
- package/docs/api/interfaces/PublicPageLayoutProps.md +1 -1
- package/docs/api/interfaces/QuickFix.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateParams.md +1 -1
- package/docs/api/interfaces/RBACAccessValidateResult.md +1 -1
- package/docs/api/interfaces/RBACAuditLogParams.md +1 -1
- package/docs/api/interfaces/RBACAuditLogResult.md +1 -1
- package/docs/api/interfaces/RBACConfig.md +26 -3
- package/docs/api/interfaces/RBACContext.md +1 -1
- package/docs/api/interfaces/RBACLogger.md +5 -5
- package/docs/api/interfaces/RBACPageAccessCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPerformanceMetrics.md +138 -0
- package/docs/api/interfaces/RBACPermissionCheckParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionCheckResult.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetParams.md +1 -1
- package/docs/api/interfaces/RBACPermissionsGetResult.md +1 -1
- package/docs/api/interfaces/RBACResult.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantParams.md +1 -1
- package/docs/api/interfaces/RBACRoleGrantResult.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeParams.md +1 -1
- package/docs/api/interfaces/RBACRoleRevokeResult.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateParams.md +1 -1
- package/docs/api/interfaces/RBACRoleValidateResult.md +1 -1
- package/docs/api/interfaces/RBACRolesListParams.md +1 -1
- package/docs/api/interfaces/RBACRolesListResult.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackParams.md +1 -1
- package/docs/api/interfaces/RBACSessionTrackResult.md +1 -1
- package/docs/api/interfaces/ResourcePermissions.md +1 -1
- package/docs/api/interfaces/RevokeEventAppRoleParams.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterContextType.md +1 -1
- package/docs/api/interfaces/RoleBasedRouterProps.md +1 -1
- package/docs/api/interfaces/RoleManagementResult.md +1 -1
- package/docs/api/interfaces/RouteAccessRecord.md +1 -1
- package/docs/api/interfaces/RouteConfig.md +1 -1
- package/docs/api/interfaces/RuntimeComplianceResult.md +1 -1
- package/docs/api/interfaces/SecureDataContextType.md +1 -1
- package/docs/api/interfaces/SecureDataProviderProps.md +1 -1
- package/docs/api/interfaces/SessionRestorationLoaderProps.md +1 -1
- package/docs/api/interfaces/SetupIssue.md +1 -1
- package/docs/api/interfaces/StorageConfig.md +1 -1
- package/docs/api/interfaces/StorageFileInfo.md +1 -1
- package/docs/api/interfaces/StorageFileMetadata.md +1 -1
- package/docs/api/interfaces/StorageListOptions.md +1 -1
- package/docs/api/interfaces/StorageListResult.md +1 -1
- package/docs/api/interfaces/StorageUploadOptions.md +1 -1
- package/docs/api/interfaces/StorageUploadResult.md +1 -1
- package/docs/api/interfaces/StorageUrlOptions.md +1 -1
- package/docs/api/interfaces/StyleImport.md +1 -1
- package/docs/api/interfaces/SwitchProps.md +1 -1
- package/docs/api/interfaces/TabsContentProps.md +1 -1
- package/docs/api/interfaces/TabsListProps.md +1 -1
- package/docs/api/interfaces/TabsProps.md +1 -1
- package/docs/api/interfaces/TabsTriggerProps.md +1 -1
- package/docs/api/interfaces/TextareaProps.md +1 -1
- package/docs/api/interfaces/ToastActionElement.md +1 -1
- package/docs/api/interfaces/ToastProps.md +1 -1
- package/docs/api/interfaces/UnifiedAuthContextType.md +1 -1
- package/docs/api/interfaces/UnifiedAuthProviderProps.md +1 -1
- package/docs/api/interfaces/UseFormDialogOptions.md +1 -1
- package/docs/api/interfaces/UseFormDialogReturn.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerOptions.md +1 -1
- package/docs/api/interfaces/UseInactivityTrackerReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventLogoReturn.md +1 -1
- package/docs/api/interfaces/UsePublicEventOptions.md +1 -1
- package/docs/api/interfaces/UsePublicEventReturn.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayOptions.md +1 -1
- package/docs/api/interfaces/UsePublicFileDisplayReturn.md +1 -1
- package/docs/api/interfaces/UsePublicRouteParamsReturn.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeOptions.md +1 -1
- package/docs/api/interfaces/UseResolvedScopeReturn.md +1 -1
- package/docs/api/interfaces/UseResourcePermissionsOptions.md +1 -1
- package/docs/api/interfaces/UserEventAccess.md +1 -1
- package/docs/api/interfaces/UserMenuProps.md +1 -1
- package/docs/api/interfaces/UserProfile.md +1 -1
- package/docs/api/modules.md +318 -59
- package/docs/best-practices/performance.md +11 -0
- package/docs/implementation-guides/file-upload-storage.md +29 -0
- package/docs/rbac/README.md +2 -1
- package/docs/rbac/api-reference.md +11 -0
- package/docs/rbac/performance.md +320 -0
- package/docs/standards/01-architecture-standard.md +5 -0
- package/docs/standards/05-security-standard.md +12 -0
- package/package.json +1 -1
- package/src/components/AddressField/AddressField.test.tsx +411 -0
- package/src/components/AddressField/AddressField.tsx +323 -0
- package/src/components/AddressField/README.md +336 -0
- package/src/components/AddressField/index.ts +10 -0
- package/src/components/AddressField/types.ts +65 -0
- package/src/components/FileDisplay/FileDisplay.test.tsx +454 -0
- package/src/components/FileDisplay/FileDisplay.tsx +28 -1
- package/src/components/index.ts +2 -0
- package/src/hooks/__tests__/useFileDisplay.unit.test.ts +30 -5
- package/src/hooks/__tests__/useOrganisationSecurity.unit.test.tsx +11 -10
- package/src/hooks/__tests__/usePublicFileDisplay.test.ts +31 -6
- package/src/hooks/index.ts +6 -0
- package/src/hooks/public/usePublicFileDisplay.ts +8 -10
- package/src/hooks/useAddressAutocomplete.test.ts +318 -0
- package/src/hooks/useAddressAutocomplete.ts +268 -0
- package/src/hooks/useFileDisplay.ts +3 -15
- package/src/hooks/useFileReference.test.ts +20 -3
- package/src/hooks/useFileReference.ts +3 -24
- package/src/hooks/useFileUrlCache.ts +246 -0
- package/src/hooks/useInactivityTracker.ts +31 -20
- package/src/hooks/useOrganisationSecurity.test.ts +10 -7
- package/src/hooks/useOrganisationSecurity.ts +3 -3
- package/src/hooks/useQueryCache.ts +315 -0
- package/src/index.ts +2 -0
- package/src/providers/services/EventServiceProvider.tsx +4 -1
- package/src/rbac/api.test.ts +21 -6
- package/src/rbac/api.ts +32 -11
- package/src/rbac/audit-batched.ts +223 -0
- package/src/rbac/audit-enhanced.ts +2 -2
- package/src/rbac/audit.test.ts +6 -5
- package/src/rbac/audit.ts +34 -6
- package/src/rbac/cache-invalidation.ts +63 -12
- package/src/rbac/cache.test.ts +2 -2
- package/src/rbac/cache.ts +61 -14
- package/src/rbac/components/PagePermissionGuard.tsx +19 -10
- package/src/rbac/components/__tests__/PagePermissionGuard.performance.test.tsx +248 -0
- package/src/rbac/config.ts +9 -0
- package/src/rbac/engine.ts +2 -21
- package/src/rbac/hooks/usePermissions.ts +21 -5
- package/src/rbac/index.ts +19 -0
- package/src/rbac/performance.ts +210 -0
- package/src/rbac/request-deduplication.ts +87 -0
- package/src/rbac/utils/deep-equal.ts +93 -0
- package/src/types/file-reference.ts +0 -1
- package/src/utils/file-reference/__tests__/file-reference.test.ts +31 -4
- package/src/utils/file-reference/index.ts +44 -15
- package/src/utils/google-places/googlePlacesUtils.test.ts +403 -0
- package/src/utils/google-places/googlePlacesUtils.ts +475 -0
- package/src/utils/google-places/index.ts +26 -0
- package/src/utils/google-places/loadGoogleMapsScript.ts +207 -0
- package/src/utils/google-places/types.ts +94 -0
- package/src/utils/index.ts +23 -0
- package/src/utils/request-deduplication.ts +165 -0
- package/src/utils/storage/helpers.ts +143 -4
- package/dist/chunk-445GEP27.js.map +0 -1
- package/dist/chunk-FMUCXFII.js +0 -76
- package/dist/chunk-FMUCXFII.js.map +0 -1
- package/dist/chunk-FSFQFJCU.js.map +0 -1
- package/dist/chunk-FXFJRTKI.js.map +0 -1
- package/dist/chunk-GRIQLQ52.js.map +0 -1
- package/dist/chunk-HDCUMOOI.js.map +0 -1
- package/dist/chunk-OALXJH4Y.js.map +0 -1
- package/dist/chunk-TC7D3CR3.js.map +0 -1
- package/dist/chunk-U6WNSFX5.js.map +0 -1
- /package/dist/{DataTable-IX2NBUTP.js.map → DataTable-K3RJRSOX.js.map} +0 -0
- /package/dist/{UnifiedAuthProvider-A4BCQRJY.js.map → UnifiedAuthProvider-B76OWOAT.js.map} +0 -0
- /package/dist/{api-BMFCXVQX.js.map → api-YP7XD5L6.js.map} +0 -0
- /package/dist/{audit-WRS3KJKI.js.map → audit-B5P6FFIR.js.map} +0 -0
- /package/dist/{chunk-HGPQUCBC.js.map → chunk-FMTK4XNN.js.map} +0 -0
- /package/dist/{chunk-XAUHJD3L.js.map → chunk-K2JGDXGU.js.map} +0 -0
- /package/dist/{chunk-DAGICKHT.js.map → chunk-ULX5FYEM.js.map} +0 -0
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file AddressField Component
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/AddressField
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*
|
|
7
|
+
* Address input field with Google Places API autocomplete.
|
|
8
|
+
* Provides address suggestions, keyboard navigation, and accessibility.
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Google Places API autocomplete integration
|
|
12
|
+
* - Debounced input with caching
|
|
13
|
+
* - Keyboard navigation (Arrow keys, Enter, Escape)
|
|
14
|
+
* - Accessible ARIA attributes
|
|
15
|
+
* - Loading and error states
|
|
16
|
+
* - place_id storage for later retrieval
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import * as React from 'react';
|
|
20
|
+
import { Input } from '../Input/Input';
|
|
21
|
+
import { LoadingSpinner } from '../LoadingSpinner';
|
|
22
|
+
import { cn } from '../../utils/core/cn';
|
|
23
|
+
import { useAddressAutocomplete } from '../../hooks/useAddressAutocomplete';
|
|
24
|
+
import type { AddressFieldProps, AddressFieldRef } from './types';
|
|
25
|
+
import type { ParsedAddress } from '../../utils/google-places';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* AddressField component
|
|
29
|
+
*
|
|
30
|
+
* A production-ready address input field with Google Places API autocomplete.
|
|
31
|
+
* Returns structured address data including place_id for later retrieval.
|
|
32
|
+
*
|
|
33
|
+
* @param props - AddressField configuration
|
|
34
|
+
* @param ref - Forwarded ref for imperative access
|
|
35
|
+
* @returns JSX.Element - The rendered address field
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```tsx
|
|
39
|
+
* <AddressField
|
|
40
|
+
* apiKey={apiKey}
|
|
41
|
+
* onChange={(address) => {
|
|
42
|
+
* // address includes place_id, full_address, lat, lng, etc.
|
|
43
|
+
* console.log(address.place_id);
|
|
44
|
+
* }}
|
|
45
|
+
* placeholder="Enter your address"
|
|
46
|
+
* />
|
|
47
|
+
* ```
|
|
48
|
+
*/
|
|
49
|
+
const AddressField = React.forwardRef<HTMLInputElement, AddressFieldProps>(
|
|
50
|
+
(
|
|
51
|
+
{
|
|
52
|
+
apiKey,
|
|
53
|
+
value: controlledValue,
|
|
54
|
+
defaultValue = '',
|
|
55
|
+
onChange,
|
|
56
|
+
onInputChange,
|
|
57
|
+
placeholder = 'Enter address',
|
|
58
|
+
error,
|
|
59
|
+
disabled,
|
|
60
|
+
className,
|
|
61
|
+
size = 'md',
|
|
62
|
+
variant = 'default',
|
|
63
|
+
autocompleteOptions,
|
|
64
|
+
debounceDelay,
|
|
65
|
+
cacheEnabled = true,
|
|
66
|
+
cacheTTL,
|
|
67
|
+
...props
|
|
68
|
+
},
|
|
69
|
+
ref
|
|
70
|
+
) => {
|
|
71
|
+
const [internalValue, setInternalValue] = React.useState(defaultValue);
|
|
72
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
73
|
+
const [selectedIndex, setSelectedIndex] = React.useState(-1);
|
|
74
|
+
const [inputFocused, setInputFocused] = React.useState(false);
|
|
75
|
+
|
|
76
|
+
const inputRef = React.useRef<HTMLInputElement>(null);
|
|
77
|
+
const suggestionsRef = React.useRef<HTMLUListElement>(null);
|
|
78
|
+
const containerRef = React.useRef<HTMLDivElement>(null);
|
|
79
|
+
|
|
80
|
+
// Use controlled or uncontrolled value
|
|
81
|
+
const value = controlledValue !== undefined ? controlledValue : internalValue;
|
|
82
|
+
|
|
83
|
+
// Use autocomplete hook
|
|
84
|
+
const { suggestions, isLoading, error: autocompleteError, selectAddress, clearSuggestions } =
|
|
85
|
+
useAddressAutocomplete(apiKey, value, {
|
|
86
|
+
autocompleteOptions,
|
|
87
|
+
debounceDelay,
|
|
88
|
+
cacheEnabled,
|
|
89
|
+
cacheTTL,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// Update suggestions visibility
|
|
93
|
+
React.useEffect(() => {
|
|
94
|
+
if (suggestions.length > 0 && inputFocused && value.trim()) {
|
|
95
|
+
setIsOpen(true);
|
|
96
|
+
} else if (suggestions.length === 0 || !value.trim()) {
|
|
97
|
+
setIsOpen(false);
|
|
98
|
+
}
|
|
99
|
+
}, [suggestions, inputFocused, value]);
|
|
100
|
+
|
|
101
|
+
// Handle input change
|
|
102
|
+
const handleInputChange = React.useCallback(
|
|
103
|
+
(e: React.ChangeEvent<HTMLInputElement>) => {
|
|
104
|
+
const newValue = e.target.value;
|
|
105
|
+
if (controlledValue === undefined) {
|
|
106
|
+
setInternalValue(newValue);
|
|
107
|
+
}
|
|
108
|
+
onInputChange?.(newValue);
|
|
109
|
+
setSelectedIndex(-1);
|
|
110
|
+
if (!newValue.trim()) {
|
|
111
|
+
onChange?.(null);
|
|
112
|
+
clearSuggestions();
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
[controlledValue, onInputChange, onChange, clearSuggestions]
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
// Handle address selection
|
|
119
|
+
const handleSelectAddress = React.useCallback(
|
|
120
|
+
async (placeId: string) => {
|
|
121
|
+
setIsOpen(false);
|
|
122
|
+
setSelectedIndex(-1);
|
|
123
|
+
const address = await selectAddress(placeId);
|
|
124
|
+
if (address) {
|
|
125
|
+
// Update input with formatted address
|
|
126
|
+
const displayValue = address.full_address || '';
|
|
127
|
+
if (controlledValue === undefined) {
|
|
128
|
+
setInternalValue(displayValue);
|
|
129
|
+
}
|
|
130
|
+
onInputChange?.(displayValue);
|
|
131
|
+
onChange?.(address);
|
|
132
|
+
}
|
|
133
|
+
inputRef.current?.blur();
|
|
134
|
+
},
|
|
135
|
+
[selectAddress, onChange, onInputChange, controlledValue]
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
// Handle keyboard navigation
|
|
139
|
+
const handleKeyDown = React.useCallback(
|
|
140
|
+
(e: React.KeyboardEvent<HTMLInputElement>) => {
|
|
141
|
+
if (!isOpen || suggestions.length === 0) {
|
|
142
|
+
if (e.key === 'Escape') {
|
|
143
|
+
setIsOpen(false);
|
|
144
|
+
inputRef.current?.blur();
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
switch (e.key) {
|
|
150
|
+
case 'ArrowDown':
|
|
151
|
+
e.preventDefault();
|
|
152
|
+
setSelectedIndex((prev) => (prev < suggestions.length - 1 ? prev + 1 : prev));
|
|
153
|
+
break;
|
|
154
|
+
case 'ArrowUp':
|
|
155
|
+
e.preventDefault();
|
|
156
|
+
setSelectedIndex((prev) => (prev > 0 ? prev - 1 : -1));
|
|
157
|
+
break;
|
|
158
|
+
case 'Enter':
|
|
159
|
+
e.preventDefault();
|
|
160
|
+
if (selectedIndex >= 0 && selectedIndex < suggestions.length) {
|
|
161
|
+
handleSelectAddress(suggestions[selectedIndex].place_id);
|
|
162
|
+
}
|
|
163
|
+
break;
|
|
164
|
+
case 'Escape':
|
|
165
|
+
e.preventDefault();
|
|
166
|
+
setIsOpen(false);
|
|
167
|
+
setSelectedIndex(-1);
|
|
168
|
+
inputRef.current?.blur();
|
|
169
|
+
break;
|
|
170
|
+
case 'Tab':
|
|
171
|
+
setIsOpen(false);
|
|
172
|
+
setSelectedIndex(-1);
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
[isOpen, suggestions, selectedIndex, handleSelectAddress]
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
// Handle focus
|
|
180
|
+
const handleFocus = React.useCallback(() => {
|
|
181
|
+
setInputFocused(true);
|
|
182
|
+
if (suggestions.length > 0 && value.trim()) {
|
|
183
|
+
setIsOpen(true);
|
|
184
|
+
}
|
|
185
|
+
}, [suggestions, value]);
|
|
186
|
+
|
|
187
|
+
// Handle blur
|
|
188
|
+
const handleBlur = React.useCallback(
|
|
189
|
+
(e: React.FocusEvent<HTMLInputElement>) => {
|
|
190
|
+
// Delay to allow click events on suggestions
|
|
191
|
+
setTimeout(() => {
|
|
192
|
+
if (!containerRef.current?.contains(document.activeElement)) {
|
|
193
|
+
setInputFocused(false);
|
|
194
|
+
setIsOpen(false);
|
|
195
|
+
setSelectedIndex(-1);
|
|
196
|
+
}
|
|
197
|
+
}, 200);
|
|
198
|
+
},
|
|
199
|
+
[]
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
// Click outside handler
|
|
203
|
+
React.useEffect(() => {
|
|
204
|
+
const handleClickOutside = (event: MouseEvent) => {
|
|
205
|
+
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
206
|
+
setIsOpen(false);
|
|
207
|
+
setSelectedIndex(-1);
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
if (isOpen) {
|
|
212
|
+
document.addEventListener('mousedown', handleClickOutside);
|
|
213
|
+
return () => {
|
|
214
|
+
document.removeEventListener('mousedown', handleClickOutside);
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
}, [isOpen]);
|
|
218
|
+
|
|
219
|
+
// Scroll selected item into view
|
|
220
|
+
React.useEffect(() => {
|
|
221
|
+
if (selectedIndex >= 0 && suggestionsRef.current) {
|
|
222
|
+
const selectedItem = suggestionsRef.current.children[selectedIndex] as HTMLElement;
|
|
223
|
+
if (selectedItem && typeof selectedItem.scrollIntoView === 'function') {
|
|
224
|
+
selectedItem.scrollIntoView({ block: 'nearest', behavior: 'smooth' });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}, [selectedIndex]);
|
|
228
|
+
|
|
229
|
+
// Combine refs
|
|
230
|
+
React.useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);
|
|
231
|
+
|
|
232
|
+
const suggestionsId = React.useId();
|
|
233
|
+
const hasError = error || !!autocompleteError;
|
|
234
|
+
|
|
235
|
+
return (
|
|
236
|
+
<div ref={containerRef} className={cn('relative w-full', className)}>
|
|
237
|
+
<div className="relative">
|
|
238
|
+
<Input
|
|
239
|
+
ref={inputRef}
|
|
240
|
+
type="text"
|
|
241
|
+
value={value}
|
|
242
|
+
onChange={handleInputChange}
|
|
243
|
+
onKeyDown={handleKeyDown}
|
|
244
|
+
onFocus={handleFocus}
|
|
245
|
+
onBlur={handleBlur}
|
|
246
|
+
placeholder={placeholder}
|
|
247
|
+
disabled={disabled}
|
|
248
|
+
error={hasError}
|
|
249
|
+
size={size}
|
|
250
|
+
variant={variant}
|
|
251
|
+
role="combobox"
|
|
252
|
+
aria-expanded={isOpen}
|
|
253
|
+
aria-autocomplete="list"
|
|
254
|
+
aria-controls={suggestionsId}
|
|
255
|
+
aria-haspopup="listbox"
|
|
256
|
+
aria-activedescendant={
|
|
257
|
+
selectedIndex >= 0 ? `${suggestionsId}-item-${selectedIndex}` : undefined
|
|
258
|
+
}
|
|
259
|
+
{...props}
|
|
260
|
+
/>
|
|
261
|
+
{isLoading && (
|
|
262
|
+
<div className="absolute right-3 top-1/2 -translate-y-1/2 pointer-events-none">
|
|
263
|
+
<LoadingSpinner size="sm" />
|
|
264
|
+
</div>
|
|
265
|
+
)}
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
{isOpen && suggestions.length > 0 && (
|
|
269
|
+
<ul
|
|
270
|
+
ref={suggestionsRef}
|
|
271
|
+
id={suggestionsId}
|
|
272
|
+
role="listbox"
|
|
273
|
+
className={cn(
|
|
274
|
+
'absolute z-[99999] w-full mt-1 max-h-60 overflow-y-auto',
|
|
275
|
+
'border border-main-300 bg-main-50 shadow-lg rounded-md',
|
|
276
|
+
'list-none p-0 m-0'
|
|
277
|
+
)}
|
|
278
|
+
data-testid="address-suggestions"
|
|
279
|
+
>
|
|
280
|
+
{suggestions.map((suggestion, index) => (
|
|
281
|
+
<li
|
|
282
|
+
key={suggestion.place_id}
|
|
283
|
+
id={`${suggestionsId}-item-${index}`}
|
|
284
|
+
role="option"
|
|
285
|
+
aria-selected={selectedIndex === index}
|
|
286
|
+
className={cn(
|
|
287
|
+
'px-3 py-2 cursor-pointer text-sm',
|
|
288
|
+
'hover:bg-main-100 focus:bg-main-100',
|
|
289
|
+
'border-b border-main-200 last:border-b-0',
|
|
290
|
+
selectedIndex === index && 'bg-main-100'
|
|
291
|
+
)}
|
|
292
|
+
onClick={() => handleSelectAddress(suggestion.place_id)}
|
|
293
|
+
onMouseEnter={() => setSelectedIndex(index)}
|
|
294
|
+
data-testid={`address-suggestion-${index}`}
|
|
295
|
+
>
|
|
296
|
+
<div className="font-medium text-main-900">
|
|
297
|
+
{suggestion.structured_formatting?.main_text || suggestion.description}
|
|
298
|
+
</div>
|
|
299
|
+
{suggestion.structured_formatting?.secondary_text && (
|
|
300
|
+
<div className="text-xs text-main-600 mt-0.5">
|
|
301
|
+
{suggestion.structured_formatting.secondary_text}
|
|
302
|
+
</div>
|
|
303
|
+
)}
|
|
304
|
+
</li>
|
|
305
|
+
))}
|
|
306
|
+
</ul>
|
|
307
|
+
)}
|
|
308
|
+
|
|
309
|
+
{autocompleteError && (
|
|
310
|
+
<p className="mt-1 text-sm text-destructive" role="alert">
|
|
311
|
+
{autocompleteError.message}
|
|
312
|
+
</p>
|
|
313
|
+
)}
|
|
314
|
+
</div>
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
AddressField.displayName = 'AddressField';
|
|
320
|
+
|
|
321
|
+
export { AddressField };
|
|
322
|
+
export type { AddressFieldProps, AddressFieldRef, ParsedAddress } from './types';
|
|
323
|
+
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
# AddressField Component
|
|
2
|
+
|
|
3
|
+
A production-ready address input field with Google Places API autocomplete integration. Provides address suggestions, keyboard navigation, caching for cost optimization, and returns structured address data including `place_id` for later retrieval.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Google Places API Integration**: Real-time address autocomplete suggestions
|
|
8
|
+
- **Cost Optimization**: Built-in caching (1hr for autocomplete, 24hr for place details)
|
|
9
|
+
- **place_id Storage**: Always returns `place_id` for efficient address retrieval
|
|
10
|
+
- **Keyboard Navigation**: Full keyboard support (Arrow keys, Enter, Escape)
|
|
11
|
+
- **Accessibility**: WCAG compliant with proper ARIA attributes
|
|
12
|
+
- **Error Handling**: Graceful error handling with user-friendly messages
|
|
13
|
+
- **Loading States**: Visual feedback during API calls
|
|
14
|
+
- **TypeScript**: Fully typed with comprehensive type definitions
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
The component is part of `@jmruthers/pace-core`. No additional installation required.
|
|
19
|
+
|
|
20
|
+
## Setup
|
|
21
|
+
|
|
22
|
+
### Google Cloud Console Configuration
|
|
23
|
+
|
|
24
|
+
1. **Enable the Places API** in your Google Cloud Console
|
|
25
|
+
2. **Create an API Key** with the following restrictions:
|
|
26
|
+
- **Application restrictions**: HTTP referrers (web sites)
|
|
27
|
+
- **Website restrictions**: Add your domain(s) and `http://localhost:*` for local development
|
|
28
|
+
- Example: `http://localhost:8091/*`
|
|
29
|
+
- Example: `https://yourdomain.com/*`
|
|
30
|
+
3. **API restrictions**: Restrict to "Places API" only (recommended for security)
|
|
31
|
+
|
|
32
|
+
### Environment Variable
|
|
33
|
+
|
|
34
|
+
Set your API key in your `.env` file:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
VITE_GOOGLE_PLACES_API_KEY=your-api-key-here
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### Basic Example
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import { AddressField } from '@jmruthers/pace-core';
|
|
46
|
+
|
|
47
|
+
function MyForm() {
|
|
48
|
+
const [address, setAddress] = useState(null);
|
|
49
|
+
const apiKey = import.meta.env.VITE_GOOGLE_PLACES_API_KEY;
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<AddressField
|
|
53
|
+
apiKey={apiKey}
|
|
54
|
+
onChange={(parsedAddress) => {
|
|
55
|
+
setAddress(parsedAddress);
|
|
56
|
+
// parsedAddress includes place_id, full_address, lat, lng, etc.
|
|
57
|
+
console.log('Selected place_id:', parsedAddress.place_id);
|
|
58
|
+
}}
|
|
59
|
+
placeholder="Enter your address"
|
|
60
|
+
/>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Controlled Input
|
|
66
|
+
|
|
67
|
+
```tsx
|
|
68
|
+
function ControlledExample() {
|
|
69
|
+
const [inputValue, setInputValue] = useState('');
|
|
70
|
+
const [address, setAddress] = useState(null);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<AddressField
|
|
74
|
+
apiKey={apiKey}
|
|
75
|
+
value={inputValue}
|
|
76
|
+
onInputChange={setInputValue}
|
|
77
|
+
onChange={setAddress}
|
|
78
|
+
/>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### With Country Restriction
|
|
84
|
+
|
|
85
|
+
```tsx
|
|
86
|
+
<AddressField
|
|
87
|
+
apiKey={apiKey}
|
|
88
|
+
onChange={handleAddressChange}
|
|
89
|
+
autocompleteOptions={{
|
|
90
|
+
components: 'country:au', // Restrict to Australia
|
|
91
|
+
}}
|
|
92
|
+
/>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### With Custom Cache Settings
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
<AddressField
|
|
99
|
+
apiKey={apiKey}
|
|
100
|
+
onChange={handleAddressChange}
|
|
101
|
+
cacheEnabled={true}
|
|
102
|
+
cacheTTL={{
|
|
103
|
+
autocomplete: 1800, // 30 minutes
|
|
104
|
+
placeDetails: 43200, // 12 hours
|
|
105
|
+
}}
|
|
106
|
+
/>
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Retrieving Address from Stored place_id
|
|
110
|
+
|
|
111
|
+
If you've stored a `place_id` in your database, you can retrieve the full address later:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
import { getAddressByPlaceId } from '@jmruthers/pace-core/utils/google-places';
|
|
115
|
+
|
|
116
|
+
// Later, retrieve address from stored place_id
|
|
117
|
+
const address = await getAddressByPlaceId(storedPlaceId, apiKey);
|
|
118
|
+
if (address) {
|
|
119
|
+
console.log(address.full_address);
|
|
120
|
+
console.log(address.lat, address.lng);
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Props
|
|
125
|
+
|
|
126
|
+
### AddressFieldProps
|
|
127
|
+
|
|
128
|
+
| Prop | Type | Default | Description |
|
|
129
|
+
|------|------|---------|-------------|
|
|
130
|
+
| `apiKey` | `string` | **required** | Google Places API key |
|
|
131
|
+
| `value` | `string` | - | Controlled input value |
|
|
132
|
+
| `defaultValue` | `string` | `''` | Uncontrolled default value |
|
|
133
|
+
| `onChange` | `(address: ParsedAddress \| null) => void` | - | Callback when address is selected |
|
|
134
|
+
| `onInputChange` | `(value: string) => void` | - | Callback when input value changes |
|
|
135
|
+
| `placeholder` | `string` | `'Enter address'` | Placeholder text |
|
|
136
|
+
| `error` | `boolean` | `false` | Error state styling |
|
|
137
|
+
| `disabled` | `boolean` | `false` | Disabled state |
|
|
138
|
+
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Input size |
|
|
139
|
+
| `variant` | `'default' \| 'destructive'` | `'default'` | Input variant |
|
|
140
|
+
| `autocompleteOptions` | `AutocompleteOptions` | - | Google Places API options |
|
|
141
|
+
| `debounceDelay` | `number` | `300` | Debounce delay in milliseconds |
|
|
142
|
+
| `cacheEnabled` | `boolean` | `true` | Enable caching |
|
|
143
|
+
| `cacheTTL` | `{ autocomplete?: number; placeDetails?: number }` | `{ autocomplete: 3600, placeDetails: 86400 }` | Cache TTL in seconds |
|
|
144
|
+
|
|
145
|
+
### AutocompleteOptions
|
|
146
|
+
|
|
147
|
+
| Option | Type | Description |
|
|
148
|
+
|--------|------|-------------|
|
|
149
|
+
| `components` | `string` | Restrict results to specific countries (e.g., `'country:au'`) |
|
|
150
|
+
| `location` | `string` | Location bias (e.g., `'-37.8136,144.9631'`) |
|
|
151
|
+
| `radius` | `number` | Radius in meters for location bias |
|
|
152
|
+
| `types` | `string` | Restrict results to specific place types |
|
|
153
|
+
| `language` | `string` | Language code for results (e.g., `'en'`) |
|
|
154
|
+
|
|
155
|
+
## ParsedAddress Type
|
|
156
|
+
|
|
157
|
+
The `onChange` callback receives a `ParsedAddress` object matching the `pace_address` table structure:
|
|
158
|
+
|
|
159
|
+
```typescript
|
|
160
|
+
interface ParsedAddress {
|
|
161
|
+
place_id: string; // Always included - stable identifier
|
|
162
|
+
full_address: string | null;
|
|
163
|
+
street_number: string | null;
|
|
164
|
+
route: string | null;
|
|
165
|
+
suburb: string | null;
|
|
166
|
+
state: string | null;
|
|
167
|
+
postcode: string | null;
|
|
168
|
+
country: string | null;
|
|
169
|
+
lat: number | null;
|
|
170
|
+
lng: number | null;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Caching Strategy
|
|
175
|
+
|
|
176
|
+
The component uses intelligent caching to reduce API costs:
|
|
177
|
+
|
|
178
|
+
- **Autocomplete Results**: Cached for 1 hour (default) by query string
|
|
179
|
+
- **Place Details**: Cached for 24 hours (default) by `place_id`
|
|
180
|
+
- **Request Deduplication**: Prevents duplicate simultaneous requests
|
|
181
|
+
|
|
182
|
+
### Why This Matters
|
|
183
|
+
|
|
184
|
+
- **Cost Savings**: Repeated searches for the same address don't trigger new API calls
|
|
185
|
+
- **Performance**: Cached results are returned instantly
|
|
186
|
+
- **place_id Storage**: Store `place_id` in your database to retrieve addresses later without autocomplete searches
|
|
187
|
+
|
|
188
|
+
### Cache Configuration
|
|
189
|
+
|
|
190
|
+
```tsx
|
|
191
|
+
<AddressField
|
|
192
|
+
apiKey={apiKey}
|
|
193
|
+
cacheEnabled={true} // Enable/disable caching
|
|
194
|
+
cacheTTL={{
|
|
195
|
+
autocomplete: 3600, // 1 hour in seconds
|
|
196
|
+
placeDetails: 86400, // 24 hours in seconds
|
|
197
|
+
}}
|
|
198
|
+
/>
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## place_id Usage
|
|
202
|
+
|
|
203
|
+
The `place_id` is a stable identifier for a location that doesn't change. It's crucial for:
|
|
204
|
+
|
|
205
|
+
1. **Storing in Database**: Save `place_id` in your `pace_address` table
|
|
206
|
+
2. **Retrieving Later**: Use `getAddressByPlaceId()` to get full address without autocomplete
|
|
207
|
+
3. **Verification**: Verify addresses haven't changed
|
|
208
|
+
4. **Cost Efficiency**: Place details lookups are cheaper than autocomplete searches
|
|
209
|
+
|
|
210
|
+
### Example: Storing and Retrieving
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
// When user selects an address
|
|
214
|
+
<AddressField
|
|
215
|
+
apiKey={apiKey}
|
|
216
|
+
onChange={async (address) => {
|
|
217
|
+
// Store in database
|
|
218
|
+
await supabase.from('pace_address').insert({
|
|
219
|
+
place_id: address.place_id, // Store this!
|
|
220
|
+
full_address: address.full_address,
|
|
221
|
+
lat: address.lat,
|
|
222
|
+
lng: address.lng,
|
|
223
|
+
// ... other fields
|
|
224
|
+
});
|
|
225
|
+
}}
|
|
226
|
+
/>
|
|
227
|
+
|
|
228
|
+
// Later, retrieve from stored place_id
|
|
229
|
+
const storedPlaceId = 'ChIJ123...';
|
|
230
|
+
const address = await getAddressByPlaceId(storedPlaceId, apiKey);
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
## Keyboard Navigation
|
|
234
|
+
|
|
235
|
+
- **Arrow Down**: Navigate to next suggestion
|
|
236
|
+
- **Arrow Up**: Navigate to previous suggestion
|
|
237
|
+
- **Enter**: Select highlighted suggestion
|
|
238
|
+
- **Escape**: Close suggestions dropdown
|
|
239
|
+
- **Tab**: Close suggestions and move to next field
|
|
240
|
+
|
|
241
|
+
## Accessibility
|
|
242
|
+
|
|
243
|
+
The component follows WCAG 2.1 guidelines:
|
|
244
|
+
|
|
245
|
+
- **ARIA Attributes**: Proper `role`, `aria-expanded`, `aria-autocomplete`, `aria-controls`
|
|
246
|
+
- **Keyboard Navigation**: Full keyboard support
|
|
247
|
+
- **Screen Readers**: Proper announcements and labels
|
|
248
|
+
- **Focus Management**: Proper focus handling
|
|
249
|
+
|
|
250
|
+
## Error Handling
|
|
251
|
+
|
|
252
|
+
The component handles various error scenarios:
|
|
253
|
+
|
|
254
|
+
- **Network Errors**: Displays user-friendly error message
|
|
255
|
+
- **API Key Errors**: Clear error about API key configuration
|
|
256
|
+
- **Rate Limiting**: Informs user about quota limits
|
|
257
|
+
- **Invalid Requests**: Validates input and displays errors
|
|
258
|
+
|
|
259
|
+
## Integration with pace_address Table
|
|
260
|
+
|
|
261
|
+
The `ParsedAddress` type matches the `pace_address` table structure, making it easy to store results:
|
|
262
|
+
|
|
263
|
+
```tsx
|
|
264
|
+
<AddressField
|
|
265
|
+
apiKey={apiKey}
|
|
266
|
+
onChange={async (address) => {
|
|
267
|
+
const { data, error } = await supabase
|
|
268
|
+
.from('pace_address')
|
|
269
|
+
.insert({
|
|
270
|
+
place_id: address.place_id,
|
|
271
|
+
full_address: address.full_address,
|
|
272
|
+
street_number: address.street_number,
|
|
273
|
+
route: address.route,
|
|
274
|
+
suburb: address.suburb,
|
|
275
|
+
state: address.state,
|
|
276
|
+
postcode: address.postcode,
|
|
277
|
+
country: address.country,
|
|
278
|
+
lat: address.lat,
|
|
279
|
+
lng: address.lng,
|
|
280
|
+
organisation_id: currentOrganisationId,
|
|
281
|
+
});
|
|
282
|
+
}}
|
|
283
|
+
/>
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## API Key Setup
|
|
287
|
+
|
|
288
|
+
1. Get a Google Places API key from [Google Cloud Console](https://console.cloud.google.com/)
|
|
289
|
+
2. Enable the following APIs:
|
|
290
|
+
- Places API
|
|
291
|
+
- Places API (New)
|
|
292
|
+
3. Set up API key restrictions (recommended)
|
|
293
|
+
4. Add to your environment variables:
|
|
294
|
+
|
|
295
|
+
```env
|
|
296
|
+
VITE_GOOGLE_PLACES_API_KEY=your-api-key-here
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Best Practices
|
|
300
|
+
|
|
301
|
+
1. **Store place_id**: Always save `place_id` in your database for later retrieval
|
|
302
|
+
2. **Enable Caching**: Keep caching enabled to reduce API costs
|
|
303
|
+
3. **Handle Errors**: Implement proper error handling in your `onChange` callback
|
|
304
|
+
4. **Country Restrictions**: Use `autocompleteOptions.components` to restrict to specific countries
|
|
305
|
+
5. **Debounce Delay**: Adjust `debounceDelay` based on your needs (default 300ms is usually good)
|
|
306
|
+
|
|
307
|
+
## Troubleshooting
|
|
308
|
+
|
|
309
|
+
### No suggestions appearing
|
|
310
|
+
|
|
311
|
+
- Check API key is valid and has Places API enabled
|
|
312
|
+
- Verify API key has billing enabled
|
|
313
|
+
- Check browser console for error messages
|
|
314
|
+
- Ensure input has at least 2-3 characters
|
|
315
|
+
|
|
316
|
+
### Suggestions not closing
|
|
317
|
+
|
|
318
|
+
- Check if there are click handlers preventing blur events
|
|
319
|
+
- Verify `onBlur` is not being prevented
|
|
320
|
+
|
|
321
|
+
### Caching not working
|
|
322
|
+
|
|
323
|
+
- Ensure `cacheEnabled` is `true` (default)
|
|
324
|
+
- Check that `useQueryCache` hook is working properly
|
|
325
|
+
|
|
326
|
+
## Related Components
|
|
327
|
+
|
|
328
|
+
- `Input` - Base input component used by AddressField
|
|
329
|
+
- `Select` - Similar dropdown pattern for reference
|
|
330
|
+
|
|
331
|
+
## Related Utilities
|
|
332
|
+
|
|
333
|
+
- `getAddressByPlaceId` - Retrieve address from stored `place_id`
|
|
334
|
+
- `fetchPlaceAutocomplete` - Direct API access (if needed)
|
|
335
|
+
- `fetchPlaceDetails` - Direct API access (if needed)
|
|
336
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file AddressField Component Exports
|
|
3
|
+
* @package @jmruthers/pace-core
|
|
4
|
+
* @module Components/AddressField
|
|
5
|
+
* @since 0.1.0
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { AddressField } from './AddressField';
|
|
9
|
+
export type { AddressFieldProps, AddressFieldRef, ParsedAddress, AutocompleteOptions } from './types';
|
|
10
|
+
|