reactjs getMenuProps在React downshift autocomplete库中是必要的吗?

67up9zun  于 12个月前  发布在  React
关注(0)|答案(1)|浏览(138)

我看到getMenuProps的自述文件,但它没有解释refs的用途。当我移动它时,它似乎有效果。我不确定它是否必须位于列表元素直接位于子项元素之上?或者它可以位于父树中的任何位置?
我用这个方法实现了一个带有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>
  )
}

字符串
我认为问题是List内部的react-virtualized具有这样的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>


(don不记得确切的类是什么,但这是要点)。
你可以设置<List containerProps={getMenuItems()}>,但这是在上面的<div class="grid">元素上设置的,而不是在滚动条上。没有办法指定滚动条属性。有什么方法可以让它工作吗?
到目前为止,我已经在上面的自动完成代码中的这三个地方进行了尝试,每个地方都会导致不同的行为,没有一个是正确的:

<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%确定。所以我想知道是否有必要使用它。或者我如何才能让它与react-virtualized一起工作?

完全关闭getMenuProps

实际上,它似乎不工作,滚动是混乱的,不知道这是哪个库。
x1c 0d1x的数据

使用<List containerProps={getMenuProps()}>

它显示额外的空间在年底滚动它看起来像,这也是错误的。


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

滚动是所有颠簸和事情重叠。


bnl4lu3b

bnl4lu3b1#

我现在基本上都在工作了:

  • 滚塑作品
  • 减少到零项目的工作
  • 这比在选择框中呈现一个巨大的列表要好得多
  • 单击一个项目工作
  • 键盘输入工程

我主要做了两件事:

  1. Forked react-virtualizedList添加一个简单的scrollerProps属性,这样它就可以直接在子节点上方指定getMenuProps()。不确定这是否完全必要。
    1.在Downshift元素上实现了stateReducer,似乎默认的stateReducer不是你想要的,所以我让它做我想要的。
    代码如下:
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>
  )
}

字符串

相关问题