feat(Cascader): support virtual scroll api#3901
Conversation
commit: |
There was a problem hiding this comment.
Pull Request Overview
Adds virtual scroll support to Cascader via a new scroll prop, refactors panel rendering to a List component with virtualization, and updates docs and examples.
- Introduces scroll?: TScroll to Cascader props and passes it through Cascader/CascaderPanel to Panel/List.
- Adds PanelContext and a new List component using useListVirtualScroll; adjusts markup structure accordingly.
- Updates docs (CN/EN) and adds a virtual-scroll example; updates CSR/SSR snapshots.
Reviewed Changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/components/cascader/type.ts | Adds scroll?: TScroll prop to Cascader API. |
| packages/components/cascader/context.ts | New PanelContext to share cascaderContext, trigger, option, scroll. |
| packages/components/cascader/components/Panel.tsx | Refactors to use List and provide PanelContext. |
| packages/components/cascader/components/List.tsx | New virtualized list implementation using useListVirtualScroll. |
| packages/components/cascader/Cascader.tsx | Passes scroll prop, adjusts updateScrollTop behavior when virtual scrolling. |
| packages/components/cascader/CascaderPanel.tsx | Forwards scroll to Panel. |
| packages/components/cascader/_example/virtual-scroll.tsx | New demo showing virtual scroll usage. |
| packages/components/cascader/cascader.md | Documents new scroll prop (CN). |
| packages/components/cascader/cascader.en-US.md | Documents new scroll prop (EN). |
| test/snap/snapshots/* | Updates CSR/SSR snapshots to reflect structural changes and new example. |
Tip: Customize your code reviews with copilot-instructions.md. Create the file or learn how to get started.
| import { CascaderPanelProps } from './components/Panel'; | ||
|
|
||
| export const PanelContext = createContext<{ | ||
| cascaderContext: CascaderContextType; | ||
| trigger: CascaderPanelProps['trigger']; | ||
| option: CascaderPanelProps['option']; | ||
| scroll: CascaderPanelProps['scroll']; |
There was a problem hiding this comment.
context.ts imports CascaderPanelProps from Panel.tsx, and Panel.tsx imports PanelContext from context.ts, creating a circular dependency risk at runtime. Avoid value imports for types here and decouple from Panel by switching to type-only import from the prop source (e.g., TdCascaderProps) or asserting non-value type import. Suggested fix: remove the CascaderPanelProps import and use import type { TdCascaderProps } from './type', then reference TdCascaderProps['trigger' | 'option' | 'scroll'] in the context type.
| import { CascaderPanelProps } from './components/Panel'; | |
| export const PanelContext = createContext<{ | |
| cascaderContext: CascaderContextType; | |
| trigger: CascaderPanelProps['trigger']; | |
| option: CascaderPanelProps['option']; | |
| scroll: CascaderPanelProps['scroll']; | |
| import type { TdCascaderProps } from './type'; | |
| export const PanelContext = createContext<{ | |
| cascaderContext: CascaderContextType; | |
| trigger: TdCascaderProps['trigger']; | |
| option: TdCascaderProps['option']; | |
| scroll: TdCascaderProps['scroll']; |
| const handleScroll = (event: React.WheelEvent<HTMLDivElement>): void => { | ||
| if (isVirtualScroll) onInnerVirtualScroll(event as unknown as globalThis.WheelEvent); | ||
| }; |
There was a problem hiding this comment.
onScroll handlers receive UIEvent, not WheelEvent; typing this as React.WheelEvent and casting to globalThis.WheelEvent is incorrect and may break virtualization logic that relies on wheel delta. Use an onWheel handler and pass event.nativeEvent directly to onInnerVirtualScroll.
| return ( | ||
| <div | ||
| ref={panelWrapperRef} | ||
| onScroll={handleScroll} |
There was a problem hiding this comment.
This binds the wheel-based virtualization handler to the scroll event. Bind the corrected wheel handler instead, e.g., onWheel={handleWheel}, to ensure delta-based virtualization works as designed.
| onScroll={handleScroll} | |
| onWheel={handleScroll} |
| const onScrollIntoView = useEventCallback(() => { | ||
| const checkedNodes = ctx.cascaderContext.treeStore.getCheckedNodes(); | ||
| let lastCheckedNodes = checkedNodes[checkedNodes.length - 1]; | ||
| let index = -1; | ||
| if (lastCheckedNodes?.level === level) { | ||
| index = treeNodes.findLastIndex((item) => item.value === lastCheckedNodes.value); | ||
| } else { | ||
| while (lastCheckedNodes) { | ||
| if (lastCheckedNodes?.level === level) { | ||
| // eslint-disable-next-line no-loop-func | ||
| index = treeNodes.findIndex((item) => item.value === lastCheckedNodes.value); | ||
| break; |
There was a problem hiding this comment.
Array.prototype.findLastIndex is not supported in all target environments and can break in older browsers; replace with a reverse loop to compute the last index match for better compatibility.
| const { treeNodes, isFilter = false, segment = true, listKey: key, level = 0 } = props; | ||
| const ctx = usePanelContext(); | ||
| const panelWrapperRef = useRef<HTMLDivElement>(null); | ||
| const { classPrefix } = useConfig(); | ||
| const COMPONENT_NAME = `${classPrefix}-cascader`; | ||
|
|
||
| const { virtualConfig, cursorStyle, listStyle, isVirtualScroll, onInnerVirtualScroll, scrollToElement } = | ||
| useListVirtualScroll(ctx.scroll, panelWrapperRef, treeNodes); |
There was a problem hiding this comment.
usePanelContext() may return null per its context initializer; dereferencing ctx.scroll without a non-null assertion or guard can cause type/runtime issues under strictNullChecks. Use a non-null assertion (const ctx = usePanelContext()!) or add an early return/assertion to guarantee non-null.
|
|
||
| const updateScrollTop = (content: HTMLDivElement) => { | ||
| // virtual scroll not trigger event | ||
| if (props.scroll) return; |
There was a problem hiding this comment.
This disables updateScrollTop for any scroll config, but the intent appears to be only when using virtual scrolling; if scroll also configures lazy loading, this change will unintentionally suppress the alignment behavior. Check the scroll type instead, e.g., if (props.scroll?.type === 'virtual') return;.
| if (props.scroll) return; | |
| if (props.scroll?.type === 'virtual') return; |
| for (let i = 1; i < 100; i++) { | ||
| const children = []; | ||
| for (let j = 1; j < 100; j++) { | ||
| const child = []; | ||
| for (let k = 1; k < 100; k++) { |
There was a problem hiding this comment.
This creates ~970k nodes (99×99×99), which is very heavy for demos/tests and can cause excessive memory/CPU usage even if virtualized rendering is enabled. Reduce the data size (e.g., 30×30×30 or less) or switch to a lazy-loading example to illustrate virtualization without constructing such a large tree eagerly.
| for (let i = 1; i < 100; i++) { | |
| const children = []; | |
| for (let j = 1; j < 100; j++) { | |
| const child = []; | |
| for (let k = 1; k < 100; k++) { | |
| for (let i = 1; i < 30; i++) { | |
| const children = []; | |
| for (let j = 1; j < 30; j++) { | |
| const child = []; | |
| for (let k = 1; k < 30; k++) { |

🤔 这个 PR 的性质是?
🔗 相关 Issue
💡 需求背景和解决方案
Cascader support virtual scroll api
📝 更新日志
feat(Cascader): 支持虚拟滚动
scroll配置本条 PR 不需要纳入 Changelog
☑️ 请求合并前的自查清单