1
- import { Button , Divider , FormGroup , H4 , MenuItem } from '@blueprintjs/core'
1
+ import { Button , Classes , Divider , FormGroup , H4 , Menu , MenuItem } from '@blueprintjs/core'
2
2
import { IconNames } from '@blueprintjs/icons'
3
3
import { Suggest } from '@blueprintjs/select'
4
- import Fuse from 'fuse.js'
5
4
import React , { useEffect , useRef , useState } from 'react'
5
+ import { FixedSizeList as List } from 'react-window'
6
6
import { ADD_CONSTRAINT , REMOVE_CONSTRAINT } from 'src/actionConstants'
7
7
import { generateId } from 'src/generateId'
8
8
9
9
import { useServiceContext } from '../../machineBus'
10
10
import { NoValuesProvided } from './NoValuesProvided'
11
11
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
+
16
22
return (
17
23
< 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 } ) }
23
29
/>
24
30
)
25
31
}
26
32
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
+
27
97
export const SelectPopup = ( {
28
98
nonIdealTitle = undefined ,
29
99
nonIdealDescription = undefined ,
30
100
label = '' ,
31
101
} ) => {
32
102
const [ uniqueId ] = useState ( ( ) => `selectPopup-${ generateId ( ) } ` )
33
103
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
44
105
45
106
if ( availableValues . length === 0 ) {
46
107
return < NoValuesProvided title = { nonIdealTitle } description = { nonIdealDescription } />
@@ -50,29 +111,33 @@ export const SelectPopup = ({
50
111
// the value directly to the added constraints list when clicked, so we reset the input here
51
112
const renderInputValue = ( ) => ''
52
113
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
-
61
114
const filterQuery = ( query , items ) => {
62
115
if ( query === '' ) {
63
116
return items . filter ( ( i ) => ! selectedValues . includes ( i . name ) )
64
117
}
65
118
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 ) ) {
69
124
return [ ]
70
125
}
71
126
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 } ]
73
130
} )
74
131
}
75
132
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
+
76
141
return (
77
142
< div >
78
143
{ selectedValues . length > 0 && (
@@ -117,12 +182,12 @@ export const SelectPopup = ({
117
182
id = { `selectPopup-${ uniqueId } ` }
118
183
items = { availableValues . map ( ( i ) => ( { name : i . item , count : i . count } ) ) }
119
184
inputValueRenderer = { renderInputValue }
120
- itemListPredicate = { filterQuery }
121
185
fill = { true }
122
- onItemSelect = { handleItemSelect }
123
186
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 } }
126
191
/>
127
192
</ FormGroup >
128
193
</ div >
0 commit comments