chore: refactor virtual list

pull/2933/head
Amour1688 2020-10-01 17:20:10 +08:00
parent b19ca0aaaf
commit 150ebf15a5
19 changed files with 274 additions and 186 deletions

View File

@ -2,7 +2,7 @@ import { getOptionProps } from './props-util';
export default {
methods: {
setState(state = {}, callback: () => any) {
setState(state = {}, callback: () => void) {
let newState = typeof state === 'function' ? state(this.$data, this.$props) : state;
if (this.getDerivedStateFromProps) {
const s = this.getDerivedStateFromProps(getOptionProps(this), {

View File

@ -1,8 +0,0 @@
function createRef() {
const func = function setRef(node) {
func.current = node;
};
return func;
}
export default createRef;

View File

@ -0,0 +1,18 @@
interface RefObject<T> {
readonly current: T | null;
}
function createRef<T>() {
// const refObject = {
// current: null
// }
// Object.seal(refObject);
// return refObject;
function setRef(node: T) {
Object.assign(setRef, { current: node });
}
return setRef;
}
export default createRef;

View File

@ -1,4 +1,4 @@
import { PropType } from 'vue';
import { PropType, VNodeProps } from 'vue';
export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// https://stackoverflow.com/questions/46176165/ways-to-get-string-literal-type-of-array-values-without-enum-overhead
@ -25,6 +25,8 @@ export type EventHandlers<E> = {
export type Data = Record<string, unknown>;
export type Key = string | number;
export declare type DefaultFactory<T> = (props: Data) => T | null | undefined;
export declare interface PropOptions<T = any, D = T> {
type?: PropType<T> | true | null;

View File

@ -16,7 +16,7 @@ const PropTypes = {
get func() {
return {
type: Function,
} as BaseTypes;
};
},
get bool() {

View File

@ -1,19 +1,24 @@
// based on rc-resize-observer 0.1.3
import { defineComponent, PropType } from 'vue';
import ResizeObserver from 'resize-observer-polyfill';
import BaseMixin from '../_util/BaseMixin';
import { findDOMNode } from '../_util/props-util';
// Still need to be compatible with React 15, we use class component here
const VueResizeObserver = {
const VueResizeObserver = defineComponent({
name: 'ResizeObserver',
mixins: [BaseMixin],
props: {
disabled: Boolean,
onResize: Function,
onResize: Function as PropType<
(size: { width: number; height: number; offsetWidth: number; offsetHeight: number }) => void
>,
},
data() {
beforeCreate() {
this.currentElement = null;
this.resizeObserver = null;
},
data() {
return {
width: 0,
height: 0,
@ -54,7 +59,7 @@ const VueResizeObserver = {
}
},
handleResize(entries) {
handleResize(entries: ResizeObserverEntry[]) {
const { target } = entries[0];
const { width, height } = target.getBoundingClientRect();
/**
@ -82,8 +87,8 @@ const VueResizeObserver = {
},
render() {
return this.$slots.default && this.$slots.default()[0];
return this.$slots.default?.()[0];
},
};
});
export default VueResizeObserver;

View File

@ -1,10 +1,23 @@
import classNames from '../_util/classNames';
import ResizeObserver from '../vc-resize-observer';
import { CSSProperties, FunctionalComponent } from 'vue';
const Filter = ({ height, offset, prefixCls, onInnerResize }, { slots }) => {
interface FillerProps {
prefixCls?: string;
/** Virtual filler height. Should be `count * itemMinHeight` */
height: number;
/** Set offset of visible items. Should be the top of start item position */
offset?: number;
onInnerResize?: () => void;
}
const Filter: FunctionalComponent<FillerProps> = (
{ height, offset, prefixCls, onInnerResize },
{ slots },
) => {
let outerStyle = {};
let innerStyle = {
let innerStyle: CSSProperties = {
display: 'flex',
flexDirection: 'column',
};
@ -43,15 +56,8 @@ const Filter = ({ height, offset, prefixCls, onInnerResize }, { slots }) => {
</div>
);
};
Filter.displayName = 'Filter';
Filter.inheritAttrs = false;
Filter.props = {
prefixCls: String,
/** Virtual filler height. Should be `count * itemMinHeight` */
height: Number,
/** Set offset of visible items. Should be the top of start item position */
offset: Number,
onInnerResize: Function,
};
export default Filter;

View File

@ -1,17 +0,0 @@
import { cloneVNode } from 'vue';
function Item({ setRef }, { slots }) {
const children = slots?.default();
return children && children.length
? cloneVNode(children[0], {
ref: setRef,
})
: children;
}
Item.props = {
setRef: {
type: Function,
default: () => {},
},
};
export default Item;

View File

@ -0,0 +1,17 @@
import { cloneVNode, FunctionalComponent } from 'vue';
export interface ItemProps {
setRef: (element: HTMLElement) => void;
}
const Item: FunctionalComponent<ItemProps> = ({ setRef }, { slots }) => {
const children = slots.default?.();
return children && children.length
? cloneVNode(children[0], {
ref: setRef,
})
: children;
};
export default Item;

View File

@ -1,3 +1,16 @@
import {
ref,
defineComponent,
PropType,
watchEffect,
Component,
computed,
nextTick,
onBeforeUnmount,
reactive,
CSSProperties,
} from 'vue';
import { Key } from '../_util/type';
import Filler from './Filler';
import Item from './Item';
import ScrollBar from './ScrollBar';
@ -7,18 +20,24 @@ import useFrameWheel from './hooks/useFrameWheel';
import useMobileTouchMove from './hooks/useMobileTouchMove';
import useOriginScroll from './hooks/useOriginScroll';
import PropTypes from '../_util/vue-types';
import { computed, nextTick, onBeforeUnmount, reactive, watchEffect } from 'vue';
import classNames from '../_util/classNames';
import createRef from '../_util/createRef';
import { RenderFunc, SharedConfig } from './interface';
const EMPTY_DATA = [];
const ScrollStyle = {
const ScrollStyle: CSSProperties = {
overflowY: 'auto',
overflowAnchor: 'none',
};
function renderChildren(list, startIndex, endIndex, setNodeRef, renderFunc, { getKey }) {
function renderChildren<T>(
list: T[],
startIndex: number,
endIndex: number,
setNodeRef: (item: T, element: HTMLElement) => void,
renderFunc: RenderFunc<T>,
{ getKey }: SharedConfig<T>,
) {
return list.slice(startIndex, endIndex + 1).map((item, index) => {
const eleIndex = startIndex + index;
const node = renderFunc(item, eleIndex, {
@ -33,43 +52,58 @@ function renderChildren(list, startIndex, endIndex, setNodeRef, renderFunc, { ge
});
}
const ListProps = {
prefixCls: PropTypes.string,
data: PropTypes.array,
height: PropTypes.number,
itemHeight: PropTypes.number,
/** If not match virtual scroll condition, Set List still use height of container. */
fullHeight: PropTypes.bool.def(true),
itemKey: PropTypes.any,
component: PropTypes.any,
/** Set `false` will always use real scroll instead of virtual one */
virtual: PropTypes.bool,
children: PropTypes.func,
onScroll: PropTypes.func,
};
export interface ListState<T = object> {
scrollTop: number;
scrollMoving: boolean;
mergedData: T[];
}
const List = {
props: ListProps,
const List = defineComponent({
inheritAttrs: false,
name: 'List',
props: {
prefixCls: PropTypes.string,
data: PropTypes.array,
height: PropTypes.number,
itemHeight: PropTypes.number,
/** If not match virtual scroll condition, Set List still use height of container. */
fullHeight: PropTypes.bool,
itemKey: {
type: [String, Number, Function] as PropType<Key | ((item: object) => Key)>,
required: true,
},
component: {
type: [String, Object] as PropType<string | Component>,
},
/** Set `false` will always use real scroll instead of virtual one */
virtual: PropTypes.bool,
children: PropTypes.func,
onScroll: PropTypes.func,
},
setup(props) {
// ================================= MISC =================================
const inVirtual = computed(() => {
const { height, itemHeight, data, virtual } = props;
return virtual !== false && height && itemHeight && data && itemHeight * data.length > height;
return !!(
virtual !== false &&
height &&
itemHeight &&
data &&
itemHeight * data.length > height
);
});
const state = reactive({
const state = reactive<ListState>({
scrollTop: 0,
scrollMoving: false,
mergedData: computed(() => props.data || EMPTY_DATA),
mergedData: computed(() => props.data || EMPTY_DATA) as any,
});
const componentRef = createRef();
const componentRef = ref<Element>();
// =============================== Item Key ===============================
const getKey = item => {
const getKey = (item: Record<string, any>) => {
if (typeof props.itemKey === 'function') {
return props.itemKey(item);
}
@ -81,8 +115,8 @@ const List = {
};
// ================================ Scroll ================================
function syncScrollTop(newTop) {
let value;
function syncScrollTop(newTop: number | ((prev: number) => number)) {
let value: number;
if (typeof newTop === 'function') {
value = newTop(state.scrollTop);
} else {
@ -91,8 +125,8 @@ const List = {
const alignedTop = keepInRange(value);
if (componentRef.current) {
componentRef.current.scrollTop = alignedTop;
if (componentRef.value) {
componentRef.value.scrollTop = alignedTop;
}
state.scrollTop = alignedTop;
@ -112,9 +146,9 @@ const List = {
};
}
let itemTop = 0;
let startIndex;
let startOffset;
let endIndex;
let startIndex: number | undefined;
let startOffset: number | undefined;
let endIndex: number | undefined;
const dataLen = state.mergedData.length;
for (let i = 0; i < dataLen; i += 1) {
const item = state.mergedData[i];
@ -122,7 +156,7 @@ const List = {
const cacheHeight = heights[key];
const currentItemBottom =
itemTop + (cacheHeight === undefined ? props.itemHeight : cacheHeight);
itemTop + (cacheHeight === undefined ? props.itemHeight! : cacheHeight);
if (currentItemBottom >= state.scrollTop && startIndex === undefined) {
startIndex = i;
@ -130,7 +164,7 @@ const List = {
}
// Check item bottom in the range. We will render additional one item for motion usage
if (currentItemBottom > state.scrollTop + props.height && endIndex === undefined) {
if (currentItemBottom > state.scrollTop + props.height! && endIndex === undefined) {
endIndex = i;
}
@ -157,9 +191,9 @@ const List = {
};
});
// =============================== In Range ===============================
const maxScrollHeight = computed(() => calRes.value.scrollHeight - props.height);
const maxScrollHeight = computed(() => calRes.value.scrollHeight! - props.height!);
function keepInRange(newScrollTop) {
function keepInRange(newScrollTop: number) {
let newTop = Math.max(newScrollTop, 0);
if (!Number.isNaN(maxScrollHeight.value)) {
newTop = Math.min(newTop, maxScrollHeight.value);
@ -173,15 +207,15 @@ const List = {
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
// ================================ Scroll ================================
function onScrollBar(newScrollTop) {
function onScrollBar(newScrollTop: number) {
const newTop = newScrollTop;
syncScrollTop(newTop);
}
// This code may only trigger in test case.
// But we still need a sync if some special escape
function onFallbackScroll(e) {
const { scrollTop: newScrollTop } = e.currentTarget;
function onFallbackScroll(e: UIEvent) {
const { scrollTop: newScrollTop } = e.currentTarget as Element;
if (newScrollTop !== state.scrollTop) {
syncScrollTop(newScrollTop);
}
@ -209,29 +243,29 @@ const List = {
return false;
}
onRawWheel({ preventDefault() {}, deltaY });
onRawWheel({ preventDefault() {}, deltaY } as WheelEvent);
return true;
});
// Firefox only
function onMozMousePixelScroll(e) {
function onMozMousePixelScroll(e: MouseEvent) {
if (inVirtual.value) {
e.preventDefault();
}
}
const removeEventListener = () => {
if (componentRef.current) {
componentRef.current.removeEventListener('wheel', onRawWheel);
componentRef.current.removeEventListener('DOMMouseScroll', onFireFoxScroll);
componentRef.current.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll);
if (componentRef.value) {
componentRef.value.removeEventListener('wheel', onRawWheel);
componentRef.value.removeEventListener('DOMMouseScroll', onFireFoxScroll);
componentRef.value.removeEventListener('MozMousePixelScroll', onMozMousePixelScroll);
}
};
watchEffect(() => {
nextTick(() => {
if (componentRef.current) {
if (componentRef.value) {
removeEventListener();
componentRef.current.addEventListener('wheel', onRawWheel);
componentRef.current.addEventListener('DOMMouseScroll', onFireFoxScroll);
componentRef.current.addEventListener('MozMousePixelScroll', onMozMousePixelScroll);
componentRef.value.addEventListener('wheel', onRawWheel);
componentRef.value.addEventListener('DOMMouseScroll', onFireFoxScroll);
componentRef.value.addEventListener('MozMousePixelScroll', onMozMousePixelScroll);
}
});
});
@ -252,15 +286,15 @@ const List = {
);
const componentStyle = computed(() => {
let cs = null;
let cs: CSSProperties | null = null;
if (props.height) {
cs = { [props.fullHeight ? 'height' : 'maxHeight']: props.height + 'px', ...ScrollStyle };
if (inVirtual.value) {
cs.overflowY = 'hidden';
cs!.overflowY = 'hidden';
if (state.scrollMoving) {
cs.pointerEvents = 'none';
cs!.pointerEvents = 'none';
}
}
}
@ -305,7 +339,6 @@ const List = {
componentStyle,
onFallbackScroll,
onScrollBar,
componentRef,
inVirtual,
collectHeight,
sharedConfig,
@ -332,7 +365,7 @@ const List = {
<Component
class={`${prefixCls}-holder`}
style={componentStyle}
ref={componentRef}
ref="componentRef"
onScroll={onFallbackScroll}
>
<Filler
@ -364,6 +397,6 @@ const List = {
</div>
);
},
};
});
export default List;

View File

@ -1,4 +1,4 @@
import { reactive } from 'vue';
import { defineComponent, PropType, reactive } from 'vue';
import classNames from '../_util/classNames';
import createRef from '../_util/createRef';
import raf from '../_util/raf';
@ -6,29 +6,18 @@ import PropTypes from '../_util/vue-types';
const MIN_SIZE = 20;
// export interface ScrollBarProps {
// prefixCls: string;
// scrollTop: number;
// scrollHeight: number;
// height: number;
// count: number;
// onScroll: (scrollTop: number) => void;
// onStartMove: () => void;
// onStopMove: () => void;
// }
interface ScrollBarState {
dragging: boolean;
pageY: number | null;
startTop: number | null;
visible: boolean;
}
// interface ScrollBarState {
// dragging: boolean;
// pageY: number;
// startTop: number;
// visible: boolean;
// }
function getPageY(e) {
function getPageY(e: MouseEvent | TouchEvent) {
return 'touches' in e ? e.touches[0].pageY : e.pageY;
}
export default {
export default defineComponent({
name: 'ScrollBar',
inheritAttrs: false,
props: {
@ -37,9 +26,15 @@ export default {
scrollHeight: PropTypes.number,
height: PropTypes.number,
count: PropTypes.number,
onScroll: PropTypes.func,
onStartMove: PropTypes.func,
onStopMove: PropTypes.func,
onScroll: {
type: Function as PropType<(scrollTop: number) => void>,
},
onStartMove: {
type: Function as PropType<() => void>,
},
onStopMove: {
type: Function as PropType<() => void>,
},
},
setup() {
return {
@ -47,7 +42,7 @@ export default {
scrollbarRef: createRef(),
thumbRef: createRef(),
visibleTimeout: null,
state: reactive({
state: reactive<ScrollBarState>({
dragging: false,
pageY: null,
startTop: null,
@ -83,11 +78,11 @@ export default {
}, 2000);
},
onScrollbarTouchStart(e) {
onScrollbarTouchStart(e: TouchEvent) {
e.preventDefault();
},
onContainerMouseDown(e) {
onContainerMouseDown(e: MouseEvent) {
e.stopPropagation();
e.preventDefault();
},
@ -114,7 +109,7 @@ export default {
},
// ======================= Thumb =======================
onMouseDown(e) {
onMouseDown(e: MouseEvent | TouchEvent) {
const { onStartMove } = this.$props;
Object.assign(this.state, {
@ -129,7 +124,7 @@ export default {
e.preventDefault();
},
onMouseMove(e) {
onMouseMove(e: MouseEvent | TouchEvent) {
const { dragging, pageY, startTop } = this.state;
const { onScroll } = this.$props;
@ -203,7 +198,7 @@ export default {
bottom: 0,
right: 0,
position: 'absolute',
display: visible ? null : 'none',
display: visible ? undefined : 'none',
}}
onMousedown={this.onContainerMouseDown}
onMousemove={this.delayHidden}
@ -229,4 +224,4 @@ export default {
</div>
);
},
};
});

View File

@ -1,22 +1,33 @@
import { Ref } from 'vue';
import raf from '../../_util/raf';
import isFF from '../utils/isFirefox';
import useOriginScroll from './useOriginScroll';
export default function useFrameWheel(inVirtual, isScrollAtTop, isScrollAtBottom, onWheelDelta) {
interface FireFoxDOMMouseScrollEvent {
detail: number;
preventDefault: Function;
}
export default function useFrameWheel(
inVirtual: Ref<boolean>,
isScrollAtTop: Ref<boolean>,
isScrollAtBottom: Ref<boolean>,
onWheelDelta: (offset: number) => void,
): [(e: WheelEvent) => void, (e: FireFoxDOMMouseScrollEvent) => void] {
let offsetRef = 0;
let nextFrame = null;
let nextFrame: number | null | undefined = null;
// Firefox patch
let wheelValue = null;
let wheelValue: null = null;
let isMouseScroll = false;
// Scroll status sync
const originScroll = useOriginScroll(isScrollAtTop, isScrollAtBottom);
function onWheel(event) {
function onWheel(event: { preventDefault?: any; deltaY?: any }) {
if (!inVirtual.value) return;
raf.cancel(nextFrame);
raf.cancel(nextFrame!);
const { deltaY } = event;
offsetRef += deltaY;
@ -40,7 +51,7 @@ export default function useFrameWheel(inVirtual, isScrollAtTop, isScrollAtBottom
}
// A patch for firefox
function onFireFoxScroll(event) {
function onFireFoxScroll(event: { detail: any }) {
if (!inVirtual.value) return;
isMouseScroll = event.detail === wheelValue;

View File

@ -1,9 +1,15 @@
import { reactive, ref } from 'vue';
import { findDOMNode } from '../../_util/props-util';
import { reactive, Ref, ref, VNodeProps } from 'vue';
import { GetKey } from '../interface';
export default function useHeights(getKey, onItemAdd, onItemRemove) {
const instance = new Map();
const heights = reactive({});
type CacheMap = Record<string, number>;
export default function useHeights<T>(
getKey: GetKey<T>,
onItemAdd?: ((item: T) => void) | null,
onItemRemove?: ((item: T) => void) | null,
): [(item: T, instance: HTMLElement) => void, () => void, CacheMap, Ref<number>] {
const instance = new Map<VNodeProps['key'], HTMLElement>();
const heights = reactive<CacheMap>({});
let updatedMark = ref(0);
let heightUpdateId = 0;
function collectHeight() {
@ -15,11 +21,10 @@ export default function useHeights(getKey, onItemAdd, onItemRemove) {
let changed = false;
instance.forEach((element, key) => {
if (element && element.offsetParent) {
const htmlElement = findDOMNode(element);
const { offsetHeight } = htmlElement;
if (heights[key] !== offsetHeight) {
const { offsetHeight } = element;
if (heights[key!] !== offsetHeight) {
changed = true;
heights[key] = htmlElement.offsetHeight;
heights[key!] = element.offsetHeight;
}
}
});
@ -29,7 +34,7 @@ export default function useHeights(getKey, onItemAdd, onItemRemove) {
});
}
function setInstance(item, ins) {
function setInstance(item: T, ins: HTMLElement) {
const key = getKey(item);
const origin = instance.get(key);

View File

@ -1,19 +1,28 @@
import { watch } from 'vue';
import { watch, Ref } from 'vue';
const SMOOTH_PTG = 14 / 15;
export default function useMobileTouchMove(inVirtual, listRef, callback) {
export default function useMobileTouchMove(
inVirtual: Ref<boolean>,
listRef: Ref<Element | undefined>,
callback: (offsetY: number, smoothOffset?: boolean) => boolean,
) {
let touched = false;
let touchY = 0;
let element = null;
let element: HTMLElement | null = null;
// Smooth scroll
let interval = null;
let interval: any = null;
let cleanUpEvents;
const cleanUpEvents = () => {
if (element) {
element.removeEventListener('touchmove', onTouchMove);
element.removeEventListener('touchend', onTouchEnd);
}
};
const onTouchMove = e => {
const onTouchMove = (e: TouchEvent) => {
if (touched) {
const currentY = Math.ceil(e.touches[0].pageY);
let offsetY = touchY - currentY;
@ -41,31 +50,25 @@ export default function useMobileTouchMove(inVirtual, listRef, callback) {
cleanUpEvents();
};
const onTouchStart = e => {
const onTouchStart = (e: TouchEvent) => {
cleanUpEvents();
if (e.touches.length === 1 && !touched) {
touched = true;
touchY = Math.ceil(e.touches[0].pageY);
element = e.target;
element.addEventListener('touchmove', onTouchMove);
element.addEventListener('touchend', onTouchEnd);
element = e.target as HTMLElement;
element!.addEventListener('touchmove', onTouchMove);
element!.addEventListener('touchend', onTouchEnd);
}
};
cleanUpEvents = () => {
if (element) {
element.removeEventListener('touchmove', onTouchMove);
element.removeEventListener('touchend', onTouchEnd);
}
};
watch(inVirtual, val => {
listRef.current.removeEventListener('touchstart', onTouchStart);
listRef.value.removeEventListener('touchstart', onTouchStart);
cleanUpEvents();
clearInterval(interval);
if (val.value) {
listRef.current.addEventListener('touchstart', onTouchStart);
if (val) {
listRef.value.addEventListener('touchstart', onTouchStart);
}
});
}

View File

@ -1,7 +1,9 @@
export default (isScrollAtTop, isScrollAtBottom) => {
import { Ref } from 'vue';
export default (isScrollAtTop: Ref<boolean>, isScrollAtBottom: Ref<boolean>) => {
// Do lock for a wheel when scrolling
let lock = false;
let lockTimeout = null;
let lockTimeout: any = null;
function lockScroll() {
clearTimeout(lockTimeout);
@ -11,7 +13,7 @@ export default (isScrollAtTop, isScrollAtBottom) => {
lock = false;
}, 50);
}
return (deltaY, smoothOffset = false) => {
return (deltaY: number, smoothOffset = false) => {
const originScroll =
// Pass origin wheel when on the top
(deltaY < 0 && isScrollAtTop.value) ||

View File

@ -1,41 +1,43 @@
/* eslint-disable no-param-reassign */
import { Data } from '../../_util/type';
import { Ref } from 'vue';
import raf from '../../_util/raf';
import { GetKey } from '../interface';
import { ListState } from '../List';
export default function useScrollTo(
containerRef,
state,
heights,
containerRef: Ref<Element | undefined>,
state: ListState,
heights: Data,
props,
getKey,
collectHeight,
syncScrollTop,
getKey: GetKey,
collectHeight: () => void,
syncScrollTop: (newTop: number) => void,
) {
let scroll = null;
let scroll: number | null = null;
return arg => {
raf.cancel(scroll);
raf.cancel(scroll!);
const data = state.mergedData;
const itemHeight = props.itemHeight;
if (typeof arg === 'number') {
syncScrollTop(arg);
} else if (arg && typeof arg === 'object') {
let index;
let index: number;
const { align } = arg;
if ('index' in arg) {
({ index } = arg);
} else {
index = data.findIndex(item => getKey(item) === arg.key);
index = data.findIndex((item: object) => getKey(item) === arg.key);
}
const { offset = 0 } = arg;
// We will retry 3 times in case dynamic height shaking
const syncScroll = (times, targetAlign) => {
if (times < 0 || !containerRef.current) return;
const syncScroll = (times: number, targetAlign?: 'top' | 'bottom') => {
if (times < 0 || !containerRef.value) return;
const height = containerRef.current.clientHeight;
const height = containerRef.value.clientHeight;
let needCollectHeight = false;
let newTargetAlign = targetAlign;
@ -51,7 +53,7 @@ export default function useScrollTo(
for (let i = 0; i <= index; i += 1) {
const key = getKey(data[i]);
itemTop = stackTop;
const cacheHeight = heights[key];
const cacheHeight = heights[key!];
itemBottom = itemTop + (cacheHeight === undefined ? itemHeight : cacheHeight);
stackTop = itemBottom;
@ -62,7 +64,7 @@ export default function useScrollTo(
}
// Scroll to
let targetTop = null;
let targetTop: number | null = null;
switch (mergedAlign) {
case 'top':
@ -73,7 +75,7 @@ export default function useScrollTo(
break;
default: {
const { scrollTop } = containerRef.current;
const { scrollTop } = containerRef.value;
const scrollBottom = scrollTop + height;
if (itemTop < scrollTop) {
newTargetAlign = 'top';
@ -83,7 +85,7 @@ export default function useScrollTo(
}
}
if (targetTop !== null && targetTop !== containerRef.current.scrollTop) {
if (targetTop !== null && targetTop !== containerRef.value.scrollTop) {
syncScrollTop(targetTop);
}
}

View File

@ -0,0 +1,14 @@
import { CSSProperties, VNodeTypes } from 'vue';
import { Key } from '../_util/type';
export type RenderFunc<T> = (
item: T,
index: number,
props: { style?: CSSProperties },
) => VNodeTypes;
export interface SharedConfig<T> {
getKey: (item: T) => Key;
}
export type GetKey<T = object> = (item: T) => Key;