React downshift 自动完成库中 getMenuProps 是必需的吗?

问题描述 投票:0回答:1

我看到了 getMenuProps 上的

readme
,但它没有解释
refs
的用途。当我移动它时,它似乎有效果。我不确定是否需要去列表元素直接在子项元素之上?或者它可以去父树中的任何地方吗? 我基本上用这个来实现带有降档+反应虚拟化的自动完成组件:

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> ) }

我认为的问题是
react-virtualized

里面的

List
有这样的DOM结构:
<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>

(不记得确切的类是什么,但这就是要点)。

您可以设置

<List containerProps={getMenuItems()}>

,但这会将其设置在上面的

<div class="grid">
元素上,而不是滚动条上。无法指定滚动条属性。有什么办法可以让它工作吗?
到目前为止,我已经在上面的自动完成代码中的这3个地方进行了尝试,每个地方都会导致不同的行为,但没有一个是正确的:

<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()}>

完全关闭 
getMenuProps

似乎是迄今为止效果最好的,但不是 100% 确定。所以我想知道是否有必要使用它。或者我怎样才能让它与反应虚拟化一起工作?

完全关闭getMenuProps

实际上它似乎不起作用,滚动混乱,不确定是哪个库。

<List containerProps={getMenuProps()}>

它看起来在滚动条的末尾显示了额外的空间,这也是错误的。

<div {...getMenuProps()}> 节点上使用

absolute

滚动很颠簸,而且东西是重叠的。

我现在大部分都可以工作了:

javascript reactjs react-virtualized downshift
1个回答
0
投票
滚动作品

减少到零项目是有效的
  • 它比在选择框中渲染巨大列表的性能要好得多
  • 单击某个项目即可工作
  • 键盘输入有效
  • 我主要做了两件事:

分叉react-virtualized

,向
    scrollerProps
  1. 添加一个简单的List属性,以便可以将
    getMenuProps()
    直接赋予子级上方。不确定这是否完全有必要。
    stateReducer
    元素上实现了
  2. Downshift
  3. ,似乎默认的
    stateReducer
    不是你想要的,所以我让它做我想要的。
    
    
    这是代码:
  4. import React, { CSSProperties, useCallback, useEffect, useMemo, useRef, useState, } 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 '@lancejpollard/react-virtualized/dist/commonjs/List' export type ItemViewCast = { isActive?: boolean isSelected?: boolean item: ItemCast list: Array<ItemCast> style?: CSSProperties } 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 mapByLabel = useMemo(() => { return items.reduce<Record<string, number>>((m, x, i) => { if (x.search) { m[x.search] = i } return m }, {}) }, [items]) const selectedIndex = value ? map[value] : undefined const selectedItem = selectedIndex != null ? items[selectedIndex] : undefined const [input, setInput] = useState( selectedItem ? renderItemToString(selectedItem) : value, ) useEffect(() => { setInput(selectedItem ? renderItemToString(selectedItem) : value) }, [value, selectedItem]) const handleStateChange = (changes: StateChangeOptions<ItemCast>) => { // console.log('changes', changes) if (changes.hasOwnProperty('selectedItem')) { const selected = changes.selectedItem if (selected) { onChange(selected.value ?? undefined) } else { onChange(undefined) } } else if (changes.hasOwnProperty('inputValue')) { setInput(changes.inputValue ?? undefined) } else if (changes.type === Downshift.stateChangeTypes.blurInput) { setInput(selectedItem ? renderItemToString(selectedItem) : value) } } 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 stateReducer = (state: any, actionAndChanges: any) => { const { type, ...changes } = actionAndChanges // console.log('CHANGES', changes, type) switch (type) { case Downshift.stateChangeTypes.keyDownEnter: case Downshift.stateChangeTypes.clickItem: return { ...changes, isOpen: false, // keep menu open after selection. type, // highlightedIndex: state.highlightedIndex, // inputValue: '', // don't add the item string as input value at selection. } case Downshift.stateChangeTypes.blurInput: return { ...changes, // inputValue: '', // don't add the item string as input value at selection. type, } default: return { ...changes, type } } } return ( <Downshift id={id ? `autocomplete-${id}` : undefined} inputValue={input} stateReducer={stateReducer} onStateChange={handleStateChange} onUserAction={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, 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, }: any & { 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} style={style} {...getItemProps({ item, index, isActive: highlightedIndex === index, isSelected: selectedIndex === index, })} /> ) } const ref = useRef(null) const size = useSize(ref) const totalRowHeight = useDynamicItemHeight ? undefined : rowCount * itemHeight const measuredHeight = window.innerHeight / 2 //size?.height ?? 0 const actualListHeight = totalRowHeight && totalRowHeight > measuredHeight ? measuredHeight : totalRowHeight ? totalRowHeight : rowCount ? measuredHeight : 0 return ( <div className="w-full relative" style={{ height: actualListHeight }} 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={actualListHeight} overscanRowCount={overscanRowCount} // noRowsRenderer={this._noRowsRenderer} rowCount={rowCount} rowHeight={useDynamicItemHeight ? getItemHeight : itemHeight} scrollerProps={actualListHeight ? getMenuProps() : {}} rowRenderer={(props: any) => { return rowRenderer({ ...props, getItemProps, highlightedIndex: highlightedIndex ?? undefined, }) }} // scrollToIndex={scrollToIndex} width={size?.width ?? 128} /> {/* // </AutoSizer> */} </div> </div> ) }

© www.soinside.com 2019 - 2024. All rights reserved.