From 832bc061e0bd9849a2724679191a47d3024adf11 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Tue, 10 Dec 2024 00:04:31 +0100 Subject: [PATCH 1/3] [autocomplete][docs] Improve Google Maps search example --- .../components/autocomplete/GoogleMaps.js | 150 +++++++++------ .../components/autocomplete/GoogleMaps.tsx | 180 +++++++++++------- .../components/autocomplete/autocomplete.md | 6 +- packages/mui-docs/src/Ad/AdCarbon.tsx | 5 + packages/mui-docs/src/utils/loadScript.ts | 7 +- 5 files changed, 213 insertions(+), 135 deletions(-) diff --git a/docs/data/material/components/autocomplete/GoogleMaps.js b/docs/data/material/components/autocomplete/GoogleMaps.js index 0287e024d5de19..7dd26015ac2c89 100644 --- a/docs/data/material/components/autocomplete/GoogleMaps.js +++ b/docs/data/material/components/autocomplete/GoogleMaps.js @@ -3,91 +3,124 @@ 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(async (request, callback) => { + 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, + }, + }; + }), + ); +}, 300); + +const emptyOptions = []; +let sessionToken; 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; - - if (!autocompleteService.current && window.google) { - autocompleteService.current = - new window.google.maps.places.AutocompleteService(); - } - if (!autocompleteService.current) { + useEnhancedEffect(() => { + if (!loaded) { return undefined; } if (inputValue === '') { - setOptions(value ? [value] : []); + setOptions(value ? [value] : emptyOptions); return undefined; } - fetch({ input: inputValue }, (results) => { - if (active) { - let newOptions = []; + // Allow to resolve the out of order request resolution. + let active = true; - if (value) { - newOptions = [value]; - } + if (!sessionToken) { + sessionToken = new window.google.maps.places.AutocompleteSessionToken(); + } - if (results) { - newOptions = [...newOptions, ...results]; - } + fetch({ input: inputValue, sessionToken }, (results) => { + if (!active) { + return; + } - setOptions(newOptions); + 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, fetch]); + }, [value, inputValue, loaded]); return ( { const { key, ...optionProps } = props; - const matches = - option.structured_formatting.main_text_matched_substrings || []; + const matches = option.structured_formatting.main_text_matched_substrings; const parts = parse( option.structured_formatting.main_text, @@ -123,25 +155,31 @@ export default function GoogleMaps() { ); return (
  • - - + + - - + + {parts.map((part, index) => ( {part.text} ))} - - {option.structured_formatting.secondary_text} - - - + {option.structured_formatting.secondary_text ? ( + + {option.structured_formatting.secondary_text} + + ) : null} + +
  • ); }} diff --git a/docs/data/material/components/autocomplete/GoogleMaps.tsx b/docs/data/material/components/autocomplete/GoogleMaps.tsx index b52bd46bd7f7ad..2d94e09bff7b0c 100644 --- a/docs/data/material/components/autocomplete/GoogleMaps.tsx +++ b/docs/data/material/components/autocomplete/GoogleMaps.tsx @@ -3,115 +3,150 @@ 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 }; - interface MainTextMatchedSubstrings { offset: number; length: number; } interface StructuredFormatting { main_text: string; - secondary_text: string; - main_text_matched_substrings?: readonly MainTextMatchedSubstrings[]; + main_text_matched_substrings: readonly MainTextMatchedSubstrings[]; + secondary_text?: string; } interface PlaceType { description: string; structured_formatting: StructuredFormatting; } +const fetch = throttle( + async ( + request: { input: string; sessionToken: any }, + callback: (results?: readonly PlaceType[]) => void, + ) => { + const { suggestions } = await ( + window as any + ).google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions( + request, + ); + + callback( + suggestions.map((suggestion: any) => { + 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: any) => ({ + offset: match.startOffset, + length: match.endOffset - match.startOffset, + }), + ), + secondary_text: place.secondaryText?.text, + }, + }; + }), + ); + }, + 300, +); + +const emptyOptions = [] as any; +let sessionToken: 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; - - if (!autocompleteService.current && (window as any).google) { - autocompleteService.current = new ( - window as any - ).google.maps.places.AutocompleteService(); - } - if (!autocompleteService.current) { + useEnhancedEffect(() => { + if (!loaded) { return undefined; } if (inputValue === '') { - setOptions(value ? [value] : []); + setOptions(value ? [value] : emptyOptions); return undefined; } - fetch({ input: inputValue }, (results?: readonly PlaceType[]) => { - if (active) { - let newOptions: readonly PlaceType[] = []; + // Allow to resolve the out of order request resolution. + let active = true; - if (value) { - newOptions = [value]; - } + if (!sessionToken) { + sessionToken = new ( + window as any + ).google.maps.places.AutocompleteSessionToken(); + } - if (results) { - newOptions = [...newOptions, ...results]; - } + fetch({ input: inputValue, sessionToken }, (results?: readonly PlaceType[]) => { + if (!active) { + return; + } + + let newOptions: readonly PlaceType[] = []; + + if (results) { + newOptions = results; - setOptions(newOptions); + if (value) { + newOptions = [ + value, + ...results.filter((result) => result.description !== value.description), + ]; + } + } else if (value) { + newOptions = [value]; } + setOptions(newOptions); }); return () => { active = false; }; - }, [value, inputValue, fetch]); + }, [value, inputValue, loaded]); return ( { const { key, ...optionProps } = props; - const matches = - option.structured_formatting.main_text_matched_substrings || []; + const matches = option.structured_formatting.main_text_matched_substrings; const parts = parse( option.structured_formatting.main_text, @@ -147,25 +181,31 @@ export default function GoogleMaps() { ); return (
  • - - + + - - + + {parts.map((part, index) => ( {part.text} ))} - - {option.structured_formatting.secondary_text} - - - + {option.structured_formatting.secondary_text ? ( + + {option.structured_formatting.secondary_text} + + ) : null} + +
  • ); }} 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; } From adc326573551768c656e41d4dbcd42385834d464 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Sat, 28 Dec 2024 23:31:08 +0100 Subject: [PATCH 2/3] polish --- .../components/autocomplete/GitHubLabel.js | 58 +++--- .../components/autocomplete/GitHubLabel.tsx | 60 +++--- .../components/autocomplete/GoogleMaps.js | 189 ++++++++++++++--- .../components/autocomplete/GoogleMaps.tsx | 191 +++++++++++++++--- .../components/autocomplete/autocomplete.md | 2 + 5 files changed, 387 insertions(+), 113 deletions(-) diff --git a/docs/data/material/components/autocomplete/GitHubLabel.js b/docs/data/material/components/autocomplete/GitHubLabel.js index 23baefb03e2edc..41e56b35af5b43 100644 --- a/docs/data/material/components/autocomplete/GitHubLabel.js +++ b/docs/data/material/components/autocomplete/GitHubLabel.js @@ -19,13 +19,19 @@ const StyledAutocompletePopper = styled('div')(({ theme }) => ({ fontSize: 13, }, [`& .${autocompleteClasses.listbox}`]: { - backgroundColor: '#fff', padding: 0, + backgroundColor: '#fff', + ...theme.applyStyles('dark', { + backgroundColor: '#1c2128', + }), [`& .${autocompleteClasses.option}`]: { minHeight: 'auto', alignItems: 'flex-start', padding: 8, - borderBottom: `1px solid ${' #eaecef'}`, + borderBottom: '1px solid #eaecef', + ...theme.applyStyles('dark', { + borderBottom: '1px solid #30363d', + }), '&[aria-selected="true"]': { backgroundColor: 'transparent', }, @@ -33,13 +39,7 @@ const StyledAutocompletePopper = styled('div')(({ theme }) => ({ { backgroundColor: theme.palette.action.hover, }, - ...theme.applyStyles('dark', { - borderBottom: `1px solid ${'#30363d'}`, - }), }, - ...theme.applyStyles('dark', { - backgroundColor: '#1c2128', - }), }, [`&.${autocompleteClasses.popperDisablePortal}`]: { position: 'relative', @@ -58,7 +58,7 @@ PopperComponent.propTypes = { }; const StyledPopper = styled(Popper)(({ theme }) => ({ - border: `1px solid ${'#e1e4e8'}`, + border: '1px solid #e1e4e8', boxShadow: `0 8px 24px ${'rgba(149, 157, 165, 0.2)'}`, color: '#24292e', backgroundColor: '#fff', @@ -67,8 +67,8 @@ const StyledPopper = styled(Popper)(({ theme }) => ({ zIndex: theme.zIndex.modal, fontSize: 13, ...theme.applyStyles('dark', { - border: `1px solid ${'#30363d'}`, - boxShadow: `0 8px 24px ${'rgb(1, 4, 9)'}`, + border: '1px solid #30363d', + boxShadow: '0 8px 24px rgb(1, 4, 9)', color: '#c9d1d9', backgroundColor: '#1c2128', }), @@ -77,30 +77,30 @@ const StyledPopper = styled(Popper)(({ theme }) => ({ const StyledInput = styled(InputBase)(({ theme }) => ({ padding: 10, width: '100%', - borderBottom: `1px solid ${'#30363d'}`, + borderBottom: '1px solid #eaecef', + ...theme.applyStyles('dark', { + borderBottom: '1px solid #30363d', + }), '& input': { borderRadius: 4, - backgroundColor: '#fff', - border: `1px solid ${'#30363d'}`, padding: 8, transition: theme.transitions.create(['border-color', 'box-shadow']), fontSize: 14, + backgroundColor: '#fff', + border: '1px solid #30363d', + ...theme.applyStyles('dark', { + backgroundColor: '#0d1117', + border: '1px solid #eaecef', + }), '&:focus': { - boxShadow: `0px 0px 0px 3px ${'rgba(3, 102, 214, 0.3)'}`, + boxShadow: '0px 0px 0px 3px rgba(3, 102, 214, 0.3)', borderColor: '#0366d6', ...theme.applyStyles('dark', { - boxShadow: `0px 0px 0px 3px ${'rgb(12, 45, 107)'}`, + boxShadow: '0px 0px 0px 3px rgb(12, 45, 107)', borderColor: '#388bfd', }), }, - ...theme.applyStyles('dark', { - backgroundColor: '#0d1117', - border: `1px solid ${'#eaecef'}`, - }), }, - ...theme.applyStyles('dark', { - borderBottom: `1px solid ${'#eaecef'}`, - }), })); const Button = styled(ButtonBase)(({ theme }) => ({ @@ -108,8 +108,11 @@ const Button = styled(ButtonBase)(({ theme }) => ({ width: '100%', textAlign: 'left', paddingBottom: 8, - color: '#586069', fontWeight: 600, + color: '#586069', + ...theme.applyStyles('dark', { + color: '#8b949e', + }), '&:hover,&:focus': { color: '#0366d6', ...theme.applyStyles('dark', { @@ -123,9 +126,6 @@ const Button = styled(ButtonBase)(({ theme }) => ({ width: 16, height: 16, }, - ...theme.applyStyles('dark', { - color: '#8b949e', - }), })); export default function GitHubLabel() { @@ -182,11 +182,11 @@ export default function GitHubLabel() {
    ({ - borderBottom: `1px solid ${'#30363d'}`, + borderBottom: '1px solid #30363d', padding: '8px 10px', fontWeight: 600, ...t.applyStyles('light', { - borderBottom: `1px solid ${'#eaecef'}`, + borderBottom: '1px solid #eaecef', }), })} > diff --git a/docs/data/material/components/autocomplete/GitHubLabel.tsx b/docs/data/material/components/autocomplete/GitHubLabel.tsx index d454737b623a12..f95d7fe1d1d396 100644 --- a/docs/data/material/components/autocomplete/GitHubLabel.tsx +++ b/docs/data/material/components/autocomplete/GitHubLabel.tsx @@ -27,15 +27,19 @@ const StyledAutocompletePopper = styled('div')(({ theme }) => ({ fontSize: 13, }, [`& .${autocompleteClasses.listbox}`]: { - backgroundColor: '#fff', - padding: 0, + backgroundColor: '#fff', + ...theme.applyStyles('dark', { + backgroundColor: '#1c2128', + }), [`& .${autocompleteClasses.option}`]: { minHeight: 'auto', alignItems: 'flex-start', padding: 8, - borderBottom: `1px solid ${' #eaecef'}`, - + borderBottom: '1px solid #eaecef', + ...theme.applyStyles('dark', { + borderBottom: '1px solid #30363d', + }), '&[aria-selected="true"]': { backgroundColor: 'transparent', }, @@ -43,13 +47,7 @@ const StyledAutocompletePopper = styled('div')(({ theme }) => ({ { backgroundColor: theme.palette.action.hover, }, - ...theme.applyStyles('dark', { - borderBottom: `1px solid ${'#30363d'}`, - }), }, - ...theme.applyStyles('dark', { - backgroundColor: '#1c2128', - }), }, [`&.${autocompleteClasses.popperDisablePortal}`]: { position: 'relative', @@ -62,7 +60,7 @@ function PopperComponent(props: PopperComponentProps) { } const StyledPopper = styled(Popper)(({ theme }) => ({ - border: `1px solid ${'#e1e4e8'}`, + border: '1px solid #e1e4e8', boxShadow: `0 8px 24px ${'rgba(149, 157, 165, 0.2)'}`, color: '#24292e', backgroundColor: '#fff', @@ -71,8 +69,8 @@ const StyledPopper = styled(Popper)(({ theme }) => ({ zIndex: theme.zIndex.modal, fontSize: 13, ...theme.applyStyles('dark', { - border: `1px solid ${'#30363d'}`, - boxShadow: `0 8px 24px ${'rgb(1, 4, 9)'}`, + border: '1px solid #30363d', + boxShadow: '0 8px 24px rgb(1, 4, 9)', color: '#c9d1d9', backgroundColor: '#1c2128', }), @@ -81,30 +79,30 @@ const StyledPopper = styled(Popper)(({ theme }) => ({ const StyledInput = styled(InputBase)(({ theme }) => ({ padding: 10, width: '100%', - borderBottom: `1px solid ${'#30363d'}`, + borderBottom: '1px solid #eaecef', + ...theme.applyStyles('dark', { + borderBottom: '1px solid #30363d', + }), '& input': { borderRadius: 4, - backgroundColor: '#fff', - border: `1px solid ${'#30363d'}`, padding: 8, transition: theme.transitions.create(['border-color', 'box-shadow']), fontSize: 14, + backgroundColor: '#fff', + border: '1px solid #30363d', + ...theme.applyStyles('dark', { + backgroundColor: '#0d1117', + border: '1px solid #eaecef', + }), '&:focus': { - boxShadow: `0px 0px 0px 3px ${'rgba(3, 102, 214, 0.3)'}`, + boxShadow: '0px 0px 0px 3px rgba(3, 102, 214, 0.3)', borderColor: '#0366d6', ...theme.applyStyles('dark', { - boxShadow: `0px 0px 0px 3px ${'rgb(12, 45, 107)'}`, + boxShadow: '0px 0px 0px 3px rgb(12, 45, 107)', borderColor: '#388bfd', }), }, - ...theme.applyStyles('dark', { - backgroundColor: '#0d1117', - border: `1px solid ${'#eaecef'}`, - }), }, - ...theme.applyStyles('dark', { - borderBottom: `1px solid ${'#eaecef'}`, - }), })); const Button = styled(ButtonBase)(({ theme }) => ({ @@ -112,8 +110,11 @@ const Button = styled(ButtonBase)(({ theme }) => ({ width: '100%', textAlign: 'left', paddingBottom: 8, - color: '#586069', fontWeight: 600, + color: '#586069', + ...theme.applyStyles('dark', { + color: '#8b949e', + }), '&:hover,&:focus': { color: '#0366d6', ...theme.applyStyles('dark', { @@ -127,9 +128,6 @@ const Button = styled(ButtonBase)(({ theme }) => ({ width: 16, height: 16, }, - ...theme.applyStyles('dark', { - color: '#8b949e', - }), })); export default function GitHubLabel() { @@ -186,11 +184,11 @@ export default function GitHubLabel() {
    ({ - borderBottom: `1px solid ${'#30363d'}`, + borderBottom: '1px solid #30363d', padding: '8px 10px', fontWeight: 600, ...t.applyStyles('light', { - borderBottom: `1px solid ${'#eaecef'}`, + borderBottom: '1px solid #eaecef', }), })} > diff --git a/docs/data/material/components/autocomplete/GoogleMaps.js b/docs/data/material/components/autocomplete/GoogleMaps.js index 7dd26015ac2c89..944fe71bda7f93 100644 --- a/docs/data/material/components/autocomplete/GoogleMaps.js +++ b/docs/data/material/components/autocomplete/GoogleMaps.js @@ -1,12 +1,18 @@ 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 Grid2 from '@mui/material/Grid2'; import Typography from '@mui/material/Typography'; +import { useTheme } from '@mui/material/styles'; import parse from 'autosuggest-highlight/parse'; -import throttle from 'lodash/throttle'; +// 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. @@ -23,31 +29,79 @@ function loadScript(src, position) { return script; } -const fetch = throttle(async (request, callback) => { - const { suggestions } = - await window.google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions( - request, - ); +function CustomPaper(props) { + const theme = useTheme(); - 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, - }, - }; - }), + return ( + + {props.children} + {/* Legal requirment https://developers.google.com/maps/documentation/javascript/policies#logo */} + ({ + display: 'flex', + justifyContent: 'flex-end', + p: 1, + pt: '1px', + ...staticTheme.applyStyles('dark', { + opacity: 0.8, + }), + })} + > + + + ); -}, 300); +} + +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 === 'Quota exceeded for quota') { + callback(request.input.length === 1 ? fakeAnswer.p : fakeAnswer.paris); + } + + throw err; + } +}, 400); const emptyOptions = []; let sessionToken; @@ -129,6 +183,9 @@ export default function GoogleMaps() { typeof option === 'string' ? option : option.description } filterOptions={(x) => x} + slots={{ + paper: CustomPaper, + }} options={options} autoComplete includeInputInList @@ -186,3 +243,87 @@ export default function GoogleMaps() { /> ); } + +// Fake data in case Google Map 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", + }, + }, + ], +}; diff --git a/docs/data/material/components/autocomplete/GoogleMaps.tsx b/docs/data/material/components/autocomplete/GoogleMaps.tsx index 2d94e09bff7b0c..f0f5bd0d2e2829 100644 --- a/docs/data/material/components/autocomplete/GoogleMaps.tsx +++ b/docs/data/material/components/autocomplete/GoogleMaps.tsx @@ -2,11 +2,16 @@ import * as React from 'react'; import Box from '@mui/material/Box'; import TextField from '@mui/material/TextField'; import Autocomplete from '@mui/material/Autocomplete'; +import Paper, { PaperProps } from '@mui/material/Paper'; import LocationOnIcon from '@mui/icons-material/LocationOn'; import Grid2 from '@mui/material/Grid2'; import Typography from '@mui/material/Typography'; +import { useTheme } from '@mui/material/styles'; import parse from 'autosuggest-highlight/parse'; -import throttle from 'lodash/throttle'; +// 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. @@ -37,39 +42,80 @@ interface PlaceType { structured_formatting: StructuredFormatting; } -const fetch = throttle( +function CustomPaper(props: PaperProps) { + const theme = useTheme(); + + return ( + + {props.children} + {/* Legal requirment https://developers.google.com/maps/documentation/javascript/policies#logo */} + ({ + display: 'flex', + justifyContent: 'flex-end', + p: 1, + pt: '1px', + ...staticTheme.applyStyles('dark', { + opacity: 0.8, + }), + })} + > + + + + ); +} + +const fetch = debounce( async ( request: { input: string; sessionToken: any }, callback: (results?: readonly PlaceType[]) => void, ) => { - const { suggestions } = await ( - window as any - ).google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions( - request, - ); - - callback( - suggestions.map((suggestion: any) => { - 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: any) => ({ - offset: match.startOffset, - length: match.endOffset - match.startOffset, - }), - ), - secondary_text: place.secondaryText?.text, - }, - }; - }), - ); + try { + const { suggestions } = await ( + window as any + ).google.maps.places.AutocompleteSuggestion.fetchAutocompleteSuggestions( + request, + ); + + callback( + suggestions.map((suggestion: any) => { + 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: any) => ({ + offset: match.startOffset, + length: match.endOffset - match.startOffset, + }), + ), + secondary_text: place.secondaryText?.text, + }, + }; + }), + ); + } catch (err: any) { + if (err.message === 'Quota exceeded for quota') { + callback(request.input.length === 1 ? fakeAnswer.p : fakeAnswer.paris); + } + + throw err; + } }, - 300, + 400, ); const emptyOptions = [] as any; @@ -155,6 +201,9 @@ export default function GoogleMaps() { typeof option === 'string' ? option : option.description } filterOptions={(x) => x} + slots={{ + paper: CustomPaper, + }} options={options} autoComplete includeInputInList @@ -212,3 +261,87 @@ export default function GoogleMaps() { /> ); } + +// Fake data in case Google Map 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", + }, + }, + ], +}; diff --git a/docs/data/material/components/autocomplete/autocomplete.md b/docs/data/material/components/autocomplete/autocomplete.md index 09ec3ffbf059bc..553ba3c910d09d 100644 --- a/docs/data/material/components/autocomplete/autocomplete.md +++ b/docs/data/material/components/autocomplete/autocomplete.md @@ -221,6 +221,8 @@ The demo relies on [autosuggest-highlight](https://github.com/moroshko/autosugge :::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). + +This demo has limited quotas to make API requests. When your quota exceeds, you will see the response for "Paris". ::: ## Multiple values From e40322d5a7b404e6b8f4c5411f4d573353c86153 Mon Sep 17 00:00:00 2001 From: Olivier Tassinari Date: Tue, 4 Feb 2025 23:10:55 +0100 Subject: [PATCH 3/3] improve docs --- docs/data/material/components/autocomplete/autocomplete.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/data/material/components/autocomplete/autocomplete.md b/docs/data/material/components/autocomplete/autocomplete.md index 553ba3c910d09d..f7cb199a45c6c3 100644 --- a/docs/data/material/components/autocomplete/autocomplete.md +++ b/docs/data/material/components/autocomplete/autocomplete.md @@ -220,7 +220,7 @@ For this demo, we need to load the [Google Maps JavaScript](https://developers.g 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). +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). This demo has limited quotas to make API requests. When your quota exceeds, you will see the response for "Paris". :::