1
1
import * as React from 'react' ;
2
+ import { VirtuosoGrid } from 'react-virtuoso' ;
2
3
import { styled } from '@mui/material/styles' ;
3
4
import MuiPaper from '@mui/material/Paper' ;
4
5
import copy from 'clipboard-copy' ;
5
6
import InputBase from '@mui/material/InputBase' ;
6
7
import Typography from '@mui/material/Typography' ;
7
8
import PropTypes from 'prop-types' ;
8
- import { debounce } from '@mui/material/utils' ;
9
9
import Grid from '@mui/material/Grid' ;
10
10
import Dialog from '@mui/material/Dialog' ;
11
11
import DialogActions from '@mui/material/DialogActions' ;
12
12
import DialogContent from '@mui/material/DialogContent' ;
13
13
import DialogTitle from '@mui/material/DialogTitle' ;
14
+ import CircularProgress from '@mui/material/CircularProgress' ;
15
+ import InputAdornment from '@mui/material/InputAdornment' ;
14
16
import IconButton from '@mui/material/IconButton' ;
15
17
import Tooltip from '@mui/material/Tooltip' ;
16
18
import Button from '@mui/material/Button' ;
17
- import * as flexsearch from 'flexsearch' ;
19
+ import flexsearch from 'flexsearch' ;
18
20
import SearchIcon from '@mui/icons-material/Search' ;
19
21
import FormControlLabel from '@mui/material/FormControlLabel' ;
20
22
import RadioGroup from '@mui/material/RadioGroup' ;
@@ -48,9 +50,7 @@ import useQueryParameterState from 'docs/src/modules/utils/useQueryParameterStat
48
50
import { HighlightedCode } from '@mui/docs/HighlightedCode' ;
49
51
import synonyms from './synonyms' ;
50
52
51
- const FlexSearchIndex = flexsearch . default . Index ;
52
-
53
- const UPDATE_SEARCH_INDEX_WAIT_MS = 220 ;
53
+ const FlexSearchIndex = flexsearch . Index ;
54
54
55
55
// const mui = {
56
56
// ExitToApp,
@@ -129,55 +129,56 @@ const StyledSvgIcon = styled(SvgIcon)(({ theme }) => ({
129
129
} ,
130
130
} ) ) ;
131
131
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 ) => {
152
133
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 }
173
140
</ div >
174
141
) ;
175
142
} ) ;
176
143
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
+ }
181
182
182
183
const ImportLink = styled ( Link ) ( ( { theme } ) => ( {
183
184
textAlign : 'right' ,
@@ -438,14 +439,7 @@ DialogDetails.propTypes = {
438
439
selectedIcon : PropTypes . object ,
439
440
} ;
440
441
441
- const Form = styled ( 'form' ) ( {
442
- position : 'sticky' ,
443
- top : 80 ,
444
- } ) ;
445
-
446
442
const Paper = styled ( MuiPaper ) ( ( { theme } ) => ( {
447
- position : 'sticky' ,
448
- top : 80 ,
449
443
display : 'flex' ,
450
444
alignItems : 'center' ,
451
445
marginBottom : theme . spacing ( 2 ) ,
@@ -516,7 +510,6 @@ function useLatest(value) {
516
510
}
517
511
518
512
export default function SearchIcons ( ) {
519
- const [ keys , setKeys ] = React . useState ( null ) ;
520
513
const [ theme , setTheme ] = useQueryParameterState ( 'theme' , 'Filled' ) ;
521
514
const [ selectedIcon , setSelectedIcon ] = useQueryParameterState ( 'selected' , '' ) ;
522
515
const [ query , setQuery ] = useQueryParameterState ( 'query' , '' ) ;
@@ -532,42 +525,30 @@ export default function SearchIcons() {
532
525
setSelectedIcon ( '' ) ;
533
526
} , [ setSelectedIcon ] ) ;
534
527
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 ] ) ;
556
542
557
543
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 ] ) ;
571
552
572
553
const dialogSelectedIcon = useLatest (
573
554
selectedIcon ? allIconsMap [ selectedIcon ] : null ,
@@ -576,29 +557,28 @@ export default function SearchIcons() {
576
557
return (
577
558
< Grid container sx = { { minHeight : 500 } } >
578
559
< 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
+ >
582
568
{ [ 'Filled' , 'Outlined' , 'Rounded' , 'Two tone' , 'Sharp' ] . map (
583
569
( currentTheme ) => {
584
570
return (
585
571
< FormControlLabel
586
572
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" /> }
595
575
label = { currentTheme }
596
576
/>
597
577
) ;
598
578
} ,
599
579
) }
600
580
</ RadioGroup >
601
- </ Form >
581
+ </ form >
602
582
</ Grid >
603
583
< Grid item xs = { 12 } sm = { 9 } >
604
584
< Paper >
@@ -611,18 +591,33 @@ export default function SearchIcons() {
611
591
onChange = { ( event ) => setQuery ( event . target . value ) }
612
592
placeholder = "Search icons…"
613
593
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
+ }
614
601
/>
615
602
</ Paper >
616
603
< Typography sx = { { mb : 1 } } > { `${ formatNumber (
617
604
icons . length ,
618
605
) } 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
+ />
620
612
</ 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 }
626
621
</ Grid >
627
622
) ;
628
623
}
0 commit comments