import * as React from 'react'; import PropTypes from 'prop-types'; import Box from '@mui/material/Box'; import TextField from '@mui/material/TextField'; import Autocomplete from '@mui/material/Autocomplete'; import Paper from '@mui/material/Paper'; import LocationOnIcon from '@mui/icons-material/LocationOn'; import Grid from '@mui/material/Grid'; import Typography from '@mui/material/Typography'; import parse from 'autosuggest-highlight/parse'; // For the sake of this demo, we have to use debounce to reduce Google Maps Places API quote use // But prefer to use throttle in practice // import throttle from 'lodash/throttle'; import { debounce } from '@mui/material/utils'; // This key was created specifically for the demo in mui.com. // You need to create a new one for your application. const GOOGLE_MAPS_API_KEY = 'AIzaSyC3aviU6KHXAjoSnxcw6qbOhjnFctbxPkE'; const useEnhancedEffect = typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; function loadScript(src, position) { const script = document.createElement('script'); script.setAttribute('async', ''); script.src = src; position.appendChild(script); return script; } function CustomPaper(props) { return ( {props.children} {/* Legal requirement https://developers.google.com/maps/documentation/javascript/policies#logo */} ({ display: 'flex', justifyContent: 'flex-end', p: '5px 10px 6px 10px', opacity: 0.9, '& path': { fill: '#5e5e5e', }, ...staticTheme.applyStyles('dark', { opacity: 0.7, '& path': { fill: '#fff', }, }), })} > ); } CustomPaper.propTypes = { /** * The content of the component. */ children: PropTypes.node, }; const fetch = debounce(async (request, callback) => { try { const { suggestions } = await window.google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions( request, ); callback( suggestions.map((suggestion) => { const place = suggestion.placePrediction; // Map to the old AutocompleteService.getPlacePredictions format // https://developers.google.com/maps/documentation/javascript/places-migration-autocomplete return { description: place.text.text, structured_formatting: { main_text: place.mainText.text, main_text_matched_substrings: place.mainText.matches.map((match) => ({ offset: match.startOffset, length: match.endOffset - match.startOffset, })), secondary_text: place.secondaryText?.text, }, }; }), ); } catch (err) { if (err.message.startsWith('Quota exceeded for quota')) { callback(request.input.length === 1 ? fakeAnswer.p : fakeAnswer.paris); } throw err; } }, 400); const emptyOptions = []; let sessionToken; export default function GoogleMaps() { const [value, setValue] = React.useState(null); const [inputValue, setInputValue] = React.useState(''); const [options, setOptions] = React.useState(emptyOptions); const callbackId = React.useId().replace(/[^\w]/g, ''); const [loaded, setLoaded] = React.useState(false); if (typeof window !== 'undefined') { if (!document.querySelector('#google-maps')) { const GOOGLE_NAMESPACE = '_google_callback'; const globalContext = window[GOOGLE_NAMESPACE] || (window[GOOGLE_NAMESPACE] = {}); globalContext[callbackId] = () => { setLoaded(true); }; const script = loadScript( `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places&loading=async&callback=${GOOGLE_NAMESPACE}.${callbackId}`, document.querySelector('head'), ); script.id = 'google-maps'; } else if (window.google && !loaded) { setLoaded(true); } } useEnhancedEffect(() => { if (!loaded) { return undefined; } if (inputValue === '') { setOptions(value ? [value] : emptyOptions); return undefined; } // Allow to resolve the out of order request resolution. let active = true; if (!sessionToken) { sessionToken = new window.google.maps.places.AutocompleteSessionToken(); } fetch({ input: inputValue, sessionToken }, (results) => { if (!active) { return; } let newOptions = []; if (results) { newOptions = results; if (value) { newOptions = [ value, ...results.filter((result) => result.description !== value.description), ]; } } else if (value) { newOptions = [value]; } setOptions(newOptions); }); return () => { active = false; }; }, [value, inputValue, loaded]); return ( typeof option === 'string' ? option : option.description } filterOptions={(x) => x} slots={{ paper: CustomPaper, }} options={options} autoComplete includeInputInList filterSelectedOptions value={value} noOptionsText="No locations" onChange={(event, newValue) => { setOptions(newValue ? [newValue, ...options] : options); setValue(newValue); }} onInputChange={(event, newInputValue) => { setInputValue(newInputValue); }} renderInput={(params) => ( )} renderOption={(props, option) => { const { key, ...optionProps } = props; const matches = option.structured_formatting.main_text_matched_substrings; const parts = parse( option.structured_formatting.main_text, matches.map((match) => [match.offset, match.offset + match.length]), ); return (
  • {parts.map((part, index) => ( {part.text} ))} {option.structured_formatting.secondary_text ? ( {option.structured_formatting.secondary_text} ) : null}
  • ); }} /> ); } // Fake data in case Google Maps Places API returns a rate limit. const fakeAnswer = { p: [ { description: 'Portugal', structured_formatting: { main_text: 'Portugal', main_text_matched_substrings: [{ offset: 0, length: 1 }], }, }, { description: 'Puerto Rico', structured_formatting: { main_text: 'Puerto Rico', main_text_matched_substrings: [{ offset: 0, length: 1 }], }, }, { description: 'Pakistan', structured_formatting: { main_text: 'Pakistan', main_text_matched_substrings: [{ offset: 0, length: 1 }], }, }, { description: 'Philippines', structured_formatting: { main_text: 'Philippines', main_text_matched_substrings: [{ offset: 0, length: 1 }], }, }, { description: 'Paris, France', structured_formatting: { main_text: 'Paris', main_text_matched_substrings: [{ offset: 0, length: 1 }], secondary_text: 'France', }, }, ], paris: [ { description: 'Paris, France', structured_formatting: { main_text: 'Paris', main_text_matched_substrings: [{ offset: 0, length: 5 }], secondary_text: 'France', }, }, { description: 'Paris, TX, USA', structured_formatting: { main_text: 'Paris', main_text_matched_substrings: [{ offset: 0, length: 5 }], secondary_text: 'TX, USA', }, }, { description: "Paris Beauvais Airport, Route de l'Aéroport, Tillé, France", structured_formatting: { main_text: 'Paris Beauvais Airport', main_text_matched_substrings: [{ offset: 0, length: 5 }], secondary_text: "Route de l'Aéroport, Tillé, France", }, }, { description: 'Paris Las Vegas, South Las Vegas Boulevard, Las Vegas, NV, USA', structured_formatting: { main_text: 'Paris Las Vegas', main_text_matched_substrings: [{ offset: 0, length: 5 }], secondary_text: 'South Las Vegas Boulevard, Las Vegas, NV, USA', }, }, { description: "Paris La Défense Arena, Jardin de l'Arche, Nanterre, France", structured_formatting: { main_text: 'Paris La Défense Arena', main_text_matched_substrings: [{ offset: 0, length: 5 }], secondary_text: "Jardin de l'Arche, Nanterre, France", }, }, ], };