1- import { Button , Divider , FormGroup , H4 , MenuItem } from '@blueprintjs/core'
1+ import { Button , Classes , Divider , FormGroup , H4 , Menu , MenuItem } from '@blueprintjs/core'
22import { IconNames } from '@blueprintjs/icons'
33import { Suggest } from '@blueprintjs/select'
4- import Fuse from 'fuse.js'
54import React , { useEffect , useRef , useState } from 'react'
5+ import { FixedSizeList as List } from 'react-window'
66import { ADD_CONSTRAINT , REMOVE_CONSTRAINT } from 'src/actionConstants'
77import { generateId } from 'src/generateId'
88
99import { useServiceContext } from '../../machineBus'
1010import { NoValuesProvided } from './NoValuesProvided'
1111
12- /**
13- * Renders the menu item for the drop down available menu items
14- */
15- const itemRenderer = ( item , props ) => {
12+ const ConstraintItem = ( { index, style, data } ) => {
13+ const { filteredItems, activeItem, handleItemSelect, infoText } = data
14+
15+ if ( index === 0 ) {
16+ return < MenuItem disabled = { true } text = { infoText } />
17+ }
18+
19+ // subtract 1 because we're adding an informative menu item before all items
20+ const name = filteredItems [ index - 1 ] . name
21+
1622 return (
1723 < MenuItem
18- key = { item . name }
19- text = { item . name }
20- active = { props . modifiers . active }
21- onClick = { props . handleClick }
22- shouldDismissPopover = { false }
24+ key = { name }
25+ text = { name }
26+ style = { style }
27+ active = { name === activeItem . name }
28+ onClick = { ( ) => handleItemSelect ( { name } ) }
2329 />
2430 )
2531}
2632
33+ const VirtualizedMenu = ( {
34+ filteredItems,
35+ itemsParentRef,
36+ query,
37+ activeItem,
38+ handleItemSelect,
39+ } ) => {
40+ const listRef = useRef ( null )
41+
42+ const isPlural = filteredItems . length > 1 ? 's' : ''
43+ const infoText =
44+ query === ''
45+ ? `Showing ${ filteredItems . length } Item${ isPlural } `
46+ : `Found ${ filteredItems . length } item${ isPlural } matching "${ query } "`
47+
48+ useEffect ( ( ) => {
49+ if ( listRef ?. current ) {
50+ const itemLocation = filteredItems . findIndex ( ( item ) => item . name === activeItem . name )
51+ // add one to offset the menu description item
52+ listRef . current . scrollToItem ( itemLocation + 1 )
53+ }
54+ } , [ activeItem , filteredItems ] )
55+
56+ const ulWrapper = ( { children, style } ) => {
57+ return (
58+ < Menu style = { style } ulRef = { itemsParentRef } >
59+ { children }
60+ </ Menu >
61+ )
62+ }
63+
64+ return (
65+ < List
66+ ref = { listRef }
67+ height = { Math . min ( 200 , ( filteredItems . length + 1 ) * 30 ) }
68+ itemSize = { 30 }
69+ width = { 300 }
70+ // add 1 because we're adding an informative menu item before all items
71+ itemCount = { filteredItems . length + 1 }
72+ innerElementType = { ulWrapper }
73+ className = { Classes . MENU }
74+ style = { { listStyle : 'none' } }
75+ itemData = { {
76+ filteredItems,
77+ activeItem,
78+ handleItemSelect,
79+ infoText,
80+ } }
81+ >
82+ { ConstraintItem }
83+ </ List >
84+ )
85+ }
86+
87+ const renderMenu = ( handleItemSelect ) => ( { filteredItems, itemsParentRef, query, activeItem } ) => (
88+ < VirtualizedMenu
89+ filteredItems = { filteredItems }
90+ itemsParentRef = { itemsParentRef }
91+ query = { query }
92+ activeItem = { activeItem }
93+ handleItemSelect = { handleItemSelect }
94+ />
95+ )
96+
2797export const SelectPopup = ( {
2898 nonIdealTitle = undefined ,
2999 nonIdealDescription = undefined ,
30100 label = '' ,
31101} ) => {
32102 const [ uniqueId ] = useState ( ( ) => `selectPopup-${ generateId ( ) } ` )
33103 const [ state , send ] = useServiceContext ( 'constraints' )
34- const { availableValues, selectedValues } = state . context
35-
36- const fuse = useRef ( new Fuse ( [ ] ) )
37-
38- useEffect ( ( ) => {
39- fuse . current = new Fuse ( availableValues , {
40- keys : [ 'item' ] ,
41- useExtendedSearch : true ,
42- } )
43- } , [ availableValues ] )
104+ const { availableValues, selectedValues, searchIndex } = state . context
44105
45106 if ( availableValues . length === 0 ) {
46107 return < NoValuesProvided title = { nonIdealTitle } description = { nonIdealDescription } />
@@ -50,29 +111,33 @@ export const SelectPopup = ({
50111 // the value directly to the added constraints list when clicked, so we reset the input here
51112 const renderInputValue = ( ) => ''
52113
53- const handleItemSelect = ( { name } ) => {
54- send ( { type : ADD_CONSTRAINT , constraint : name } )
55- }
56-
57- const handleButtonClick = ( constraint ) => ( ) => {
58- send ( { type : REMOVE_CONSTRAINT , constraint } )
59- }
60-
61114 const filterQuery = ( query , items ) => {
62115 if ( query === '' ) {
63116 return items . filter ( ( i ) => ! selectedValues . includes ( i . name ) )
64117 }
65118
66- const fuseResults = fuse . current . search ( query )
67- return fuseResults . flatMap ( ( r ) => {
68- if ( selectedValues . includes ( r . item . item ) ) {
119+ // flexSearch's default result limit is set 1000, so we set it to the length of all items
120+ const results = searchIndex . search ( query , availableValues . length )
121+
122+ return results . flatMap ( ( value ) => {
123+ if ( selectedValues . includes ( value ) ) {
69124 return [ ]
70125 }
71126
72- return [ { name : r . item . item , count : r . item . count } ]
127+ const item = items . find ( ( it ) => it . name === value )
128+
129+ return [ { name : item . name , count : item . count } ]
73130 } )
74131 }
75132
133+ const handleItemSelect = ( { name } ) => {
134+ send ( { type : ADD_CONSTRAINT , constraint : name } )
135+ }
136+
137+ const handleButtonClick = ( constraint ) => ( ) => {
138+ send ( { type : REMOVE_CONSTRAINT , constraint } )
139+ }
140+
76141 return (
77142 < div >
78143 { selectedValues . length > 0 && (
@@ -117,12 +182,12 @@ export const SelectPopup = ({
117182 id = { `selectPopup-${ uniqueId } ` }
118183 items = { availableValues . map ( ( i ) => ( { name : i . item , count : i . count } ) ) }
119184 inputValueRenderer = { renderInputValue }
120- itemListPredicate = { filterQuery }
121185 fill = { true }
122- onItemSelect = { handleItemSelect }
123186 resetOnSelect = { true }
124- noResults = { < MenuItem disabled = { true } text = "No results match your entry" /> }
125- itemRenderer = { itemRenderer }
187+ itemListRenderer = { renderMenu ( handleItemSelect ) }
188+ onItemSelect = { handleItemSelect }
189+ itemListPredicate = { filterQuery }
190+ popoverProps = { { captureDismiss : true } }
126191 />
127192 </ FormGroup >
128193 </ div >
0 commit comments