From 30125ac56e3599eceb2e423e6fef2d71c3766889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=AD=84=E5=85=B5?= Date: Mon, 11 Nov 2024 17:04:31 +0800 Subject: [PATCH] feat: virtual list --- package.json | 3 +- src/Cascader.tsx | 13 ++ src/OptionList/Column.tsx | 247 +++++++++++++++++++++----------------- src/context.ts | 3 + 4 files changed, 156 insertions(+), 110 deletions(-) diff --git a/package.json b/package.json index d68675e0..85ed3970 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,8 @@ "classnames": "^2.3.1", "rc-select": "~14.16.2", "rc-tree": "~5.10.1", - "rc-util": "^5.43.0" + "rc-util": "^5.43.0", + "rc-virtual-list": "^3.14.8" }, "devDependencies": { "@rc-component/father-plugin": "^1.0.0", diff --git a/src/Cascader.tsx b/src/Cascader.tsx index c746af10..202a0cb9 100644 --- a/src/Cascader.tsx +++ b/src/Cascader.tsx @@ -119,6 +119,10 @@ interface BaseCascaderProps< // Icon expandIcon?: React.ReactNode; loadingIcon?: React.ReactNode; + + virtual?: boolean; + listHeight?: number; + listItemHeight?: number; } export interface FieldNames< @@ -232,6 +236,9 @@ const Cascader = React.forwardRef((props, re dropdownMatchSelectWidth = false, showCheckedStrategy = SHOW_PARENT, optionRender, + virtual = true, + listHeight = 170, + listItemHeight = 28, ...restProps } = props; @@ -407,6 +414,9 @@ const Cascader = React.forwardRef((props, re loadingIcon, dropdownMenuColumnStyle, optionRender, + virtual, + listHeight, + listItemHeight, }), [ mergedOptions, @@ -424,6 +434,9 @@ const Cascader = React.forwardRef((props, re loadingIcon, dropdownMenuColumnStyle, optionRender, + virtual, + listHeight, + listItemHeight, ], ); diff --git a/src/OptionList/Column.tsx b/src/OptionList/Column.tsx index c46cc457..6d354757 100644 --- a/src/OptionList/Column.tsx +++ b/src/OptionList/Column.tsx @@ -5,6 +5,8 @@ import CascaderContext from '../context'; import { SEARCH_MARK } from '../hooks/useSearchOptions'; import { isLeaf, toPathKey } from '../utils/commonUtil'; import Checkbox from './Checkbox'; +import List from 'rc-virtual-list'; +import type { ListRef } from 'rc-virtual-list'; export const FIX_LABEL = '__cascader_fix_label__'; @@ -41,6 +43,7 @@ export default function Column) { + const ref = React.useRef(null); const menuPrefixCls = `${prefixCls}-menu`; const menuItemPrefixCls = `${prefixCls}-menu-item`; @@ -52,6 +55,9 @@ export default function Column - {optionInfoList.map( - ({ - disabled, - label, - value, - isLeaf: isMergedLeaf, - isLoading, - checked, - halfChecked, - option, - fullPath, - fullPathKey, - disableCheckbox, - }) => { - // >>>>> Open - const triggerOpenPath = () => { - if (isOptionDisabled(disabled)) { - return; - } - const nextValueCells = [...fullPath]; - if (hoverOpen && isMergedLeaf) { - nextValueCells.pop(); - } - onActive(nextValueCells); - }; - - // >>>>> Selection - const triggerSelect = () => { - if (isSelectable(option) && !isOptionDisabled(disabled)) { - onSelect(fullPath, isMergedLeaf); - } - }; - - // >>>>> Title - let title: string | undefined; - if (typeof option.title === 'string') { - title = option.title; - } else if (typeof label === 'string') { - title = label; + + // scrollIntoView effect in virtual list + React.useEffect(() => { + if (virtual && ref.current && activeValue) { + const startIndex = optionInfoList.findIndex(({ value }) => value === activeValue); + ref.current.scrollTo({ index: startIndex, align: 'auto' }); + } + }, [optionInfoList, virtual, activeValue]) + + const renderLi = (item: typeof optionInfoList[0]) => { + const { + disabled, + label, + value, + isLeaf: isMergedLeaf, + isLoading, + checked, + halfChecked, + option, + fullPath, + fullPathKey, + disableCheckbox + } = item; + + const triggerOpenPath = () => { + if (isOptionDisabled(disabled)) { + return; + } + const nextValueCells = [...fullPath]; + if (hoverOpen && isMergedLeaf) { + nextValueCells.pop(); + } + onActive(nextValueCells); + }; + + // >>>>> Selection + const triggerSelect = () => { + if (isSelectable(option) && !isOptionDisabled(disabled)) { + onSelect(fullPath, isMergedLeaf); + } + }; + + // >>>>> Title + let title: string | undefined; + if (typeof option.title === 'string') { + title = option.title; + } else if (typeof label === 'string') { + title = label; + } + + // >>>>> Render + return ( +
  • { + triggerOpenPath(); + if (disableCheckbox) { + return; + } + if (!multiple || isMergedLeaf) { + triggerSelect(); } + }} + onDoubleClick={() => { + if (changeOnSelect) { + onToggleOpen(false); + } + }} + onMouseEnter={() => { + if (hoverOpen) { + triggerOpenPath(); + } + }} + onMouseDown={e => { + // Prevent selector from blurring + e.preventDefault(); + }} + > + {multiple && ( + ) => { + if (disableCheckbox) { + return; + } + e.stopPropagation(); + triggerSelect(); + }} + /> + )} +
    + {optionRender ? optionRender(option) : label} +
    + {!isLoading && expandIcon && !isMergedLeaf && ( +
    {expandIcon}
    + )} + {isLoading && loadingIcon && ( +
    {loadingIcon}
    + )} +
  • + ); + }; - // >>>>> Render - return ( -
  • { - triggerOpenPath(); - if (disableCheckbox) { - return; - } - if (!multiple || isMergedLeaf) { - triggerSelect(); - } - }} - onDoubleClick={() => { - if (changeOnSelect) { - onToggleOpen(false); - } - }} - onMouseEnter={() => { - if (hoverOpen) { - triggerOpenPath(); - } - }} - onMouseDown={e => { - // Prevent selector from blurring - e.preventDefault(); - }} - > - {multiple && ( - ) => { - if (disableCheckbox) { - return; - } - e.stopPropagation(); - triggerSelect(); - }} - /> - )} -
    - {optionRender ? optionRender(option) : label} -
    - {!isLoading && expandIcon && !isMergedLeaf && ( -
    {expandIcon}
    - )} - {isLoading && loadingIcon && ( -
    {loadingIcon}
    - )} -
  • - ); - }, + return ( +
      + {virtual ? ( + + {renderLi} + + ) : ( + optionInfoList.map(renderLi) )}
    ); diff --git a/src/context.ts b/src/context.ts index 85e3b6a3..eb1643ed 100644 --- a/src/context.ts +++ b/src/context.ts @@ -22,6 +22,9 @@ export interface CascaderContextProps { loadingIcon?: React.ReactNode; dropdownMenuColumnStyle?: React.CSSProperties; optionRender?: CascaderProps['optionRender']; + virtual?: boolean; + listHeight?: number; + listItemHeight?: number; } const CascaderContext = React.createContext({} as CascaderContextProps);