@@ -44,6 +44,14 @@ const LocationMapPicker: React.FC<LocationMapPickerProps> = ({
4444 const mapRef = useRef < google . maps . Map | null > ( null ) ; // Use a ref for the map instance
4545 const autocompleteRef = useRef < google . maps . places . Autocomplete | null > ( null ) ;
4646 const placeListenerRef = useRef < google . maps . MapsEventListener | null > ( null ) ;
47+ // New Autocomplete Element (2025) support
48+ const placeElRef = useRef < any > ( null ) ;
49+ const placeElContainerRef = useRef < HTMLDivElement | null > ( null ) ;
50+ const placeElHandlerRef = useRef < ( ( e : any ) => void ) | null > ( null ) ;
51+ const placesServiceRef = useRef < google . maps . places . PlacesService | null > (
52+ null ,
53+ ) ;
54+ const [ useNewAutocomplete , setUseNewAutocomplete ] = useState < boolean > ( false ) ;
4755 const geocoderRef = useRef < google . maps . Geocoder | null > ( null ) ;
4856 const inputRef = useRef < HTMLInputElement | null > ( null ) ;
4957
@@ -225,13 +233,152 @@ const LocationMapPicker: React.FC<LocationMapPickerProps> = ({
225233 }
226234 } ;
227235
228- // Initialize native Places Autocomplete on the input (no @react-google-maps/api wrapper)
236+ // Handler for new PlaceAutocompleteElement selections
237+ const onPlaceElementChanged = useCallback ( ( ) => {
238+ const g : any = ( window as any ) . google ;
239+ const el = placeElRef . current ;
240+ if ( ! g ?. maps ?. places || ! el ) return ;
241+ // Try to get place directly; fallback to value.place/placeId
242+ let place : any = null ;
243+ try {
244+ if ( typeof el . getPlace === "function" ) {
245+ place = el . getPlace ( ) ;
246+ }
247+ if ( ! place && el . value ) {
248+ place = el . value ?. place || el . value ;
249+ }
250+ } catch { }
251+
252+ const processPlace = ( p : any ) => {
253+ if ( ! p || ! p . geometry || ! p . geometry . location ) return ;
254+ const rawName = p . name || "" ;
255+ const getAddressComponent = ( type : string ) : string => {
256+ if ( ! p . address_components ) return "" ;
257+ const comp = p . address_components . find ( ( c : any ) =>
258+ c . types . includes ( type ) ,
259+ ) ;
260+ return comp ? comp . long_name : "" ;
261+ } ;
262+ const route = getAddressComponent ( "route" ) ;
263+ const barangay =
264+ getAddressComponent ( "sublocality_level_1" ) ||
265+ getAddressComponent ( "sublocality" ) ||
266+ getAddressComponent ( "neighborhood" ) ;
267+ const city =
268+ getAddressComponent ( "locality" ) ||
269+ getAddressComponent ( "administrative_area_level_2" ) ;
270+ const province = getAddressComponent ( "administrative_area_level_1" ) ;
271+
272+ let displayAddress = composeFormattedWithPlace (
273+ rawName ,
274+ p . formatted_address ,
275+ ) ;
276+ if ( ! displayAddress ) {
277+ displayAddress = `${ p . geometry . location . lat ( ) . toFixed ( 5 ) } , ${ p . geometry . location
278+ . lng ( )
279+ . toFixed ( 5 ) } `;
280+ }
281+ const structured : StructuredLocation = {
282+ lat : p . geometry . location . lat ( ) ,
283+ lng : p . geometry . location . lng ( ) ,
284+ address : displayAddress ,
285+ rawName : rawName || undefined ,
286+ route : route || undefined ,
287+ barangay : barangay || undefined ,
288+ city : city || undefined ,
289+ province : province || undefined ,
290+ } ;
291+ setInternalPosition ( { lat : structured . lat , lng : structured . lng } ) ;
292+ if ( inputRef . current && ! useNewAutocomplete )
293+ inputRef . current . value = structured . address ;
294+ onChange ( structured ) ;
295+ persistLocation ( structured ) ;
296+ mapRef . current ?. panTo ( { lat : structured . lat , lng : structured . lng } ) ;
297+ mapRef . current ?. setZoom ( 17 ) ;
298+ } ;
299+
300+ if ( place && place . geometry && place . geometry . location ) {
301+ processPlace ( place ) ;
302+ return ;
303+ }
304+ // If we only have a place_id (common with prediction), fetch details
305+ const placeId =
306+ place ?. place_id || el . placeId || el . value ?. placeId || el . value ?. place_id ;
307+ if ( placeId ) {
308+ try {
309+ if ( ! placesServiceRef . current ) {
310+ placesServiceRef . current = new g . maps . places . PlacesService (
311+ document . createElement ( "div" ) ,
312+ ) ;
313+ }
314+ if ( ! placesServiceRef . current ) return ;
315+ placesServiceRef . current . getDetails (
316+ {
317+ placeId,
318+ fields : [
319+ "geometry" ,
320+ "name" ,
321+ "formatted_address" ,
322+ "address_components" ,
323+ ] ,
324+ } ,
325+ ( res : any , status : any ) => {
326+ if ( status === g . maps . places . PlacesServiceStatus . OK && res ) {
327+ processPlace ( res ) ;
328+ }
329+ } ,
330+ ) ;
331+ } catch { }
332+ }
333+ } , [ onChange , persistLocation , useNewAutocomplete ] ) ;
334+
335+ // Initialize Places Autocomplete, preferring the new PlaceAutocompleteElement when available
229336 useEffect ( ( ) => {
230337 let intervalId : number | null = null ;
231338 const init = ( ) => {
232- if ( autocompleteRef . current || ! inputRef . current ) return false ;
233339 const g = ( window as any ) . google ;
234340 if ( ! g ?. maps ?. places ) return false ;
341+ // Try new element first
342+ try {
343+ if (
344+ g . maps . places . PlaceAutocompleteElement &&
345+ placeElContainerRef . current
346+ ) {
347+ // Create and attach the new element
348+ const el = new g . maps . places . PlaceAutocompleteElement ( ) ;
349+ placeElRef . current = el ;
350+ // Optional: bias types to geocode-like results
351+ try {
352+ el . types = [ "geocode" ] ; // best-effort; ignored if not supported
353+ } catch { }
354+ // Region restriction (Philippines) and initial bias around current position
355+ try {
356+ // New element uses 'countries' for restriction
357+ ( el as any ) . countries = [ "ph" ] ; // ISO 3166-1 alpha-2
358+ } catch { }
359+ try {
360+ // Bias searches around the current internalPosition (~80km radius)
361+ ( el as any ) . locationBias = {
362+ center : { lat : internalPosition . lat , lng : internalPosition . lng } ,
363+ radius : 5500 ,
364+ } as any ;
365+ } catch { }
366+ // Wire selection listener
367+ const handler = ( ) => onPlaceElementChanged ( ) ;
368+ placeElHandlerRef . current = handler ;
369+ // Some builds dispatch 'place_changed', others 'gmpxplacechanged'; listen to both
370+ el . addEventListener ?.( "place_changed" , handler ) ;
371+ el . addEventListener ?.( "gmpxplacechanged" , handler ) ;
372+ // Mount element into container
373+ placeElContainerRef . current . innerHTML = "" ;
374+ placeElContainerRef . current . appendChild ( el ) ;
375+ setUseNewAutocomplete ( true ) ;
376+ return true ;
377+ }
378+ } catch { }
379+
380+ // Fallback: legacy Autocomplete bound to our input
381+ if ( autocompleteRef . current || ! inputRef . current ) return false ;
235382 try {
236383 autocompleteRef . current = new g . maps . places . Autocomplete (
237384 inputRef . current ,
@@ -243,6 +390,7 @@ const LocationMapPicker: React.FC<LocationMapPickerProps> = ({
243390 "address_components" ,
244391 ] ,
245392 types : [ "geocode" ] ,
393+ componentRestrictions : { country : [ "ph" ] } ,
246394 // You can add componentRestrictions here if needed
247395 } ,
248396 ) ;
@@ -270,12 +418,67 @@ const LocationMapPicker: React.FC<LocationMapPickerProps> = ({
270418 } , 200 ) ;
271419 }
272420 return ( ) => {
421+ // Cleanup legacy autocomplete listener
273422 if ( placeListenerRef . current ) {
274423 placeListenerRef . current . remove ( ) ;
275424 placeListenerRef . current = null ;
276425 }
426+ // Cleanup new element listeners and DOM
427+ if ( placeElRef . current && placeElHandlerRef . current ) {
428+ try {
429+ placeElRef . current . removeEventListener ?.(
430+ "place_changed" ,
431+ placeElHandlerRef . current ,
432+ ) ;
433+ placeElRef . current . removeEventListener ?.(
434+ "gmpxplacechanged" ,
435+ placeElHandlerRef . current ,
436+ ) ;
437+ } catch { }
438+ }
439+ if ( placeElContainerRef . current ) {
440+ try {
441+ placeElContainerRef . current . innerHTML = "" ;
442+ } catch { }
443+ }
277444 } ;
278- } , [ onPlaceChanged ] ) ;
445+ } , [ onPlaceChanged , onPlaceElementChanged ] ) ;
446+
447+ // Update bias/restrictions when the internal position changes
448+ useEffect ( ( ) => {
449+ const g : any = ( window as any ) . google ;
450+ if ( ! g ?. maps ?. places ) return ;
451+ try {
452+ if ( useNewAutocomplete && placeElRef . current ) {
453+ // Bias around current internalPosition
454+ ( placeElRef . current as any ) . locationBias = {
455+ center : { lat : internalPosition . lat , lng : internalPosition . lng } ,
456+ radius : 80000 ,
457+ } as any ;
458+ // Ensure country restriction remains applied
459+ try {
460+ ( placeElRef . current as any ) . countries = [ "ph" ] ;
461+ } catch { }
462+ } else if ( autocompleteRef . current ) {
463+ // Legacy: bias via bounds (not strict)
464+ const delta = 0.4 ; // ~44km latitude; longitude varies with lat
465+ const sw = new g . maps . LatLng (
466+ internalPosition . lat - delta ,
467+ internalPosition . lng - delta ,
468+ ) ;
469+ const ne = new g . maps . LatLng (
470+ internalPosition . lat + delta ,
471+ internalPosition . lng + delta ,
472+ ) ;
473+ const bounds = new g . maps . LatLngBounds ( sw , ne ) ;
474+ autocompleteRef . current . setBounds ( bounds ) ;
475+ autocompleteRef . current . setOptions ( {
476+ strictBounds : false ,
477+ componentRestrictions : { country : [ "ph" ] } ,
478+ } as any ) ;
479+ }
480+ } catch { }
481+ } , [ internalPosition , useNewAutocomplete ] ) ;
279482
280483 // Load persisted location if available and no value provided
281484 React . useEffect ( ( ) => {
@@ -296,12 +499,24 @@ const LocationMapPicker: React.FC<LocationMapPickerProps> = ({
296499 return (
297500 < div className = "space-y-2" >
298501 < label className = "text-xs font-medium text-gray-600" > { label } </ label >
299- < input
300- ref = { inputRef }
301- type = "text"
302- placeholder = "Search location or drag the pin"
303- className = { `w-full rounded-lg border p-2 text-sm focus:border-blue-500 focus:outline-none ${ highlight ? "border-red-500 ring-2 ring-red-200" : "border-gray-300" } ` }
304- />
502+ { useNewAutocomplete ? (
503+ < div
504+ ref = { placeElContainerRef }
505+ className = { `w-full rounded-lg border p-2 text-sm ${
506+ highlight ? "border-red-500 ring-2 ring-red-200" : "border-gray-300"
507+ } `}
508+ // The new element will be injected here
509+ />
510+ ) : (
511+ < input
512+ ref = { inputRef }
513+ type = "text"
514+ placeholder = "Search location or drag the pin"
515+ className = { `w-full rounded-lg border p-2 text-sm focus:border-blue-500 focus:outline-none ${
516+ highlight ? "border-red-500 ring-2 ring-red-200" : "border-gray-300"
517+ } `}
518+ />
519+ ) }
305520 < div
306521 className = { `rounded-xl ${ highlight ? "border-2 border-red-500 ring-2 ring-red-200" : "border border-gray-200" } ` }
307522 style = { { overflow : "hidden" } }
0 commit comments