I see the readme on getMenuProps, but it doesn't explain what the refs are used for. It seems to have an effect when I move it around. I'm not sure if it is required to go on a list element directly above child item elements? Or can it go anywhere in the parent tree?
I have this basically for implementing an autocomplete component with downshift + react-virtualized:
import React, { useCallback, useMemo, useRef } from 'react'
import Downshift, { StateChangeOptions } from 'downshift'
import Input from './Input'
import { matchSorter } from 'match-sorter'
import { CloseIcon, IconButton, useSize } from './base'
import TriangleDownIcon from './icon/TriangleDown'
import List, {
ListRowProps,
} from 'react-virtualized/dist/commonjs/List'
export type ItemViewCast = {
isActive?: boolean
isSelected?: boolean
item: ItemCast
list: Array<ItemCast>
}
export type ItemCast = {
height?: number
value: string
search: string
}
export default function Autocomplete({
value,
onChange,
placeholder,
items,
itemRenderer,
listHeight,
overscanRowCount = 10,
useDynamicItemHeight,
itemHeight = 128,
showScrollingPlaceholder,
clearable,
id,
renderItemToString,
}: {
renderItemToString: (item: ItemCast | null) => string
id?: string
value?: string
placeholder?: string
onChange: (val?: string) => void
items: Array<ItemCast>
itemRenderer: React.ComponentType<ItemViewCast>
listHeight?: number | string
overscanRowCount?: number
useDynamicItemHeight?: boolean
itemHeight?: number
showScrollingPlaceholder?: boolean
clearable?: boolean
}) {
const map = useMemo(() => {
return items.reduce<Record<string, number>>((m, x, i) => {
if (x.value) {
m[x.value] = i
}
return m
}, {})
}, [items])
const handleStateChange = (changes: StateChangeOptions<ItemCast>) => {
if (changes.hasOwnProperty('selectedItem')) {
onChange(changes.selectedItem?.value ?? undefined)
} else if (changes.hasOwnProperty('inputValue')) {
// onChange(changes.inputValue ?? undefined)
}
}
const clearSelection = () => {
if (clearable) {
onChange(undefined)
}
}
const getItemHeight = useCallback(
({ index }: { index: number }) => {
return items[index]?.height ?? itemHeight
},
[items, itemHeight],
)
// const handleScrollToRowChange = (event) => {
// const {rowCount} = this.state;
// let scrollToIndex = Math.min(
// rowCount - 1,
// parseInt(event.target.value, 10),
// );
// if (isNaN(scrollToIndex)) {
// scrollToIndex = undefined;
// }
// // this.setState({scrollToIndex});
// }
const selectedIndex = value ? map[value] : undefined
const selectedItem =
selectedIndex != null ? items[selectedIndex] : undefined
return (
<Downshift
id={id ? `autocomplete-${id}` : undefined}
inputValue={value}
onStateChange={handleStateChange}
itemToString={renderItemToString}
>
{({
getLabelProps,
getInputProps,
getToggleButtonProps,
getMenuProps,
getItemProps,
getRootProps,
isOpen,
selectedItem,
inputValue,
highlightedIndex,
}) => {
return (
<div className="w-full relative">
<div className="w-full relative">
<Input
{...getInputProps({
isOpen,
// inputValue,
placeholder,
})}
/>
<div className="absolute right-0 top-0 h-full">
<div className="flex items-center h-full">
{selectedItem && clearable ? (
<IconButton
onClick={clearSelection}
aria-label="clear selection"
className="w-32 h-32"
>
<CloseIcon />
</IconButton>
) : (
<IconButton
className="w-32 h-32"
{...getToggleButtonProps()}
>
<TriangleDownIcon />
</IconButton>
)}
</div>
</div>
</div>
{isOpen && (
<Menu
id={id}
itemRenderer={itemRenderer}
overscanRowCount={overscanRowCount}
listHeight={listHeight}
getMenuProps={getMenuProps}
items={items}
inputValue={inputValue ?? undefined}
value={value}
highlightedIndex={highlightedIndex ?? undefined}
useDynamicItemHeight={useDynamicItemHeight}
getItemHeight={getItemHeight}
itemHeight={itemHeight}
showScrollingPlaceholder={showScrollingPlaceholder}
getItemProps={getItemProps}
/>
)}
</div>
)
}}
</Downshift>
)
}
function Menu({
getMenuProps,
listHeight,
overscanRowCount,
items,
inputValue,
value,
highlightedIndex,
useDynamicItemHeight,
getItemHeight,
itemHeight,
showScrollingPlaceholder,
itemRenderer,
getItemProps,
id,
}: {
id?: string
items: Array<ItemCast>
listHeight?: number | string
value?: string
overscanRowCount: number
highlightedIndex?: number
useDynamicItemHeight?: boolean
itemHeight: number
getItemHeight: ({ index }: { index: number }) => number
inputValue?: string
getMenuProps: () => Record<string, any>
getItemProps: (opts: any) => Record<string, any>
showScrollingPlaceholder?: boolean
itemRenderer: React.ComponentType<ItemViewCast>
}) {
const listRef = useRef(null)
const filteredItems = useMemo(() => {
if (!inputValue) {
return items
}
return matchSorter(items, inputValue, { keys: ['search'] })
}, [items, inputValue])
const filteredMap = useMemo(() => {
return filteredItems.reduce<Record<string, number>>((m, x, i) => {
if (x.value) {
m[x.value] = i
}
return m
}, {})
}, [filteredItems])
const rowCount = filteredItems.length
const selectedIndex = value ? filteredMap[value] : undefined
const Item = itemRenderer
// const selectedItem =
// selectedIndex != null ? filteredItems[selectedIndex] : undefined
const rowRenderer = ({
index,
isScrolling,
key,
style,
getItemProps,
highlightedIndex,
}: ListRowProps & {
getItemProps: (opts: any) => Record<string, any>
highlightedIndex?: number
}) => {
if (showScrollingPlaceholder && isScrolling) {
return (
<div
// className={cx(styles.row, styles.isScrollingPlaceholder)}
key={key}
style={style}
>
Scrolling...
</div>
)
}
const item = filteredItems[index]
if (useDynamicItemHeight) {
// switch (item.size) {
// case 75:
// additionalContent = <div>It is medium-sized.</div>
// break
// case 100:
// additionalContent = (
// <div>
// It is large-sized.
// <br />
// It has a 3rd row.
// </div>
// )
// break
// }
}
return (
<Item
key={item.value}
item={item}
list={filteredItems}
{...getItemProps({
item,
index,
isActive: highlightedIndex === index,
isSelected: selectedIndex === index,
})}
/>
)
}
const ref = useRef(null)
const size = useSize(ref)
return (
<div
className="w-full relative"
style={{ height: listHeight }}
ref={ref}
>
<div className="w-full absolute h-full bg-zinc-50 z-3000">
<List
id={id ? `autocomplete-list-${id}` : undefined}
className="relative w-full"
ref={listRef}
height={size?.height ?? 0}
overscanRowCount={overscanRowCount}
// noRowsRenderer={this._noRowsRenderer}
rowCount={rowCount}
rowHeight={useDynamicItemHeight ? getItemHeight : itemHeight}
containerProps={getMenuProps()}
rowRenderer={(props: ListRowProps) => {
return rowRenderer({
...props,
getItemProps,
highlightedIndex: highlightedIndex ?? undefined,
})
}}
// scrollToIndex={scrollToIndex}
width={size?.width ?? 128}
/>
{/* // </AutoSizer> */}
</div>
</div>
)
}
The problem I think is that react-virtualized inside List has this DOM structure:
<div class="grid">
<div class="scroller">
<div class="item">item from `Item` component 1</div>
<div class="item">item from `Item` component 2</div>
</div>
</div>
(don't remember what the exact classes are, but that is the gist).
You can set <List containerProps={getMenuItems()}> but that sets it on the above <div class="grid"> element, not the scroller. There is no way to specify scroller props. Is there any way to get this working?
So far I have tried in these 3 places in the above Autocomplete code, and each causes different behavior, none of which are correct:
<div
className="w-full relative"
style={{ height: listHeight }}
ref={ref}
{...getMenuProps()}
>
<div {...getMenuProps()} className="w-full absolute h-full bg-zinc-50 z-3000">
<List containerProps={getMenuProps()}>
Leaving getMenuProps off altogether seems to be working the best so far, but not 100% sure. So I'm wondering in part if it's necessary to use. Or how can I get this working with react-virtualized?
With getMenuProps off altogether
Actually it doesn't seem to be working, scrolling is messed up, not sure which lib that is.
With <List containerProps={getMenuProps()}>
It shows extra space at the end of the scroller it looks like, which is also wrong.
With <div {...getMenuProps()}> on the absolute node
Scrolling is all jolty and things are overlapping.



I got it all mostly working now:
I did two main things:
scrollerPropsproperty to theListso it can be given thegetMenuProps()directly above the children. Not sure if that totally was necessary yet.stateReduceron theDownshiftelement, it seems the defaultstateReduceris not what you'd want, so I made it do what I want.Here is the code: