Skip to content

Commit b2aa49d

Browse files
committed
fix: select dynamic virtual list
1 parent 604372f commit b2aa49d

File tree

3 files changed

+102
-89
lines changed

3 files changed

+102
-89
lines changed

components/vc-virtual-list/List.tsx

+86-71
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
onBeforeUnmount,
1010
reactive,
1111
CSSProperties,
12+
watch,
1213
} from 'vue';
1314
import { Key } from '../_util/type';
1415
import Filler from './Filler';
@@ -53,10 +54,9 @@ function renderChildren<T>(
5354
});
5455
}
5556

56-
export interface ListState<T = object> {
57+
export interface ListState {
5758
scrollTop: number;
5859
scrollMoving: boolean;
59-
mergedData: T[];
6060
}
6161

6262
const List = defineComponent({
@@ -97,7 +97,10 @@ const List = defineComponent({
9797
const state = reactive<ListState>({
9898
scrollTop: 0,
9999
scrollMoving: false,
100-
mergedData: computed(() => props.data || EMPTY_DATA) as any,
100+
});
101+
102+
const mergedData = computed(() => {
103+
return props.data || EMPTY_DATA;
101104
});
102105

103106
const componentRef = ref<HTMLDivElement>();
@@ -108,7 +111,7 @@ const List = defineComponent({
108111
if (typeof props.itemKey === 'function') {
109112
return props.itemKey(item);
110113
}
111-
return item[props.itemKey];
114+
return item?.[props.itemKey];
112115
};
113116

114117
const sharedConfig = {
@@ -135,83 +138,94 @@ const List = defineComponent({
135138
// ================================ Height ================================
136139
const [setInstance, collectHeight, heights] = useHeights(getKey, null, null);
137140

138-
const calRes = ref();
139-
watchEffect(() => {
140-
if (!useVirtual.value) {
141-
calRes.value = {
142-
scrollHeight: undefined,
143-
start: 0,
144-
end: state.mergedData.length - 1,
145-
offset: undefined,
146-
};
147-
return;
148-
}
149-
150-
// Always use virtual scroll bar in avoid shaking
151-
if (!inVirtual.value) {
152-
calRes.value = {
153-
scrollHeight: fillerInnerRef.value?.offsetHeight || 0,
154-
start: 0,
155-
end: state.mergedData.length - 1,
156-
offset: undefined,
157-
};
158-
return;
159-
}
160-
161-
let itemTop = 0;
162-
let startIndex: number | undefined;
163-
let startOffset: number | undefined;
164-
let endIndex: number | undefined;
165-
const dataLen = state.mergedData.length;
166-
for (let i = 0; i < dataLen; i += 1) {
167-
const item = state.mergedData[i];
168-
const key = getKey(item);
169-
170-
const cacheHeight = heights[key];
171-
const currentItemBottom =
172-
itemTop + (cacheHeight === undefined ? props.itemHeight! : cacheHeight);
173-
174-
if (currentItemBottom >= state.scrollTop && startIndex === undefined) {
175-
startIndex = i;
176-
startOffset = itemTop;
141+
const calRes = ref<{
142+
scrollHeight?: number;
143+
start?: number;
144+
end?: number;
145+
offset?: number;
146+
}>({});
147+
watch(
148+
[inVirtual, useVirtual, () => state.scrollTop, mergedData, heights, () => props.height],
149+
() => {
150+
if (!useVirtual.value) {
151+
calRes.value = {
152+
scrollHeight: undefined,
153+
start: 0,
154+
end: mergedData.value.length - 1,
155+
offset: undefined,
156+
};
157+
return;
177158
}
178159

179-
// Check item bottom in the range. We will render additional one item for motion usage
180-
if (currentItemBottom > state.scrollTop + props.height! && endIndex === undefined) {
181-
endIndex = i;
160+
// Always use virtual scroll bar in avoid shaking
161+
if (!inVirtual.value) {
162+
calRes.value = {
163+
scrollHeight: fillerInnerRef.value?.offsetHeight || 0,
164+
start: 0,
165+
end: mergedData.value.length - 1,
166+
offset: undefined,
167+
};
168+
return;
182169
}
183170

184-
itemTop = currentItemBottom;
185-
}
171+
let itemTop = 0;
172+
let startIndex: number | undefined;
173+
let startOffset: number | undefined;
174+
let endIndex: number | undefined;
175+
const dataLen = mergedData.value.length;
176+
const data = mergedData.value;
177+
for (let i = 0; i < dataLen; i += 1) {
178+
const item = data[i];
179+
const key = getKey(item);
180+
181+
const cacheHeight = heights[key];
182+
const currentItemBottom =
183+
itemTop + (cacheHeight === undefined ? props.itemHeight! : cacheHeight);
184+
185+
if (currentItemBottom >= state.scrollTop && startIndex === undefined) {
186+
startIndex = i;
187+
startOffset = itemTop;
188+
}
186189

187-
// Fallback to normal if not match. This code should never reach
188-
/* istanbul ignore next */
189-
if (startIndex === undefined) {
190-
startIndex = 0;
191-
startOffset = 0;
192-
}
193-
if (endIndex === undefined) {
194-
endIndex = state.mergedData.length - 1;
195-
}
190+
// Check item bottom in the range. We will render additional one item for motion usage
191+
if (currentItemBottom > state.scrollTop + props.height! && endIndex === undefined) {
192+
endIndex = i;
193+
}
196194

197-
// Give cache to improve scroll experience
198-
endIndex = Math.min(endIndex + 1, state.mergedData.length);
199-
calRes.value = {
200-
scrollHeight: itemTop,
201-
start: startIndex,
202-
end: endIndex,
203-
offset: startOffset,
204-
};
205-
});
195+
itemTop = currentItemBottom;
196+
}
197+
198+
// Fallback to normal if not match. This code should never reach
199+
/* istanbul ignore next */
200+
if (startIndex === undefined) {
201+
startIndex = 0;
202+
startOffset = 0;
203+
}
204+
if (endIndex === undefined) {
205+
endIndex = dataLen - 1;
206+
}
207+
208+
// Give cache to improve scroll experience
209+
endIndex = Math.min(endIndex + 1, dataLen);
210+
calRes.value = {
211+
scrollHeight: itemTop,
212+
start: startIndex,
213+
end: endIndex,
214+
offset: startOffset,
215+
};
216+
},
217+
{ immediate: true },
218+
);
206219

207220
// =============================== In Range ===============================
208221
const maxScrollHeight = computed(() => calRes.value.scrollHeight! - props.height!);
209222

210223
function keepInRange(newScrollTop: number) {
211-
let newTop = Math.max(newScrollTop, 0);
224+
let newTop = newScrollTop;
212225
if (!Number.isNaN(maxScrollHeight.value)) {
213226
newTop = Math.min(newTop, maxScrollHeight.value);
214227
}
228+
newTop = Math.max(newTop, 0);
215229
return newTop;
216230
}
217231

@@ -226,8 +240,7 @@ const List = defineComponent({
226240
syncScrollTop(newTop);
227241
}
228242

229-
// This code may only trigger in test case.
230-
// But we still need a sync if some special escape
243+
// When data size reduce. It may trigger native scroll event back to fit scroll position
231244
function onFallbackScroll(e: UIEvent) {
232245
const { scrollTop: newScrollTop } = e.currentTarget as Element;
233246
if (Math.abs(newScrollTop - state.scrollTop) >= 1) {
@@ -299,7 +312,7 @@ const List = defineComponent({
299312
// ================================= Ref ==================================
300313
const scrollTo = useScrollTo(
301314
componentRef,
302-
state,
315+
mergedData,
303316
heights,
304317
props,
305318
getKey,
@@ -328,6 +341,7 @@ const List = defineComponent({
328341

329342
return {
330343
state,
344+
mergedData,
331345
componentStyle,
332346
scrollTo,
333347
onFallbackScroll,
@@ -360,7 +374,7 @@ const List = defineComponent({
360374
...restProps
361375
} = { ...this.$props, ...this.$attrs } as any;
362376
const mergedClassName = classNames(prefixCls, className);
363-
const { scrollTop, mergedData } = this.state;
377+
const { scrollTop } = this.state;
364378
const { scrollHeight, offset, start, end } = this.calRes;
365379
const {
366380
componentStyle,
@@ -370,6 +384,7 @@ const List = defineComponent({
370384
collectHeight,
371385
sharedConfig,
372386
setInstance,
387+
mergedData,
373388
} = this;
374389
const listChildren = renderChildren(
375390
mergedData,

components/vc-virtual-list/ScrollBar.tsx

+10-13
Original file line numberDiff line numberDiff line change
@@ -208,37 +208,34 @@ export default defineComponent({
208208
const ptg = scrollTop / enableScrollRange;
209209
return ptg * enableHeightRange;
210210
},
211-
// Not show scrollbar when height is large thane scrollHeight
212-
getVisible() {
213-
const { visible } = this.state;
211+
// Not show scrollbar when height is large than scrollHeight
212+
showScroll() {
214213
const { height, scrollHeight } = this.$props;
215-
216-
if (height >= scrollHeight) {
217-
return false;
218-
}
219-
220-
return visible;
214+
return scrollHeight > height;
221215
},
222216
},
223217

224218
render() {
225219
// eslint-disable-next-line no-unused-vars
226-
const { dragging } = this.state;
220+
const { dragging, visible } = this.state;
227221
const { prefixCls } = this.$props;
228222
const spinHeight = this.getSpinHeight() + 'px';
229223
const top = this.getTop() + 'px';
230-
const visible = this.getVisible();
224+
const canScroll = this.showScroll();
225+
const mergedVisible = canScroll && visible;
231226
return (
232227
<div
233228
ref={this.scrollbarRef}
234-
class={`${prefixCls}-scrollbar`}
229+
class={classNames(`${prefixCls}-scrollbar`, {
230+
[`${prefixCls}-scrollbar-show`]: canScroll,
231+
})}
235232
style={{
236233
width: '8px',
237234
top: 0,
238235
bottom: 0,
239236
right: 0,
240237
position: 'absolute',
241-
display: visible ? undefined : 'none',
238+
display: mergedVisible ? undefined : 'none',
242239
}}
243240
onMousedown={this.onContainerMouseDown}
244241
onMousemove={this.delayHidden}

components/vc-virtual-list/hooks/useScrollTo.tsx

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
11
import { Data } from '../../_util/type';
2-
import { Ref } from 'vue';
2+
import { ComputedRef, Ref } from 'vue';
33
import raf from '../../_util/raf';
44
import { GetKey } from '../interface';
5-
import { ListState } from '../List';
65

76
export default function useScrollTo(
87
containerRef: Ref<Element | undefined>,
9-
state: ListState,
8+
mergedData: ComputedRef<any[]>,
109
heights: Data,
1110
props,
1211
getKey: GetKey,
@@ -25,7 +24,7 @@ export default function useScrollTo(
2524

2625
// Normal scroll logic
2726
raf.cancel(scroll!);
28-
const data = state.mergedData;
27+
const data = mergedData.value;
2928
const itemHeight = props.itemHeight;
3029
if (typeof arg === 'number') {
3130
syncScrollTop(arg);
@@ -58,7 +57,9 @@ export default function useScrollTo(
5857
let itemTop = 0;
5958
let itemBottom = 0;
6059

61-
for (let i = 0; i <= index; i += 1) {
60+
const maxLen = Math.min(data.length, index);
61+
62+
for (let i = 0; i <= maxLen; i += 1) {
6263
const key = getKey(data[i]);
6364
itemTop = stackTop;
6465
const cacheHeight = heights[key!];

0 commit comments

Comments
 (0)