// Window large lists with react-virtual
// http://localhost:3000/isolated/final/04.js
import React from 'react'
import {useVirtual} from 'react-virtual'
import {useCombobox} from '../use-combobox'
import {getItems} from '../workerized-filter-cities'
import {useAsync, useForceRerender} from '../utils'
const getVirtualRowStyles = ({size, start}) => ({
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: size,
transform: `translateY(${start}px)`,
})
function Menu({
items,
getMenuProps,
getItemProps,
highlightedIndex,
selectedItem,
listRef,
virtualRows,
totalHeight,
}) {
return (
<ul {...getMenuProps({ref: listRef})}>
<li style={{height: totalHeight}} /> // The reason here add a li is tell react virtual, the total height of the items, so the scolling bar can display correct size
{virtualRows.map(({index, size, start}) => {
const item = items[index]
if (!item) return null
return (
<ListItem
key={item.id}
getItemProps={getItemProps}
item={item}
index={index}
isSelected={selectedItem?.id === item.id}
isHighlighted={highlightedIndex === index}
style={getVirtualRowStyles({size, start})}
>
{item.name}
</ListItem>
)
})}
</ul>
)
}
function ListItem({
getItemProps,
item,
index,
isHighlighted,
isSelected,
style,
...props
}) {
return (
<li
{...getItemProps({
index,
item,
style: {
backgroundColor: isHighlighted ? 'lightgray' : 'inherit',
fontWeight: isSelected ? 'bold' : 'normal',
...style,
},
...props,
})}
/>
)
}
function App() {
const forceRerender = useForceRerender()
const [inputValue, setInputValue] = React.useState('')
const {data: items, run} = useAsync({data: [], status: 'pending'})
React.useEffect(() => {
run(getItems(inputValue))
}, [inputValue, run])
const listRef = React.useRef()
const rowVirtualizer = useVirtual({
size: items.length,
parentRef: listRef,
estimateSize: React.useCallback(() => 20, []),
overscan: 10,
})
const {
selectedItem,
highlightedIndex,
getComboboxProps,
getInputProps,
getItemProps,
getLabelProps,
getMenuProps,
selectItem,
} = useCombobox({
items,
inputValue,
onInputValueChange: ({inputValue: newValue}) => setInputValue(newValue),
onSelectedItemChange: ({selectedItem}) =>
alert(
selectedItem
? `You selected ${selectedItem.name}`
: 'Selection Cleared',
),
itemToString: item => (item ? item.name : ''),
scrollIntoView: () => {},
onHighlightedIndexChange: ({highlightedIndex}) =>
highlightedIndex !== -1 && rowVirtualizer.scrollToIndex(highlightedIndex),
})
return (
<div className="city-app">
<button onClick={forceRerender}>force rerender</button>
<div>
<label {...getLabelProps()}>Find a city</label>
<div {...getComboboxProps()}>
<input {...getInputProps({type: 'text'})} />
<button onClick={() => selectItem(null)} aria-label="toggle menu">
✕
</button>
</div>
<Menu
items={items}
getMenuProps={getMenuProps}
getItemProps={getItemProps}
highlightedIndex={highlightedIndex}
selectedItem={selectedItem}
listRef={listRef}
virtualRows={rowVirtualizer.virtualItems}
totalHeight={rowVirtualizer.totalSize}
/>
</div>
</div>
)
}
export default App