diff --git a/docs/data/material/components/autocomplete/GoogleMaps.js b/docs/data/material/components/autocomplete/GoogleMaps.js index 0287e024d5de19..9a05f8e66c3643 100644 --- a/docs/data/material/components/autocomplete/GoogleMaps.js +++ b/docs/data/material/components/autocomplete/GoogleMaps.js @@ -3,91 +3,100 @@ import Box from '@mui/material/Box'; import TextField from '@mui/material/TextField'; import Autocomplete from '@mui/material/Autocomplete'; import LocationOnIcon from '@mui/icons-material/LocationOn'; -import Grid from '@mui/material/Grid'; +import Grid2 from '@mui/material/Grid2'; import Typography from '@mui/material/Typography'; import parse from 'autosuggest-highlight/parse'; -import { debounce } from '@mui/material/utils'; +import throttle from 'lodash/throttle'; // 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'; -function loadScript(src, position, id) { - if (!position) { - return; - } +const useEnhancedEffect = + typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; +function loadScript(src, position) { const script = document.createElement('script'); script.setAttribute('async', ''); - script.setAttribute('id', id); script.src = src; position.appendChild(script); + return script; } const autocompleteService = { current: null }; +const fetch = throttle((request, callback) => { + autocompleteService.current.getPlacePredictions(request, callback); +}, 200); + +const emptyOptions = []; + export default function GoogleMaps() { const [value, setValue] = React.useState(null); const [inputValue, setInputValue] = React.useState(''); - const [options, setOptions] = React.useState([]); - const loaded = React.useRef(false); + const [options, setOptions] = React.useState(emptyOptions); + const callbackId = React.useId().replace(/:/g, ''); + const [loaded, setLoaded] = React.useState(false); - if (typeof window !== 'undefined' && !loaded.current) { + if (typeof window !== 'undefined') { if (!document.querySelector('#google-maps')) { - loadScript( - `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places`, + 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'), - 'google-maps', ); + script.id = 'google-maps'; + } else if (window.google && !loaded) { + setLoaded(true); } - - loaded.current = true; } - const fetch = React.useMemo( - () => - debounce((request, callback) => { - autocompleteService.current.getPlacePredictions(request, callback); - }, 400), - [], - ); - - React.useEffect(() => { - let active = true; + useEnhancedEffect(() => { + if (!loaded) { + return undefined; + } - if (!autocompleteService.current && window.google) { + if (!autocompleteService.current) { autocompleteService.current = new window.google.maps.places.AutocompleteService(); } - if (!autocompleteService.current) { - return undefined; - } if (inputValue === '') { - setOptions(value ? [value] : []); + setOptions(value ? [value] : emptyOptions); return undefined; } + // Allow to resolve the out of order request resolution. + let active = true; + fetch({ input: inputValue }, (results) => { - if (active) { - let newOptions = []; + if (!active) { + return; + } - if (value) { - newOptions = [value]; - } + let newOptions = []; - if (results) { - newOptions = [...newOptions, ...results]; - } + if (value) { + newOptions = [value]; + } - setOptions(newOptions); + if (results) { + newOptions = [...newOptions, ...results]; } + + setOptions(newOptions); }); return () => { active = false; }; - }, [value, inputValue, fetch]); + }, [value, inputValue, loaded]); return ( - - + + - - + + {parts.map((part, index) => ( {part.text} @@ -140,8 +153,8 @@ export default function GoogleMaps() { {option.structured_formatting.secondary_text} - - + + ); }} diff --git a/docs/data/material/components/autocomplete/GoogleMaps.tsx b/docs/data/material/components/autocomplete/GoogleMaps.tsx index b52bd46bd7f7ad..0ff699a6b4f16e 100644 --- a/docs/data/material/components/autocomplete/GoogleMaps.tsx +++ b/docs/data/material/components/autocomplete/GoogleMaps.tsx @@ -3,25 +3,24 @@ import Box from '@mui/material/Box'; import TextField from '@mui/material/TextField'; import Autocomplete from '@mui/material/Autocomplete'; import LocationOnIcon from '@mui/icons-material/LocationOn'; -import Grid from '@mui/material/Grid'; +import Grid2 from '@mui/material/Grid2'; import Typography from '@mui/material/Typography'; import parse from 'autosuggest-highlight/parse'; -import { debounce } from '@mui/material/utils'; +import throttle from 'lodash/throttle'; // 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'; -function loadScript(src: string, position: HTMLElement | null, id: string) { - if (!position) { - return; - } +const useEnhancedEffect = + typeof window !== 'undefined' ? React.useLayoutEffect : React.useEffect; +function loadScript(src: string, position: HTMLElement) { const script = document.createElement('script'); script.setAttribute('async', ''); - script.setAttribute('id', id); script.src = src; position.appendChild(script); + return script; } const autocompleteService = { current: null }; @@ -40,78 +39,86 @@ interface PlaceType { structured_formatting: StructuredFormatting; } +const fetch = throttle( + ( + request: { input: string }, + callback: (results?: readonly PlaceType[]) => void, + ) => { + (autocompleteService.current as any).getPlacePredictions(request, callback); + }, + 200, +); + +const emptyOptions = [] as any; + export default function GoogleMaps() { const [value, setValue] = React.useState(null); const [inputValue, setInputValue] = React.useState(''); - const [options, setOptions] = React.useState([]); - const loaded = React.useRef(false); + const [options, setOptions] = React.useState(emptyOptions); + const callbackId = React.useId().replace(/:/g, ''); + const [loaded, setLoaded] = React.useState(false); - if (typeof window !== 'undefined' && !loaded.current) { + if (typeof window !== 'undefined') { if (!document.querySelector('#google-maps')) { - loadScript( - `https://maps.googleapis.com/maps/api/js?key=${GOOGLE_MAPS_API_KEY}&libraries=places`, - document.querySelector('head'), - 'google-maps', + const GOOGLE_NAMESPACE = '_google_callback'; + const globalContext = + // @ts-ignore + 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 as any).google && !loaded) { + setLoaded(true); } - - loaded.current = true; } - const fetch = React.useMemo( - () => - debounce( - ( - request: { input: string }, - callback: (results?: readonly PlaceType[]) => void, - ) => { - (autocompleteService.current as any).getPlacePredictions( - request, - callback, - ); - }, - 400, - ), - [], - ); - - React.useEffect(() => { - let active = true; + useEnhancedEffect(() => { + if (!loaded) { + return undefined; + } - if (!autocompleteService.current && (window as any).google) { + if (!autocompleteService.current) { autocompleteService.current = new ( window as any ).google.maps.places.AutocompleteService(); } - if (!autocompleteService.current) { - return undefined; - } if (inputValue === '') { - setOptions(value ? [value] : []); + setOptions(value ? [value] : emptyOptions); return undefined; } + // Allow to resolve the out of order request resolution. + let active = true; + fetch({ input: inputValue }, (results?: readonly PlaceType[]) => { - if (active) { - let newOptions: readonly PlaceType[] = []; + if (!active) { + return; + } - if (value) { - newOptions = [value]; - } + let newOptions: readonly PlaceType[] = []; - if (results) { - newOptions = [...newOptions, ...results]; - } + if (value) { + newOptions = [value]; + } - setOptions(newOptions); + if (results) { + newOptions = [...newOptions, ...results]; } + + setOptions(newOptions); }); return () => { active = false; }; - }, [value, inputValue, fetch]); + }, [value, inputValue, loaded]); return ( - - + + - - + + {parts.map((part, index) => ( {part.text} @@ -164,8 +175,8 @@ export default function GoogleMaps() { {option.structured_formatting.secondary_text} - - + + ); }} diff --git a/docs/data/material/components/autocomplete/autocomplete.md b/docs/data/material/components/autocomplete/autocomplete.md index 0f7b7c214ce503..09ec3ffbf059bc 100644 --- a/docs/data/material/components/autocomplete/autocomplete.md +++ b/docs/data/material/components/autocomplete/autocomplete.md @@ -215,12 +215,10 @@ overriding the `filterOptions` prop: A customized UI for Google Maps Places Autocomplete. For this demo, we need to load the [Google Maps JavaScript](https://developers.google.com/maps/documentation/javascript/overview) and [Google Places](https://developers.google.com/maps/documentation/places/web-service/overview) API. -:::info -The following demo relies on [autosuggest-highlight](https://github.com/moroshko/autosuggest-highlight), a small (1 kB) utility for highlighting text in autosuggest and autocomplete components. -::: - {{"demo": "GoogleMaps.js"}} +The demo relies on [autosuggest-highlight](https://github.com/moroshko/autosuggest-highlight), a small (1 kB) utility for highlighting text in autosuggest and autocomplete components. + :::error Before you can start using the Google Maps JavaScript API and Places API, you need to get your own [API key](https://developers.google.com/maps/documentation/javascript/get-api-key). ::: diff --git a/packages/mui-docs/src/Ad/AdCarbon.tsx b/packages/mui-docs/src/Ad/AdCarbon.tsx index 73727ae49dd714..f0add7044eb790 100644 --- a/packages/mui-docs/src/Ad/AdCarbon.tsx +++ b/packages/mui-docs/src/Ad/AdCarbon.tsx @@ -47,6 +47,11 @@ function AdCarbonImage() { // // To solve the issue, for example StrictModel double effect execution, we debounce the load action. const load = setTimeout(() => { + // The DOM node could have unmounted at this point. + if (!ref.current) { + return; + } + const script = loadScript( 'https://cdn.carbonads.com/carbon.js?serve=CKYIL27L&placement=material-uicom', ref.current, diff --git a/packages/mui-docs/src/utils/loadScript.ts b/packages/mui-docs/src/utils/loadScript.ts index 5c245efc2d03d9..4f9e811aae2067 100644 --- a/packages/mui-docs/src/utils/loadScript.ts +++ b/packages/mui-docs/src/utils/loadScript.ts @@ -1,10 +1,7 @@ -export default function loadScript(src: string, position: HTMLElement | null) { +export default function loadScript(src: string, position: HTMLElement) { const script = document.createElement('script'); script.setAttribute('async', ''); script.src = src; - if (position) { - position.appendChild(script); - } - + position.appendChild(script); return script; }