Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[docs][material-ui] Fix Material Icon search lag and other improvements #41330

Merged
merged 10 commits into from
Aug 9, 2024
213 changes: 104 additions & 109 deletions docs/data/material/components/material-icons/SearchIcons.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
import * as React from 'react';
import { VirtuosoGrid } from 'react-virtuoso';
import { styled } from '@mui/material/styles';
import MuiPaper from '@mui/material/Paper';
import copy from 'clipboard-copy';
import InputBase from '@mui/material/InputBase';
import Typography from '@mui/material/Typography';
import PropTypes from 'prop-types';
import { debounce } from '@mui/material/utils';
import Grid from '@mui/material/Grid';
import Dialog from '@mui/material/Dialog';
import DialogActions from '@mui/material/DialogActions';
import DialogContent from '@mui/material/DialogContent';
import DialogTitle from '@mui/material/DialogTitle';
import CircularProgress from '@mui/material/CircularProgress';
import InputAdornment from '@mui/material/InputAdornment';
import IconButton from '@mui/material/IconButton';
import Tooltip from '@mui/material/Tooltip';
import Button from '@mui/material/Button';
import * as flexsearch from 'flexsearch';
import flexsearch from 'flexsearch';
import SearchIcon from '@mui/icons-material/Search';
import FormControlLabel from '@mui/material/FormControlLabel';
import RadioGroup from '@mui/material/RadioGroup';
Expand Down Expand Up @@ -48,9 +50,7 @@ import useQueryParameterState from 'docs/src/modules/utils/useQueryParameterStat
import { HighlightedCode } from '@mui/docs/HighlightedCode';
import synonyms from './synonyms';

const FlexSearchIndex = flexsearch.default.Index;

const UPDATE_SEARCH_INDEX_WAIT_MS = 220;
const FlexSearchIndex = flexsearch.Index;

// const mui = {
// ExitToApp,
Expand Down Expand Up @@ -129,55 +129,56 @@ const StyledSvgIcon = styled(SvgIcon)(({ theme }) => ({
},
}));

const Icons = React.memo(function Icons(props) {
const { icons, handleOpenClick } = props;

const handleIconClick = (icon) => () => {
if (Math.random() < 0.1) {
window.gtag('event', 'material-icons', {
eventAction: 'click',
eventLabel: icon.name,
});
window.gtag('event', 'material-icons-theme', {
eventAction: 'click',
eventLabel: icon.theme,
});
}
};

const handleLabelClick = (event) => {
selectNode(event.currentTarget);
};

const ListWrapper = React.forwardRef(({ style, children, ...props }, ref) => {
return (
<div>
{icons.map((icon) => {
/* eslint-disable jsx-a11y/click-events-have-key-events */
return (
<StyledIcon key={icon.importName} onClick={handleIconClick(icon)}>
<StyledSvgIcon
component={icon.Component}
fontSize="large"
tabIndex={-1}
onClick={handleOpenClick}
title={icon.importName}
/>
<div>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- TODO: a11y */}
<div onClick={handleLabelClick}>{icon.importName}</div>
</div>
{/* eslint-enable jsx-a11y/click-events-have-key-events */}
</StyledIcon>
);
})}
<div
ref={ref}
{...props}
style={{ display: 'flex', flexWrap: 'wrap', ...style }}
>
{children}
</div>
);
});

Icons.propTypes = {
handleOpenClick: PropTypes.func.isRequired,
icons: PropTypes.array.isRequired,
};
function Icon(handleOpenClick) {
return function itemContent(_, icon) {
const handleIconClick = () => {
if (Math.random() < 0.1) {
window.gtag('event', 'material-icons', {
eventAction: 'click',
eventLabel: icon.name,
});
window.gtag('event', 'material-icons-theme', {
eventAction: 'click',
eventLabel: icon.theme,
});
}
};

const handleLabelClick = (event) => {
selectNode(event.currentTarget);
};

return (
/* eslint-disable jsx-a11y/click-events-have-key-events */
<StyledIcon key={icon.importName} onClick={handleIconClick}>
<StyledSvgIcon
component={icon.Component}
fontSize="large"
tabIndex={-1}
onClick={handleOpenClick}
title={icon.importName}
/>
<div>
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions -- TODO: a11y */}
<div onClick={handleLabelClick}>{icon.importName}</div>
</div>
{/* eslint-enable jsx-a11y/click-events-have-key-events */}
</StyledIcon>
);
};
}

const ImportLink = styled(Link)(({ theme }) => ({
textAlign: 'right',
Expand Down Expand Up @@ -438,14 +439,7 @@ DialogDetails.propTypes = {
selectedIcon: PropTypes.object,
};

const Form = styled('form')({
position: 'sticky',
top: 80,
});

const Paper = styled(MuiPaper)(({ theme }) => ({
position: 'sticky',
top: 80,
display: 'flex',
alignItems: 'center',
marginBottom: theme.spacing(2),
Expand Down Expand Up @@ -516,7 +510,6 @@ function useLatest(value) {
}

export default function SearchIcons() {
const [keys, setKeys] = React.useState(null);
const [theme, setTheme] = useQueryParameterState('theme', 'Filled');
const [selectedIcon, setSelectedIcon] = useQueryParameterState('selected', '');
const [query, setQuery] = useQueryParameterState('query', '');
Expand All @@ -532,42 +525,30 @@ export default function SearchIcons() {
setSelectedIcon('');
}, [setSelectedIcon]);

const updateSearchResults = React.useMemo(
() =>
debounce((value) => {
if (value === '') {
setKeys(null);
} else {
searchIndex.searchAsync(value, { limit: 3000 }).then((results) => {
setKeys(results);

// Keep track of the no results so we can add synonyms in the future.
if (value.length >= 4 && results.length === 0) {
window.gtag('event', 'material-icons', {
eventAction: 'no-results',
eventLabel: value,
});
}
});
}
}, UPDATE_SEARCH_INDEX_WAIT_MS),
[],
);
const deferredQuery = React.useDeferredValue(query);
const deferredTheme = React.useDeferredValue(theme);

const isPending = query !== deferredQuery || theme !== deferredTheme;

const icons = React.useMemo(() => {
const keys =
deferredQuery === ''
? null
: searchIndex.search(deferredQuery, { limit: 3000 });
return (keys === null ? allIcons : keys.map((key) => allIconsMap[key])).filter(
(icon) => deferredTheme === icon.theme,
);
}, [deferredQuery, deferredTheme]);

React.useEffect(() => {
updateSearchResults(query);
return () => {
updateSearchResults.clear();
};
}, [query, updateSearchResults]);

const icons = React.useMemo(
() =>
(keys === null ? allIcons : keys.map((key) => allIconsMap[key])).filter(
(icon) => theme === icon.theme,
),
[theme, keys],
);
// Keep track of the no results so we can add synonyms in the future.
if (deferredQuery.length >= 4 && icons.length === 0) {
window.gtag('event', 'material-icons', {
eventAction: 'no-results',
eventLabel: deferredQuery,
});
}
}, [deferredQuery, icons.length]);

const dialogSelectedIcon = useLatest(
selectedIcon ? allIconsMap[selectedIcon] : null,
Expand All @@ -576,29 +557,28 @@ export default function SearchIcons() {
return (
<Grid container sx={{ minHeight: 500 }}>
<Grid item xs={12} sm={3}>
<Form>
<Typography sx={{ fontWeight: 500, mb: 1 }}>Filter the style</Typography>
<RadioGroup>
<form>
<Typography fontWeight={500} sx={{ mb: 1 }}>
Filter the style
</Typography>
<RadioGroup
value={theme}
onChange={(event) => setTheme(event.target.value)}
>
{['Filled', 'Outlined', 'Rounded', 'Two tone', 'Sharp'].map(
(currentTheme) => {
return (
<FormControlLabel
key={currentTheme}
control={
<Radio
size="small"
checked={theme === currentTheme}
onChange={() => setTheme(currentTheme)}
value={currentTheme}
/>
}
value={currentTheme}
control={<Radio size="small" />}
label={currentTheme}
/>
);
},
)}
</RadioGroup>
</Form>
</form>
</Grid>
<Grid item xs={12} sm={9}>
<Paper>
Expand All @@ -611,18 +591,33 @@ export default function SearchIcons() {
onChange={(event) => setQuery(event.target.value)}
placeholder="Search icons…"
inputProps={{ 'aria-label': 'search icons' }}
endAdornment={
isPending ? (
<InputAdornment position="end">
<CircularProgress size={16} sx={{ mr: 2 }} />
</InputAdornment>
) : null
}
/>
</Paper>
<Typography sx={{ mb: 1 }}>{`${formatNumber(
icons.length,
)} matching results`}</Typography>
<Icons icons={icons} handleOpenClick={handleOpenClick} />
<VirtuosoGrid
style={{ height: 500 }}
data={icons}
components={{ List: ListWrapper }}
itemContent={Icon(handleOpenClick)}
/>
</Grid>
<DialogDetails
open={!!selectedIcon}
selectedIcon={dialogSelectedIcon}
handleClose={handleClose}
/>
{/* 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. */}
{selectedIcon ? (
<DialogDetails
open={!!selectedIcon}
selectedIcon={dialogSelectedIcon}
handleClose={handleClose}
/>
) : null}
</Grid>
);
}
Loading