Skip to content

Commit

Permalink
fix(comp:table): the scroll body is incorrect, when virtual enabled
Browse files Browse the repository at this point in the history
  • Loading branch information
danranVm committed Sep 29, 2022
1 parent c0935b2 commit 59e4ba5
Show file tree
Hide file tree
Showing 10 changed files with 118 additions and 90 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@
"tslib": "^2.4.0",
"typescript": "^4.7.4",
"unplugin-vue-components": "^0.22.4",
"vite": "^3.1.0",
"vite": "3.1.3",
"vitest": "^0.21.1",
"vue": "^3.2.39",
"yaml-front-matter": "^4.1.1"
Expand Down
2 changes: 1 addition & 1 deletion packages/cdk/scroll/src/virtual/VirtualScroll.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export default defineComponent({
})

const scrollTo = useScrollTo(props, holderRef, getKey, heights, collectHeights, syncScrollTop)
expose({ scrollTo })
expose({ scrollTo, holderRef })

const mergedData = computed(() => props.dataSource.slice(startIndex.value, endIndex.value + 1))
watch(mergedData, data => callEmit(props.onScrolledChange, startIndex.value, endIndex.value, data))
Expand Down
18 changes: 5 additions & 13 deletions packages/cdk/scroll/src/virtual/contents/Holder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@

import { type CSSProperties, computed, defineComponent, inject, onBeforeUnmount, onMounted, ref } from 'vue'

import { isString, throttle } from 'lodash-es'
import { throttle } from 'lodash-es'

import { offResize, onResize } from '@idux/cdk/resize'
import { convertCssPixel } from '@idux/cdk/utils'

import { virtualScrollToken } from '../token'

Expand All @@ -28,21 +29,12 @@ export default defineComponent({

const style = computed<CSSProperties | undefined>(() => {
const { height, fullHeight } = props
if (!height) {
if (!height || height <= 0) {
return undefined
}

if (isString(height)) {
return { height }
return {
[fullHeight ? 'height' : 'maxHeight']: convertCssPixel(height),
}

/* eslint-disable indent */
return height <= 0
? undefined
: {
[fullHeight ? 'height' : 'maxHeight']: height + 'px',
}
/* eslint-enable indent */
})

const fillerStyle = computed<CSSProperties | undefined>(() => {
Expand Down
2 changes: 1 addition & 1 deletion packages/components/table/docs/Api.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ export type TablePaginationPosition = 'topStart' | 'top' | 'topEnd' | 'bottomSta
| 名称 | 说明 | 参数类型 | 备注 |
| --- | --- | --- | --- |
| `scrollTo` | 滚动到指定位置 | `(option?: number \| VirtualScrollToOptions) => void` | `virtual` 模式下可用 |
| `scrollTo` | 滚动到指定高度或位置 | `(option?: number \| VirtualScrollToOptions) => void` | `VirtualScrollToOptions` 参数仅 `virtual` 模式下可用 |
### IxTableColumn
Expand Down
2 changes: 1 addition & 1 deletion packages/components/table/src/Table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ export default defineComponent({
)
const selectableContext = useSelectable(props, locale, columnsContext.flattedColumns, dataContext)

useScrollOnChange(props, config, scrollContext.scrollBodyRef, mergedPagination, activeSorters, activeFilters)
useScrollOnChange(props, config, mergedPagination, activeSorters, activeFilters, scrollContext.scrollTo)

const context = {
props,
Expand Down
144 changes: 96 additions & 48 deletions packages/components/table/src/composables/useScroll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
* found in the LICENSE file at https://github.com/IDuxFE/idux/blob/main/LICENSE
*/

import { type ComputedRef, type Ref, computed, onBeforeUnmount, ref } from 'vue'
import { type ComputedRef, type Ref, computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'

import { useResizeObserver } from '@idux/cdk/resize'
import { type VirtualScrollInstance, type VirtualScrollToFn, getScrollBarSize } from '@idux/cdk/scroll'
import { type VirtualScrollInstance, type VirtualScrollToFn, getScrollBarSize, scrollToTop } from '@idux/cdk/scroll'
import { Logger, convertCssPixel, convertElement } from '@idux/cdk/utils'

import { type TableProps } from '../types'
Expand All @@ -19,8 +19,25 @@ export function useScroll(
mergedAutoHeight: ComputedRef<boolean>,
{ setStickyScrollLeft }: StickyContext,
): ScrollContext {
const { scrollHeadRef, scrollBodyRef, scrollContentRef, scrollFootRef, handleScroll, pingedStart, pingedEnd } =
useScrollRef(setStickyScrollLeft)
const virtualScrollRef = ref<VirtualScrollInstance | undefined>()
const scrollHeadRef = ref<HTMLDivElement>()
const scrollBodyRef = ref<HTMLDivElement>()
const scrollContentRef = ref<HTMLDivElement>()
const scrollFootRef = ref<HTMLDivElement>()

watch(virtualScrollRef, instance => {
if (instance) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
scrollBodyRef.value = (instance as any).holderRef.value
}
})

const { handleScroll, pingedStart, pingedEnd } = useScrollRef(
scrollHeadRef,
scrollBodyRef,
scrollFootRef,
setStickyScrollLeft,
)

__DEV__ &&
props.scroll?.x &&
Expand All @@ -30,33 +47,29 @@ export function useScroll(
props.scroll?.y &&
Logger.warn('components/table', '`scroll.y` was deprecated, please use `scroll.height` instead')

const scrollWithAutoHeight = ref(mergedAutoHeight.value)
const calcScrollWithAutoHeight = () => {
const bodyEl = convertElement(scrollBodyRef.value)
if (!mergedAutoHeight.value || !bodyEl) {
scrollWithAutoHeight.value = false
} else {
scrollWithAutoHeight.value = props.virtual || bodyEl.scrollHeight > bodyEl.clientHeight
}
}
useResizeObserver(scrollBodyRef, calcScrollWithAutoHeight)
useResizeObserver(scrollContentRef, calcScrollWithAutoHeight)

const scrollWithAutoHeight = useScrollWithAutoHeight(props, mergedAutoHeight, scrollBodyRef, scrollContentRef)
const scrollWidth = computed(() => convertCssPixel(props.scroll?.width || props.scroll?.x))
const scrollHeight = computed(
() => convertCssPixel(props.scroll?.height || props.scroll?.y) || (scrollWithAutoHeight.value ? 'auto' : ''),
)
const scrollHeight = computed(() => {
let height = convertCssPixel(props.scroll?.height || props.scroll?.y)
if (!height && mergedAutoHeight.value && (props.virtual || scrollWithAutoHeight.value)) {
height = 'auto'
}
return height
})

const scrollBarSize = computed(() => getScrollBarSize(convertElement(scrollBodyRef)))
const scrollBarSizeOnFixedHolder = computed(() => (scrollHeight.value ? scrollBarSize.value : 0))

const scrollTo: VirtualScrollToFn = options => {
if (props.virtual) {
return (scrollBodyRef.value as unknown as VirtualScrollInstance)?.scrollTo(options)
virtualScrollRef.value?.scrollTo(options)
} else {
scrollToTop({ target: convertElement(scrollBodyRef), top: 0, duration: 200 })
}
}

return {
virtualScrollRef,
scrollHeadRef,
scrollBodyRef,
scrollContentRef,
Expand All @@ -73,9 +86,10 @@ export function useScroll(
}

export interface ScrollContext {
virtualScrollRef: Ref<VirtualScrollInstance | undefined>
scrollHeadRef: Ref<HTMLDivElement | undefined>
scrollBodyRef: Ref<HTMLDivElement | undefined>
scrollContentRef: Ref<HTMLTableElement | undefined>
scrollContentRef: Ref<HTMLDivElement | undefined>
scrollFootRef: Ref<HTMLDivElement | undefined>
handleScroll: (evt?: Event, scrollLeft?: number) => void
scrollTo: VirtualScrollToFn
Expand All @@ -92,26 +106,15 @@ export interface ScrollOptions {
scrollLeft?: number
}

function useScrollRef(setStickyScrollLeft: (value: number) => void) {
const scrollHeadRef = ref<HTMLDivElement>()
const scrollBodyRef = ref<HTMLDivElement>()
const scrollContentRef = ref<HTMLTableElement>()
const scrollFootRef = ref<HTMLDivElement>()

const changeStickyScrollLeft = (scrollLeft: number) => {
const scrollBodyElement = convertElement(scrollBodyRef)
if (!scrollBodyElement) {
return
}
const { clientWidth, scrollWidth } = scrollBodyElement
setStickyScrollLeft((scrollLeft / scrollWidth) * clientWidth || 0)
}

function useScrollRef(
scrollHeadRef: Ref<HTMLDivElement | undefined>,
scrollBodyRef: Ref<HTMLDivElement | undefined>,
scrollFootRef: Ref<HTMLDivElement | undefined>,
setStickyScrollLeft: (value: number) => void,
) {
const pingedStart = ref(false)
const pingedEnd = ref(false)

const lockedScrollTargetRef = ref<HTMLElement>()

let timeout: number | undefined

const clearTimer = () => {
Expand All @@ -121,6 +124,10 @@ function useScrollRef(setStickyScrollLeft: (value: number) => void) {
}
}

onBeforeUnmount(() => clearTimer())

const lockedScrollTargetRef = ref<HTMLElement>()

const lockScrollTarget = (target: HTMLElement | undefined) => {
lockedScrollTargetRef.value = target
clearTimer()
Expand All @@ -131,8 +138,6 @@ function useScrollRef(setStickyScrollLeft: (value: number) => void) {
}, 100)
}

onBeforeUnmount(() => clearTimer())

const forceScroll = (scrollLeft: number, target: HTMLElement | undefined) => {
if (!target) {
return
Expand All @@ -142,6 +147,15 @@ function useScrollRef(setStickyScrollLeft: (value: number) => void) {
}
}

const changeStickyScrollLeft = (scrollLeft: number) => {
const scrollBodyElement = convertElement(scrollBodyRef)
if (!scrollBodyElement) {
return
}
const { clientWidth, scrollWidth } = scrollBodyElement
setStickyScrollLeft((scrollLeft / scrollWidth) * clientWidth || 0)
}

const handleScroll = (evt?: Event, scrollLeft?: number) => {
const currentTarget = evt?.currentTarget as HTMLElement | undefined
const mergedScrollLeft = scrollLeft ?? currentTarget!.scrollLeft
Expand All @@ -162,13 +176,47 @@ function useScrollRef(setStickyScrollLeft: (value: number) => void) {
}
}

return {
scrollHeadRef,
scrollBodyRef,
scrollContentRef,
scrollFootRef,
handleScroll,
pingedStart,
pingedEnd,
return { handleScroll, pingedStart, pingedEnd }
}

// 针对非虚拟滚动场景,需要判断 scrollHeight 和 clientHeight
function useScrollWithAutoHeight(
props: TableProps,
mergedAutoHeight: ComputedRef<boolean>,
scrollBodyRef: Ref<HTMLDivElement | undefined>,
scrollContentRef: Ref<HTMLDivElement | undefined>,
) {
const scrollWithAutoHeight = ref(true)

const calcScrollWithAutoHeight = () => {
const bodyEl = convertElement(scrollBodyRef.value)
if (!bodyEl) {
scrollWithAutoHeight.value = false
} else {
scrollWithAutoHeight.value = bodyEl.scrollHeight > bodyEl.clientHeight
}
}

let stopResizeObservers: Array<() => void> = []
const stopHandler = () => stopResizeObservers.forEach(stop => stop())

onMounted(() => {
watch(
[mergedAutoHeight, () => props.virtual],
([autoHeight, virtual]) => {
stopHandler()
if (autoHeight && !virtual) {
stopResizeObservers = [
useResizeObserver(scrollBodyRef, calcScrollWithAutoHeight).stop,
useResizeObserver(scrollContentRef, calcScrollWithAutoHeight).stop,
]
}
},
{ immediate: true },
)
})

onBeforeUnmount(() => stopHandler())

return scrollWithAutoHeight
}
31 changes: 9 additions & 22 deletions packages/components/table/src/composables/useScrollOnChange.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,48 +10,35 @@ import type { ActiveFilter } from './useFilterable'
import type { ActiveSorter } from './useSortable'
import type { TableConfig } from '@idux/components/config'

import { type ComputedRef, type Ref, type WatchStopHandle, computed, watch } from 'vue'
import { type ComputedRef, type WatchStopHandle, computed, watch } from 'vue'

import { type VirtualScrollInstance, scrollToTop } from '@idux/cdk/scroll'
import { type VirtualScrollToFn } from '@idux/cdk/scroll'

export function useScrollOnChange(
props: TableProps,
config: TableConfig,
scrollBodyRef: Ref<HTMLElement | VirtualScrollInstance | undefined>,
mergedPagination: ComputedRef<TablePagination | null>,
activeSorters: ComputedRef<ActiveSorter[]>,
activeFilters: ComputedRef<ActiveFilter[]>,
scrollTo: VirtualScrollToFn,
): void {
const mergedScrollToTopOnChange = computed(() => props.scrollToTopOnChange ?? config.scrollToTopOnChange)

let stopOnChangeWatch: WatchStopHandle | undefined
const startOnChangeWatch = () => {
stopOnChangeWatch = watch(
const startOnChangeWatch = () =>
watch(
[() => mergedPagination.value?.pageIndex, () => mergedPagination.value?.pageSize, activeSorters, activeFilters],
() => {
if (!scrollBodyRef.value) {
return
}

if (props.virtual) {
;(scrollBodyRef.value as VirtualScrollInstance).scrollTo(0)
} else {
scrollToTop({
target: scrollBodyRef.value as HTMLElement,
top: 0,
})
}
},
() => scrollTo(0),
)
}

let stopOnChangeWatch: WatchStopHandle | undefined

watch(
mergedScrollToTopOnChange,
scrollToTopOnChange => {
stopOnChangeWatch?.()

if (scrollToTopOnChange) {
startOnChangeWatch()
stopOnChangeWatch = startOnChangeWatch()
}
},
{ immediate: true },
Expand Down
3 changes: 2 additions & 1 deletion packages/components/table/src/main/MainTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export default defineComponent({
flattedData,
isSticky,
mergedSticky,
virtualScrollRef,
scrollBodyRef,
scrollContentRef,
handleScroll,
Expand Down Expand Up @@ -184,7 +185,7 @@ export default defineComponent({

children.push(
<CdkVirtualScroll
ref={scrollBodyRef}
ref={virtualScrollRef}
style={contentStyle.value}
dataSource={flattedData.value}
fullHeight={scroll?.fullHeight}
Expand Down
2 changes: 1 addition & 1 deletion packages/components/transfer/src/list/List.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ export default defineComponent({
})

const transferListApi: TransferListApi = {
scrollTo: (...params) => virtualScrollRef.value?.scrollTo(...params),
scrollTo: option => virtualScrollRef.value?.scrollTo(option),
}

expose(transferListApi)
Expand Down
2 changes: 1 addition & 1 deletion packages/components/tree/src/Tree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ export default defineComponent({
inputRef?.value?.blur()
}
const scrollTo = (option?: number | VirtualScrollToOptions) => {
virtualScrollRef?.value?.scrollTo(option)
virtualScrollRef.value?.scrollTo(option)
}

const getNode = (key: VKey) => {
Expand Down

0 comments on commit 59e4ba5

Please sign in to comment.