Skip to content

Commit 30125ac

Browse files
author
魄兵
committed
feat: virtual list
1 parent 751451f commit 30125ac

File tree

4 files changed

+156
-110
lines changed

4 files changed

+156
-110
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@
4646
"classnames": "^2.3.1",
4747
"rc-select": "~14.16.2",
4848
"rc-tree": "~5.10.1",
49-
"rc-util": "^5.43.0"
49+
"rc-util": "^5.43.0",
50+
"rc-virtual-list": "^3.14.8"
5051
},
5152
"devDependencies": {
5253
"@rc-component/father-plugin": "^1.0.0",

src/Cascader.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,10 @@ interface BaseCascaderProps<
119119
// Icon
120120
expandIcon?: React.ReactNode;
121121
loadingIcon?: React.ReactNode;
122+
123+
virtual?: boolean;
124+
listHeight?: number;
125+
listItemHeight?: number;
122126
}
123127

124128
export interface FieldNames<
@@ -232,6 +236,9 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
232236
dropdownMatchSelectWidth = false,
233237
showCheckedStrategy = SHOW_PARENT,
234238
optionRender,
239+
virtual = true,
240+
listHeight = 170,
241+
listItemHeight = 28,
235242
...restProps
236243
} = props;
237244

@@ -407,6 +414,9 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
407414
loadingIcon,
408415
dropdownMenuColumnStyle,
409416
optionRender,
417+
virtual,
418+
listHeight,
419+
listItemHeight,
410420
}),
411421
[
412422
mergedOptions,
@@ -424,6 +434,9 @@ const Cascader = React.forwardRef<CascaderRef, InternalCascaderProps>((props, re
424434
loadingIcon,
425435
dropdownMenuColumnStyle,
426436
optionRender,
437+
virtual,
438+
listHeight,
439+
listItemHeight,
427440
],
428441
);
429442

src/OptionList/Column.tsx

Lines changed: 138 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import CascaderContext from '../context';
55
import { SEARCH_MARK } from '../hooks/useSearchOptions';
66
import { isLeaf, toPathKey } from '../utils/commonUtil';
77
import Checkbox from './Checkbox';
8+
import List from 'rc-virtual-list';
9+
import type { ListRef } from 'rc-virtual-list';
810

911
export const FIX_LABEL = '__cascader_fix_label__';
1012

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

@@ -52,6 +55,9 @@ export default function Column<OptionType extends DefaultOptionType = DefaultOpt
5255
loadingIcon,
5356
dropdownMenuColumnStyle,
5457
optionRender,
58+
virtual,
59+
listItemHeight,
60+
listHeight,
5561
} = React.useContext(CascaderContext);
5662

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

103109
// ============================ Render ============================
104-
return (
105-
<ul className={menuPrefixCls} role="menu">
106-
{optionInfoList.map(
107-
({
108-
disabled,
109-
label,
110-
value,
111-
isLeaf: isMergedLeaf,
112-
isLoading,
113-
checked,
114-
halfChecked,
115-
option,
116-
fullPath,
117-
fullPathKey,
118-
disableCheckbox,
119-
}) => {
120-
// >>>>> Open
121-
const triggerOpenPath = () => {
122-
if (isOptionDisabled(disabled)) {
123-
return;
124-
}
125-
const nextValueCells = [...fullPath];
126-
if (hoverOpen && isMergedLeaf) {
127-
nextValueCells.pop();
128-
}
129-
onActive(nextValueCells);
130-
};
131-
132-
// >>>>> Selection
133-
const triggerSelect = () => {
134-
if (isSelectable(option) && !isOptionDisabled(disabled)) {
135-
onSelect(fullPath, isMergedLeaf);
136-
}
137-
};
138-
139-
// >>>>> Title
140-
let title: string | undefined;
141-
if (typeof option.title === 'string') {
142-
title = option.title;
143-
} else if (typeof label === 'string') {
144-
title = label;
110+
111+
// scrollIntoView effect in virtual list
112+
React.useEffect(() => {
113+
if (virtual && ref.current && activeValue) {
114+
const startIndex = optionInfoList.findIndex(({ value }) => value === activeValue);
115+
ref.current.scrollTo({ index: startIndex, align: 'auto' });
116+
}
117+
}, [optionInfoList, virtual, activeValue])
118+
119+
const renderLi = (item: typeof optionInfoList[0]) => {
120+
const {
121+
disabled,
122+
label,
123+
value,
124+
isLeaf: isMergedLeaf,
125+
isLoading,
126+
checked,
127+
halfChecked,
128+
option,
129+
fullPath,
130+
fullPathKey,
131+
disableCheckbox
132+
} = item;
133+
134+
const triggerOpenPath = () => {
135+
if (isOptionDisabled(disabled)) {
136+
return;
137+
}
138+
const nextValueCells = [...fullPath];
139+
if (hoverOpen && isMergedLeaf) {
140+
nextValueCells.pop();
141+
}
142+
onActive(nextValueCells);
143+
};
144+
145+
// >>>>> Selection
146+
const triggerSelect = () => {
147+
if (isSelectable(option) && !isOptionDisabled(disabled)) {
148+
onSelect(fullPath, isMergedLeaf);
149+
}
150+
};
151+
152+
// >>>>> Title
153+
let title: string | undefined;
154+
if (typeof option.title === 'string') {
155+
title = option.title;
156+
} else if (typeof label === 'string') {
157+
title = label;
158+
}
159+
160+
// >>>>> Render
161+
return (
162+
<li
163+
key={fullPathKey}
164+
className={classNames(menuItemPrefixCls, {
165+
[`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
166+
[`${menuItemPrefixCls}-active`]:
167+
activeValue === value || activeValue === fullPathKey,
168+
[`${menuItemPrefixCls}-disabled`]: isOptionDisabled(disabled),
169+
[`${menuItemPrefixCls}-loading`]: isLoading,
170+
})}
171+
style={dropdownMenuColumnStyle}
172+
role="menuitemcheckbox"
173+
title={title}
174+
aria-checked={checked}
175+
data-path-key={fullPathKey}
176+
onClick={() => {
177+
triggerOpenPath();
178+
if (disableCheckbox) {
179+
return;
180+
}
181+
if (!multiple || isMergedLeaf) {
182+
triggerSelect();
145183
}
184+
}}
185+
onDoubleClick={() => {
186+
if (changeOnSelect) {
187+
onToggleOpen(false);
188+
}
189+
}}
190+
onMouseEnter={() => {
191+
if (hoverOpen) {
192+
triggerOpenPath();
193+
}
194+
}}
195+
onMouseDown={e => {
196+
// Prevent selector from blurring
197+
e.preventDefault();
198+
}}
199+
>
200+
{multiple && (
201+
<Checkbox
202+
prefixCls={`${prefixCls}-checkbox`}
203+
checked={checked}
204+
halfChecked={halfChecked}
205+
disabled={isOptionDisabled(disabled) || disableCheckbox}
206+
disableCheckbox={disableCheckbox}
207+
onClick={(e: React.MouseEvent<HTMLSpanElement>) => {
208+
if (disableCheckbox) {
209+
return;
210+
}
211+
e.stopPropagation();
212+
triggerSelect();
213+
}}
214+
/>
215+
)}
216+
<div className={`${menuItemPrefixCls}-content`}>
217+
{optionRender ? optionRender(option) : label}
218+
</div>
219+
{!isLoading && expandIcon && !isMergedLeaf && (
220+
<div className={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div>
221+
)}
222+
{isLoading && loadingIcon && (
223+
<div className={`${menuItemPrefixCls}-loading-icon`}>{loadingIcon}</div>
224+
)}
225+
</li>
226+
);
227+
};
146228

147-
// >>>>> Render
148-
return (
149-
<li
150-
key={fullPathKey}
151-
className={classNames(menuItemPrefixCls, {
152-
[`${menuItemPrefixCls}-expand`]: !isMergedLeaf,
153-
[`${menuItemPrefixCls}-active`]:
154-
activeValue === value || activeValue === fullPathKey,
155-
[`${menuItemPrefixCls}-disabled`]: isOptionDisabled(disabled),
156-
[`${menuItemPrefixCls}-loading`]: isLoading,
157-
})}
158-
style={dropdownMenuColumnStyle}
159-
role="menuitemcheckbox"
160-
title={title}
161-
aria-checked={checked}
162-
data-path-key={fullPathKey}
163-
onClick={() => {
164-
triggerOpenPath();
165-
if (disableCheckbox) {
166-
return;
167-
}
168-
if (!multiple || isMergedLeaf) {
169-
triggerSelect();
170-
}
171-
}}
172-
onDoubleClick={() => {
173-
if (changeOnSelect) {
174-
onToggleOpen(false);
175-
}
176-
}}
177-
onMouseEnter={() => {
178-
if (hoverOpen) {
179-
triggerOpenPath();
180-
}
181-
}}
182-
onMouseDown={e => {
183-
// Prevent selector from blurring
184-
e.preventDefault();
185-
}}
186-
>
187-
{multiple && (
188-
<Checkbox
189-
prefixCls={`${prefixCls}-checkbox`}
190-
checked={checked}
191-
halfChecked={halfChecked}
192-
disabled={isOptionDisabled(disabled) || disableCheckbox}
193-
disableCheckbox={disableCheckbox}
194-
onClick={(e: React.MouseEvent<HTMLSpanElement>) => {
195-
if (disableCheckbox) {
196-
return;
197-
}
198-
e.stopPropagation();
199-
triggerSelect();
200-
}}
201-
/>
202-
)}
203-
<div className={`${menuItemPrefixCls}-content`}>
204-
{optionRender ? optionRender(option) : label}
205-
</div>
206-
{!isLoading && expandIcon && !isMergedLeaf && (
207-
<div className={`${menuItemPrefixCls}-expand-icon`}>{expandIcon}</div>
208-
)}
209-
{isLoading && loadingIcon && (
210-
<div className={`${menuItemPrefixCls}-loading-icon`}>{loadingIcon}</div>
211-
)}
212-
</li>
213-
);
214-
},
229+
return (
230+
<ul className={menuPrefixCls} role="menu">
231+
{virtual ? (
232+
<List
233+
ref={ref}
234+
itemKey="fullPathKey"
235+
height={listHeight}
236+
itemHeight={listItemHeight}
237+
virtual={virtual}
238+
data={optionInfoList}
239+
>
240+
{renderLi}
241+
</List>
242+
) : (
243+
optionInfoList.map(renderLi)
215244
)}
216245
</ul>
217246
);

src/context.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ export interface CascaderContextProps {
2222
loadingIcon?: React.ReactNode;
2323
dropdownMenuColumnStyle?: React.CSSProperties;
2424
optionRender?: CascaderProps['optionRender'];
25+
virtual?: boolean;
26+
listHeight?: number;
27+
listItemHeight?: number;
2528
}
2629

2730
const CascaderContext = React.createContext<CascaderContextProps>({} as CascaderContextProps);

0 commit comments

Comments
 (0)