Skip to content

Commit 5ecb9cc

Browse files
anle9650JanpotZeeshanTamboli
authored
[docs][material-ui] Fix Material Icon search lag and other improvements (#41330)
Co-authored-by: MUI bot <2109932+Janpot@users.noreply.github.com> Co-authored-by: ZeeshanTamboli <zeeshan.tamboli@gmail.com>
1 parent 0137be7 commit 5ecb9cc

File tree

1 file changed

+104
-109
lines changed

1 file changed

+104
-109
lines changed

docs/data/material/components/material-icons/SearchIcons.js

+104-109
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,22 @@
11
import * as React from 'react';
2+
import { VirtuosoGrid } from 'react-virtuoso';
23
import { styled } from '@mui/material/styles';
34
import MuiPaper from '@mui/material/Paper';
45
import copy from 'clipboard-copy';
56
import InputBase from '@mui/material/InputBase';
67
import Typography from '@mui/material/Typography';
78
import PropTypes from 'prop-types';
8-
import { debounce } from '@mui/material/utils';
99
import Grid from '@mui/material/Grid';
1010
import Dialog from '@mui/material/Dialog';
1111
import DialogActions from '@mui/material/DialogActions';
1212
import DialogContent from '@mui/material/DialogContent';
1313
import DialogTitle from '@mui/material/DialogTitle';
14+
import CircularProgress from '@mui/material/CircularProgress';
15+
import InputAdornment from '@mui/material/InputAdornment';
1416
import IconButton from '@mui/material/IconButton';
1517
import Tooltip from '@mui/material/Tooltip';
1618
import Button from '@mui/material/Button';
17-
import * as flexsearch from 'flexsearch';
19+
import flexsearch from 'flexsearch';
1820
import SearchIcon from '@mui/icons-material/Search';
1921
import FormControlLabel from '@mui/material/FormControlLabel';
2022
import RadioGroup from '@mui/material/RadioGroup';
@@ -48,9 +50,7 @@ import useQueryParameterState from 'docs/src/modules/utils/useQueryParameterStat
4850
import { HighlightedCode } from '@mui/docs/HighlightedCode';
4951
import synonyms from './synonyms';
5052

51-
const FlexSearchIndex = flexsearch.default.Index;
52-
53-
const UPDATE_SEARCH_INDEX_WAIT_MS = 220;
53+
const FlexSearchIndex = flexsearch.Index;
5454

5555
// const mui = {
5656
// ExitToApp,
@@ -129,55 +129,56 @@ const StyledSvgIcon = styled(SvgIcon)(({ theme }) => ({
129129
},
130130
}));
131131

132-
const Icons = React.memo(function Icons(props) {
133-
const { icons, handleOpenClick } = props;
134-
135-
const handleIconClick = (icon) => () => {
136-
if (Math.random() < 0.1) {
137-
window.gtag('event', 'material-icons', {
138-
eventAction: 'click',
139-
eventLabel: icon.name,
140-
});
141-
window.gtag('event', 'material-icons-theme', {
142-
eventAction: 'click',
143-
eventLabel: icon.theme,
144-
});
145-
}
146-
};
147-
148-
const handleLabelClick = (event) => {
149-
selectNode(event.currentTarget);
150-
};
151-
132+
const ListWrapper = React.forwardRef(({ style, children, ...props }, ref) => {
152133
return (
153-
<div>
154-
{icons.map((icon) => {
155-
/* eslint-disable jsx-a11y/click-events-have-key-events */
156-
return (
157-
<StyledIcon key={icon.importName} onClick={handleIconClick(icon)}>
158-
<StyledSvgIcon
159-
component={icon.Component}
160-
fontSize="large"
161-
tabIndex={-1}
162-
onClick={handleOpenClick}
163-
title={icon.importName}
164-
/>
165-
<div>
166-
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- TODO: a11y */}
167-
<div onClick={handleLabelClick}>{icon.importName}</div>
168-
</div>
169-
{/* eslint-enable jsx-a11y/click-events-have-key-events */}
170-
</StyledIcon>
171-
);
172-
})}
134+
<div
135+
ref={ref}
136+
{...props}
137+
style={{ display: 'flex', flexWrap: 'wrap', ...style }}
138+
>
139+
{children}
173140
</div>
174141
);
175142
});
176143

177-
Icons.propTypes = {
178-
handleOpenClick: PropTypes.func.isRequired,
179-
icons: PropTypes.array.isRequired,
180-
};
144+
function Icon(handleOpenClick) {
145+
return function itemContent(_, icon) {
146+
const handleIconClick = () => {
147+
if (Math.random() < 0.1) {
148+
window.gtag('event', 'material-icons', {
149+
eventAction: 'click',
150+
eventLabel: icon.name,
151+
});
152+
window.gtag('event', 'material-icons-theme', {
153+
eventAction: 'click',
154+
eventLabel: icon.theme,
155+
});
156+
}
157+
};
158+
159+
const handleLabelClick = (event) => {
160+
selectNode(event.currentTarget);
161+
};
162+
163+
return (
164+
/* eslint-disable jsx-a11y/click-events-have-key-events */
165+
<StyledIcon key={icon.importName} onClick={handleIconClick}>
166+
<StyledSvgIcon
167+
component={icon.Component}
168+
fontSize="large"
169+
tabIndex={-1}
170+
onClick={handleOpenClick}
171+
title={icon.importName}
172+
/>
173+
<div>
174+
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- TODO: a11y */}
175+
<div onClick={handleLabelClick}>{icon.importName}</div>
176+
</div>
177+
{/* eslint-enable jsx-a11y/click-events-have-key-events */}
178+
</StyledIcon>
179+
);
180+
};
181+
}
181182

182183
const ImportLink = styled(Link)(({ theme }) => ({
183184
textAlign: 'right',
@@ -438,14 +439,7 @@ DialogDetails.propTypes = {
438439
selectedIcon: PropTypes.object,
439440
};
440441

441-
const Form = styled('form')({
442-
position: 'sticky',
443-
top: 80,
444-
});
445-
446442
const Paper = styled(MuiPaper)(({ theme }) => ({
447-
position: 'sticky',
448-
top: 80,
449443
display: 'flex',
450444
alignItems: 'center',
451445
marginBottom: theme.spacing(2),
@@ -516,7 +510,6 @@ function useLatest(value) {
516510
}
517511

518512
export default function SearchIcons() {
519-
const [keys, setKeys] = React.useState(null);
520513
const [theme, setTheme] = useQueryParameterState('theme', 'Filled');
521514
const [selectedIcon, setSelectedIcon] = useQueryParameterState('selected', '');
522515
const [query, setQuery] = useQueryParameterState('query', '');
@@ -532,42 +525,30 @@ export default function SearchIcons() {
532525
setSelectedIcon('');
533526
}, [setSelectedIcon]);
534527

535-
const updateSearchResults = React.useMemo(
536-
() =>
537-
debounce((value) => {
538-
if (value === '') {
539-
setKeys(null);
540-
} else {
541-
searchIndex.searchAsync(value, { limit: 3000 }).then((results) => {
542-
setKeys(results);
543-
544-
// Keep track of the no results so we can add synonyms in the future.
545-
if (value.length >= 4 && results.length === 0) {
546-
window.gtag('event', 'material-icons', {
547-
eventAction: 'no-results',
548-
eventLabel: value,
549-
});
550-
}
551-
});
552-
}
553-
}, UPDATE_SEARCH_INDEX_WAIT_MS),
554-
[],
555-
);
528+
const deferredQuery = React.useDeferredValue(query);
529+
const deferredTheme = React.useDeferredValue(theme);
530+
531+
const isPending = query !== deferredQuery || theme !== deferredTheme;
532+
533+
const icons = React.useMemo(() => {
534+
const keys =
535+
deferredQuery === ''
536+
? null
537+
: searchIndex.search(deferredQuery, { limit: 3000 });
538+
return (keys === null ? allIcons : keys.map((key) => allIconsMap[key])).filter(
539+
(icon) => deferredTheme === icon.theme,
540+
);
541+
}, [deferredQuery, deferredTheme]);
556542

557543
React.useEffect(() => {
558-
updateSearchResults(query);
559-
return () => {
560-
updateSearchResults.clear();
561-
};
562-
}, [query, updateSearchResults]);
563-
564-
const icons = React.useMemo(
565-
() =>
566-
(keys === null ? allIcons : keys.map((key) => allIconsMap[key])).filter(
567-
(icon) => theme === icon.theme,
568-
),
569-
[theme, keys],
570-
);
544+
// Keep track of the no results so we can add synonyms in the future.
545+
if (deferredQuery.length >= 4 && icons.length === 0) {
546+
window.gtag('event', 'material-icons', {
547+
eventAction: 'no-results',
548+
eventLabel: deferredQuery,
549+
});
550+
}
551+
}, [deferredQuery, icons.length]);
571552

572553
const dialogSelectedIcon = useLatest(
573554
selectedIcon ? allIconsMap[selectedIcon] : null,
@@ -576,29 +557,28 @@ export default function SearchIcons() {
576557
return (
577558
<Grid container sx={{ minHeight: 500 }}>
578559
<Grid item xs={12} sm={3}>
579-
<Form>
580-
<Typography sx={{ fontWeight: 500, mb: 1 }}>Filter the style</Typography>
581-
<RadioGroup>
560+
<form>
561+
<Typography fontWeight={500} sx={{ mb: 1 }}>
562+
Filter the style
563+
</Typography>
564+
<RadioGroup
565+
value={theme}
566+
onChange={(event) => setTheme(event.target.value)}
567+
>
582568
{['Filled', 'Outlined', 'Rounded', 'Two tone', 'Sharp'].map(
583569
(currentTheme) => {
584570
return (
585571
<FormControlLabel
586572
key={currentTheme}
587-
control={
588-
<Radio
589-
size="small"
590-
checked={theme === currentTheme}
591-
onChange={() => setTheme(currentTheme)}
592-
value={currentTheme}
593-
/>
594-
}
573+
value={currentTheme}
574+
control={<Radio size="small" />}
595575
label={currentTheme}
596576
/>
597577
);
598578
},
599579
)}
600580
</RadioGroup>
601-
</Form>
581+
</form>
602582
</Grid>
603583
<Grid item xs={12} sm={9}>
604584
<Paper>
@@ -611,18 +591,33 @@ export default function SearchIcons() {
611591
onChange={(event) => setQuery(event.target.value)}
612592
placeholder="Search icons…"
613593
inputProps={{ 'aria-label': 'search icons' }}
594+
endAdornment={
595+
isPending ? (
596+
<InputAdornment position="end">
597+
<CircularProgress size={16} sx={{ mr: 2 }} />
598+
</InputAdornment>
599+
) : null
600+
}
614601
/>
615602
</Paper>
616603
<Typography sx={{ mb: 1 }}>{`${formatNumber(
617604
icons.length,
618605
)} matching results`}</Typography>
619-
<Icons icons={icons} handleOpenClick={handleOpenClick} />
606+
<VirtuosoGrid
607+
style={{ height: 500 }}
608+
data={icons}
609+
components={{ List: ListWrapper }}
610+
itemContent={Icon(handleOpenClick)}
611+
/>
620612
</Grid>
621-
<DialogDetails
622-
open={!!selectedIcon}
623-
selectedIcon={dialogSelectedIcon}
624-
handleClose={handleClose}
625-
/>
613+
{/* Temporary fix for Dialog not closing sometimes and Backdrop stuck at opacity 0 (see issue https://github.com/mui/material-ui/issues/32286). One disadvantage is that the closing animation is not applied. */}
614+
{selectedIcon ? (
615+
<DialogDetails
616+
open={!!selectedIcon}
617+
selectedIcon={dialogSelectedIcon}
618+
handleClose={handleClose}
619+
/>
620+
) : null}
626621
</Grid>
627622
);
628623
}

0 commit comments

Comments
 (0)