import React, { useState, useEffect, useRef, forwardRef, useMemo, useCallback, createElement, } from "react"; import { VariableSizeList, ListOnScrollProps, ListChildComponentProps, } from "react-window"; import { ListProps, List, ListItemProps } from "./List"; import { DropdownBox } from "../dropdown"; import { RocketProps } from "../_util/create-rocket"; import { EmptyTip } from "../tips"; import { mergeRefs } from "../_util/merge-refs"; import { StyledProps } from "../_type"; import { noop } from "../_util/noop"; import { useListScroll } from "../_util/use-list-scroll"; export interface VirtualizedListProps extends Omit { /** * 列表项 */ items: VirtualizedListItem[]; /** * 列表高 */ listHeight?: number; /** * 滚动到底部事件 */ onScrollBottom?: (props: ListOnScrollProps) => void; /** * 初始偏移 */ initialScrollOffsetIndex?: number; /** * 虚拟滚动组件 ref */ virtualizedRef?: React.Ref; /** * 用于参照的列表容器 * @default DropdownBox */ container?: | React.FunctionComponent | React.ComponentClass | string; /** * 列表容器样式 */ containerStyle?: React.CSSProperties; /** * 列表容器类名 */ containerClassName?: string; } function Row({ data, index, style }: ListChildComponentProps) { const { type, text, props = {} } = data[index]; if (type === "tips") { return ( {text} ); } if (type === "group") { return ( {text} ); } if (type === "divider") { return ; } return ( {text} ); } export const VirtualizedList = forwardRef(function VirtualizedList( { virtual = true, ...props }: VirtualizedListProps & { virtual?: boolean }, ref: React.Ref ) { if (virtual) { return ; } return ; }); VirtualizedList.displayName = "VirtualizedList"; const VList = forwardRef(function VList( { items = [], onScrollBottom, virtualizedRef, listHeight, // ListProps type, split, className, style, container, containerStyle, containerClassName, initialScrollOffsetIndex, }: VirtualizedListProps, ref: React.Ref ) { const vRef = useRef(null); const listRef = useRef(null); const [tipsSize, setTipsSize] = useState(undefined); const [itemSize, setItemSize] = useState(undefined); const [groupSize, setGroupSize] = useState(undefined); const [dividerSize, setDividerSize] = useState(undefined); const [listSize, setListSize] = useState(listHeight); const innerElementType = useMemo( () => forwardRef( ( { style: innerStyle = {}, ...innerProps }: ListProps, ref: React.Ref ) => ( ) ), [type, split, className, style] ); const getItemSize = useCallback( (index: number) => { switch (items[index].type) { case "tips": return tipsSize || itemSize; case "group": return groupSize || itemSize; case "divider": return dividerSize || 9; default: return itemSize; } }, [dividerSize, groupSize, itemSize, items, tipsSize] ); // VariableSizeList 默认按 index 缓存 style,需手动触发重置 useEffect(() => { if (vRef.current) { items.forEach((_, i) => vRef.current.resetAfterIndex(i, false)); } }, [groupSize, itemSize, items, tipsSize]); const itemsSize = useMemo( () => items.reduce((p, _, i) => p + getItemSize(i), 0), [getItemSize, items] ); function handleScroll(props: ListOnScrollProps) { if (itemsSize > listSize && listRef.current && onScrollBottom) { const { scrollHeight, scrollTop, clientHeight } = listRef.current; if (scrollHeight <= Math.ceil(clientHeight + scrollTop + 1)) { onScrollBottom(props); } } } const height = Math.min(itemsSize, listSize); const initialScrollOffset = useMemo(() => { if (!initialScrollOffsetIndex || initialScrollOffsetIndex < 0) { return 0; } let offset = 0; for (let i = 0; i < initialScrollOffsetIndex; ++i) { offset += getItemSize(i); } // react-window 在 offset < height 时出现可能出现渲缺失 return offset + getItemSize(initialScrollOffsetIndex) > height ? offset : 0; }, [getItemSize, height, initialScrollOffsetIndex]); const option = items.find(i => i.type === "option")?.text; const tips = items.find(i => i.type === "tips")?.text; const group = items.find(i => i.type === "group")?.text; return ( <> {[itemSize, listSize].every(s => typeof s !== "undefined") && ( items[index].key} itemData={items} initialScrollOffset={initialScrollOffset} > {Row} )} ); }); VList.displayName = "List"; function MirrorList({ itemReference, tipsReference, groupReference, onTipsSizeChange, onItemSizeChange, onGroupSizeChange, onDividerSizeChange, onListSizeChange, container = DropdownBox, containerStyle = {}, containerClassName, ...props }: ListProps & { container: VirtualizedListProps["container"]; containerClassName: VirtualizedListProps["containerClassName"]; containerStyle: VirtualizedListProps["containerStyle"]; tipsReference?: React.ReactNode; itemReference?: React.ReactNode; groupReference?: React.ReactNode; onTipsSizeChange: (size: number) => void; onItemSizeChange: (size: number) => void; onGroupSizeChange: (size: number) => void; onDividerSizeChange: (size: number) => void; onListSizeChange: (size: number) => void; }) { const tipsRef = useRef(null); const listRef = useRef(null); const groupRef = useRef(null); const itemRef = useRef(null); const dividerRef = useRef(null); useEffect(() => { if (itemRef.current) { onItemSizeChange(itemRef.current.clientHeight); } // tipsRef / groupRef 存在时才取高度 if (tipsRef.current && tipsReference) { onTipsSizeChange(tipsRef.current.clientHeight); } if (groupRef.current && groupReference) { onGroupSizeChange(groupRef.current.clientHeight); } if (dividerRef.current) { onDividerSizeChange(dividerRef.current.clientHeight); } if (listRef.current) { onListSizeChange(listRef.current.clientHeight); } }, [ tipsReference, itemReference, groupReference, onTipsSizeChange, onItemSizeChange, onGroupSizeChange, onListSizeChange, onDividerSizeChange, ]); return createElement( container, { style: { ...containerStyle, position: "absolute", top: -9999, left: 0, }, className: containerClassName, }, {tipsReference || } {groupReference || "MirrorGroup"} {itemReference || "MirrorItem"}
  • Holder
  • ); } export type VirtualizedListItemType = "tips" | "option" | "group" | "divider"; /** * 虚拟滚动列表项 */ export interface VirtualizedListItem { /** * 列表项类型 */ type: VirtualizedListItemType; /** * 列表项标识 */ key: string; /** * 列表项展示内容 */ text: React.ReactNode; /** * 列表项附加内容 */ props?: ListItemProps; /** * 列表项属性 */ option?: T; } function VItem({ style = {}, ...props }: ListItemProps) { return ( ); } function VGroupLabel({ style = {}, ...props }: RocketProps) { return ( ); } function VStatusTip({ style = {}, ...props }: RocketProps) { return ( ); } function VDivider({ style = {}, ...props }: RocketProps) { return ( ); } /** * 兼容虚拟滚动 List 接口的普通 List */ const NormalList = forwardRef(function NormalList( { items, type, split, onScrollBottom, listHeight, style = {}, className, }: VirtualizedListProps, ref: React.Ref ) { const listRef = useRef(null); const scrollTo = useListScroll(listRef); useEffect(() => { const currentIndex = items.findIndex(item => item.props?.current); scrollTo(currentIndex); }, [items, scrollTo]); // 普通 List return ( {items.map(item => { switch (item.type) { case "tips": return ( {item.text} ); case "group": return ( {item.text} ); case "divider": return ; } return ( {item.text} ); })} ); }); NormalList.displayName = "List";