Skip to content

Commit

Permalink
feat: virtual list
Browse files Browse the repository at this point in the history
  • Loading branch information
魄兵 committed Nov 12, 2024
1 parent 751451f commit ae863de
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 110 deletions.
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
13 changes: 13 additions & 0 deletions src/Cascader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ interface BaseCascaderProps<
// Icon
expandIcon?: React.ReactNode;
loadingIcon?: React.ReactNode;

virtual?: boolean;
listHeight?: number;
listItemHeight?: number;
}

export interface FieldNames<
Expand Down Expand Up @@ -232,6 +236,9 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
dropdownMatchSelectWidth = false,
showCheckedStrategy = SHOW_PARENT,
optionRender,
virtual = true,
listHeight = 170,
listItemHeight = 28,
...restProps
} = props;

Expand Down Expand Up @@ -407,6 +414,9 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
loadingIcon,
dropdownMenuColumnStyle,
optionRender,
virtual,
listHeight,
listItemHeight,
}),
[
mergedOptions,
Expand All @@ -424,6 +434,9 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
loadingIcon,
dropdownMenuColumnStyle,
optionRender,
virtual,
listHeight,
listItemHeight,
],
);

Expand Down
247 changes: 138 additions & 109 deletions src/OptionList/Column.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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__';

Expand Down Expand Up @@ -41,6 +43,7 @@ export default function Column<OptionType extends DefaultOptionType = DefaultOpt
isSelectable,
disabled: propsDisabled,
}: ColumnProps<OptionType>) {
const ref = React.useRef<ListRef>(null);
const menuPrefixCls = `${prefixCls}-menu`;
const menuItemPrefixCls = `${prefixCls}-menu-item`;

Expand All @@ -52,6 +55,9 @@ export default function Column<OptionType extends DefaultOptionType = DefaultOpt
loadingIcon,
dropdownMenuColumnStyle,
optionRender,
virtual,
listItemHeight,
listHeight,
} = React.useContext(CascaderContext);

const hoverOpen = expandTrigger === 'hover';
Expand Down Expand Up @@ -101,117 +107,140 @@ export default function Column<OptionType extends DefaultOptionType = DefaultOpt
);

// ============================ Render ============================
return (
<ul className={menuPrefixCls} role="menu">
{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])

Check notice

Code scanning / CodeQL

Semicolon insertion Note

Avoid automated semicolon insertion (90% of all statements in
the enclosing function
have an explicit semicolon).

const renderLi = (item) => {
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 (
<li
key={fullPathKey}
className={classNames(menuItemPrefixCls, {
[`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
[`${menuItemPrefixCls}-active`]:
activeValue === value || activeValue === fullPathKey,
[`${menuItemPrefixCls}-disabled`]: isOptionDisabled(disabled),
[`${menuItemPrefixCls}-loading`]: isLoading,
})}
style={dropdownMenuColumnStyle}
role="menuitemcheckbox"
title={title}
aria-checked={checked}
data-path-key={fullPathKey}
onClick={() => {
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 && (
<Checkbox
prefixCls={`${prefixCls}-checkbox`}
checked={checked}
halfChecked={halfChecked}
disabled={isOptionDisabled(disabled) || disableCheckbox}
disableCheckbox={disableCheckbox}
onClick={(e: React.MouseEvent<HTMLSpanElement>) => {
if (disableCheckbox) {
return;
}
e.stopPropagation();
triggerSelect();
}}
/>
)}
<div className={`${menuItemPrefixCls}-content`}>
{optionRender ? optionRender(option) : label}
</div>
{!isLoading && expandIcon && !isMergedLeaf && (
<div className={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div>
)}
{isLoading && loadingIcon && (
<div className={`${menuItemPrefixCls}-loading-icon`}>{loadingIcon}</div>
)}
</li>
);
};

// >>>>> Render
return (
<li
key={fullPathKey}
className={classNames(menuItemPrefixCls, {
[`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
[`${menuItemPrefixCls}-active`]:
activeValue === value || activeValue === fullPathKey,
[`${menuItemPrefixCls}-disabled`]: isOptionDisabled(disabled),
[`${menuItemPrefixCls}-loading`]: isLoading,
})}
style={dropdownMenuColumnStyle}
role="menuitemcheckbox"
title={title}
aria-checked={checked}
data-path-key={fullPathKey}
onClick={() => {
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 && (
<Checkbox
prefixCls={`${prefixCls}-checkbox`}
checked={checked}
halfChecked={halfChecked}
disabled={isOptionDisabled(disabled) || disableCheckbox}
disableCheckbox={disableCheckbox}
onClick={(e: React.MouseEvent<HTMLSpanElement>) => {
if (disableCheckbox) {
return;
}
e.stopPropagation();
triggerSelect();
}}
/>
)}
<div className={`${menuItemPrefixCls}-content`}>
{optionRender ? optionRender(option) : label}
</div>
{!isLoading && expandIcon && !isMergedLeaf && (
<div className={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div>
)}
{isLoading && loadingIcon && (
<div className={`${menuItemPrefixCls}-loading-icon`}>{loadingIcon}</div>
)}
</li>
);
},
return (
<ul className={menuPrefixCls} role="menu">
{virtual ? (
<List
ref={ref}
itemKey="fullPathKey"
height={listHeight}
itemHeight={listItemHeight}
virtual={virtual}
data={optionInfoList}
>
{renderLi}
</List>
) : (
optionInfoList.map(renderLi)
)}
</ul>
);
Expand Down
3 changes: 3 additions & 0 deletions src/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CascaderContextProps>({} as CascaderContextProps);
Expand Down

0 comments on commit ae863de

Please sign in to comment.