Skip to content

Commit

Permalink
feat(cdk:scroll): virtual scrollTo supports horizontal target
Browse files Browse the repository at this point in the history
BREAKING CHANGE: scrollTo option index is deprecated, use rowIndex instead
BREAKING CHANGE: scrollTo option offset is deprecated, use verticalOffset instead
BREAKING CHANGE: scrollTo option key is deprecated, use rowKey instead
BREAKING CHANGE: scrollTo option align is deprecated, use verticalAlign instead
  • Loading branch information
sallerli1 committed Jan 25, 2024
1 parent 84140cf commit 437c034
Show file tree
Hide file tree
Showing 8 changed files with 460 additions and 128 deletions.
@@ -1,8 +1,8 @@
// Vitest Snapshot v1

exports[`VirtualScroll > basic work > render work 1`] = `
"<div class=\\"cdk-virtual-scroll cdk-virtual-scroll-overflowed-vertical\\">
<div class=\\"cdk-virtual-scroll-holder\\" style=\\"max-height: 200px; width: 100%; overflow-x: auto; overflow-y: auto;\\">
"<div class=\\"cdk-virtual-scroll\\">
<div class=\\"cdk-virtual-scroll-holder\\" style=\\"max-height: 200px; width: 200px; overflow-x: auto; overflow-y: auto;\\">
<!---->
<div class=\\"cdk-virtual-scroll-filler-vertical\\" style=\\"height: 400px; width: 0px;\\"></div>
<div class=\\"cdk-virtual-scroll-content\\" style=\\"margin-top: 0px; margin-left: 0px;\\"><span class=\\"virtual-item\\" style=\\"height: 20px;\\">key-0 - 0</span><span class=\\"virtual-item\\" style=\\"height: 20px;\\">key-1 - 1</span><span class=\\"virtual-item\\" style=\\"height: 20px;\\">key-2 - 2</span><span class=\\"virtual-item\\" style=\\"height: 20px;\\">key-3 - 3</span><span class=\\"virtual-item\\" style=\\"height: 20px;\\">key-4 - 4</span><span class=\\"virtual-item\\" style=\\"height: 20px;\\">key-5 - 5</span><span class=\\"virtual-item\\" style=\\"height: 20px;\\">key-6 - 6</span><span class=\\"virtual-item\\" style=\\"height: 20px;\\">key-7 - 7</span><span class=\\"virtual-item\\" style=\\"height: 20px;\\">key-8 - 8</span><span class=\\"virtual-item\\" style=\\"height: 20px;\\">key-9 - 9</span><span class=\\"virtual-item\\" style=\\"height: 20px;\\">key-10 - 10</span></div>
Expand All @@ -13,8 +13,8 @@ exports[`VirtualScroll > basic work > render work 1`] = `
`;
exports[`VirtualScroll > basic work > rowRender work 1`] = `
"<div class=\\"cdk-virtual-scroll cdk-virtual-scroll-overflowed-vertical\\">
<div class=\\"cdk-virtual-scroll-holder\\" style=\\"max-height: 200px; width: 100%; overflow-x: auto; overflow-y: auto;\\">
"<div class=\\"cdk-virtual-scroll\\">
<div class=\\"cdk-virtual-scroll-holder\\" style=\\"max-height: 200px; width: 200px; overflow-x: auto; overflow-y: auto;\\">
<!---->
<div class=\\"cdk-virtual-scroll-filler-vertical\\" style=\\"height: 400px; width: 0px;\\"></div>
<div class=\\"cdk-virtual-scroll-content\\" style=\\"margin-top: 0px; margin-left: 0px;\\"><span class=\\"virtual-item\\">key-0 - 0</span><span class=\\"virtual-item\\">key-1 - 1</span><span class=\\"virtual-item\\">key-2 - 2</span><span class=\\"virtual-item\\">key-3 - 3</span><span class=\\"virtual-item\\">key-4 - 4</span><span class=\\"virtual-item\\">key-5 - 5</span><span class=\\"virtual-item\\">key-6 - 6</span><span class=\\"virtual-item\\">key-7 - 7</span><span class=\\"virtual-item\\">key-8 - 8</span><span class=\\"virtual-item\\">key-9 - 9</span><span class=\\"virtual-item\\">key-10 - 10</span></div>
Expand Down
138 changes: 103 additions & 35 deletions packages/cdk/scroll/__tests__/virtualScroll.spec.ts
Expand Up @@ -2,7 +2,7 @@ import { MountingOptions, VueWrapper, flushPromises, mount } from '@vue/test-uti
import { h } from 'vue'

import VirtualScroll from '../src/virtual/VirtualScroll'
import { VirtualRowRenderFn, VirtualScrollInstance, VirtualScrollProps } from '../src/virtual/types'
import { VirtualColRenderFn, VirtualRowRenderFn, VirtualScrollInstance, VirtualScrollProps } from '../src/virtual/types'

const getData = (length: number, key = 'key') => {
const data: { key: string }[] = []
Expand All @@ -11,14 +11,29 @@ const getData = (length: number, key = 'key') => {
}
return data
}
const getGridData = (
rowLength: number,
colLength: number,
rowKey = 'row',
colKey = 'col',
): { key: string; data: { key: string }[] }[] => {
const data = Array.from(new Array(rowLength)).map((_, rowIndex) => ({
key: `${rowKey}-${rowIndex}`,
data: Array.from(new Array(colLength)).map((_, colIndex) => ({ key: `${colKey}-${colIndex}` })),
}))

return data
}

const defaultProps = {
height: 200,
width: 200,
rowHeight: 20,
colWidth: 20,
getKey: 'key',
} as const

const defaultItemSlot = `
const defaultRowSlot = `
<template #row="{ item, index }">
<span class="virtual-item" style="height: 20px;">{{ item.key }} - {{ index }}</span>
</template>
Expand All @@ -27,28 +42,37 @@ const defaultItemSlot = `
describe('VirtualScroll', () => {
const VirtualScrollMount = (options?: MountingOptions<Partial<VirtualScrollProps>>) => {
const { props, ...rest } = options || {}
const mergedOptions = { props: { ...defaultProps, ...props }, ...rest } as MountingOptions<VirtualScrollProps>
const scrollHeight = (props?.dataSource?.length ?? 0) * 20

vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockImplementation(() => scrollHeight)

return mount(VirtualScroll, mergedOptions) as VueWrapper<VirtualScrollInstance>
const mergedOptions = {
props: { ...defaultProps, ...props },
...rest,
} as MountingOptions<VirtualScrollProps>
const rowLength = props?.dataSource?.length
const scrollHeight = rowLength ? rowLength * 20 : 200
let colLength = 0

const row = props?.dataSource?.[0] as { data: unknown[] } | undefined

if (row?.data) {
colLength = row.data.length
}
const scrollWidth = colLength ? colLength * 20 : 200

const wrapper = mount(VirtualScroll, mergedOptions) as VueWrapper<VirtualScrollInstance>
const holderEl = wrapper.vm.getHolderElement()

vi.spyOn(holderEl, 'scrollHeight', 'get').mockImplementation(() => scrollHeight)
vi.spyOn(holderEl, 'scrollWidth', 'get').mockImplementation(() => scrollWidth)
vi.spyOn(holderEl, 'clientHeight', 'get').mockImplementation(() => 200)
vi.spyOn(holderEl, 'clientWidth', 'get').mockImplementation(() => 200)

return wrapper
}

beforeAll(() => {
vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 200)
})

afterEach(() => {
vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockClear()
vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockClear()
})

describe('basic work', () => {
test('render work', async () => {
const wrapper = VirtualScrollMount({
props: { dataSource: getData(20) },
slots: { item: defaultItemSlot },
slots: { row: defaultRowSlot },
})

expect(wrapper.html()).toMatchSnapshot()
Expand All @@ -62,7 +86,7 @@ describe('VirtualScroll', () => {
test.skip('tag work', async () => {
const wrapper = VirtualScrollMount({
props: { dataSource: getData(20) },
slots: { item: defaultItemSlot },
slots: { item: defaultRowSlot },
})

expect(wrapper.find('.cdk-virtual-scroll-holder').element.tagName).toEqual('DIV')
Expand All @@ -80,7 +104,7 @@ describe('VirtualScroll', () => {
test('dataSource work', async () => {
const wrapper = VirtualScrollMount({
props: { dataSource: getData(5) },
slots: { item: defaultItemSlot },
slots: { item: defaultRowSlot },
})

expect(wrapper.findAll('.virtual-item').length).toEqual(5)
Expand All @@ -97,7 +121,7 @@ describe('VirtualScroll', () => {
test('fullHeight work', async () => {
const wrapper = VirtualScrollMount({
props: { dataSource: getData(5) },
slots: { item: defaultItemSlot },
slots: { item: defaultRowSlot },
})

expect(wrapper.find('.cdk-virtual-scroll-holder').attributes('style')).toContain('height: 200px')
Expand All @@ -114,7 +138,7 @@ describe('VirtualScroll', () => {
test('height work', async () => {
const wrapper = VirtualScrollMount({
props: { dataSource: getData(5) },
slots: { item: defaultItemSlot },
slots: { item: defaultRowSlot },
})

expect(wrapper.find('.cdk-virtual-scroll-holder').attributes('style')).toContain('height: 200px')
Expand All @@ -134,7 +158,7 @@ describe('VirtualScroll', () => {
test('itemHeight work', async () => {
const wrapper = VirtualScrollMount({
props: { dataSource: getData(5) },
slots: { item: defaultItemSlot },
slots: { item: defaultRowSlot },
})

expect(wrapper.findAll('.virtual-item').length).toEqual(5)
Expand All @@ -151,7 +175,7 @@ describe('VirtualScroll', () => {
test('getKey work', async () => {
const wrapper = VirtualScrollMount({
props: { dataSource: getData(20) },
slots: { item: defaultItemSlot },
slots: { item: defaultRowSlot },
})

wrapper.findAll('.virtual-item').forEach((item, index) => expect(item.text()).toEqual(`key-${index} - ${index}`))
Expand All @@ -176,56 +200,100 @@ describe('VirtualScroll', () => {

describe('scroll work', () => {
test('scrollTo work', async () => {
const rowRender: VirtualRowRenderFn = ({ children }) => {
return h('div', { class: 'virtual-row' }, children)
}
const colRender: VirtualColRenderFn = ({ item, index }) => {
const { key } = item as { key: string }

return h('div', { class: 'virtual-col' }, [`${key}-${index}`])
}

vi.useFakeTimers()

const wrapper = VirtualScrollMount({
props: { dataSource: getData(40) },
slots: { item: defaultItemSlot },
props: { dataSource: getGridData(40, 40), rowRender, colRender },
})
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(0)

wrapper.vm.scrollTo(100)
vi.runAllTimers()

expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(100)

wrapper.vm.scrollTo({ index: 20, align: 'top' })
wrapper.vm.scrollTo({ rowIndex: 20, verticalAlign: 'top' })
vi.runAllTimers()

expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(400)

wrapper.vm.scrollTo({ index: 20, align: 'bottom' })
wrapper.vm.scrollTo({ rowIndex: 20, verticalAlign: 'bottom' })
vi.runAllTimers()

expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(220)

wrapper.vm.scrollTo({ key: 'key-20', align: 'top' })
wrapper.vm.scrollTo({ rowKey: 'row-20', verticalAlign: 'top' })
vi.runAllTimers()
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(400)

wrapper.vm.scrollTo({ rowKey: 'row-20', verticalAlign: 'top', verticalOffset: 20 })
vi.runAllTimers()
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(380)

wrapper.vm.scrollTo({ top: 100, left: 100 })
vi.runAllTimers()
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(100)
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollLeft).toEqual(100)

wrapper.vm.scrollTo({ rowIndex: 20, colIndex: 20, verticalAlign: 'top', horizontalAlign: 'start' })
vi.runAllTimers()
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(400)
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollLeft).toEqual(400)

wrapper.vm.scrollTo({ key: 'key-20', align: 'top', offset: 20 })
wrapper.vm.scrollTo({ rowIndex: 20, colIndex: 20, verticalAlign: 'bottom', horizontalAlign: 'end' })
vi.runAllTimers()
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(220)
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollLeft).toEqual(220)

wrapper.vm.scrollTo({ rowKey: 'row-20', colKey: 'col-20', verticalAlign: 'top', horizontalAlign: 'start' })
vi.runAllTimers()
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(400)
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollLeft).toEqual(400)

wrapper.vm.scrollTo({
rowKey: 'row-20',
colKey: 'col-20',
verticalAlign: 'top',
horizontalAlign: 'start',
verticalOffset: 20,
horizontalOffset: 20,
})
vi.runAllTimers()
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(380)
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollLeft).toEqual(380)

wrapper.vm.scrollTo(9999)
vi.runAllTimers()
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(600)

wrapper.vm.scrollTo({ top: 9999, left: 9999 })
vi.runAllTimers()
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(600)
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollLeft).toEqual(600)

wrapper.vm.scrollTo(-1)
vi.runAllTimers()
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(0)

wrapper.vm.scrollTo({ top: -1, left: -1 })
vi.runAllTimers()
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollTop).toEqual(0)
expect(wrapper.find('.cdk-virtual-scroll-holder').element.scrollLeft).toEqual(0)

vi.useRealTimers()
})

test('onScroll work', async () => {
const onScroll = vi.fn()
const wrapper = VirtualScrollMount({
props: { dataSource: getData(100) },
slots: { item: defaultItemSlot },
slots: { item: defaultRowSlot },
attrs: { onScroll },
})

Expand All @@ -239,7 +307,7 @@ describe('VirtualScroll', () => {
test('moving work', async () => {
const wrapper = VirtualScrollMount({
props: { dataSource: getData(100) },
slots: { item: defaultItemSlot },
slots: { item: defaultRowSlot },
})

wrapper.find('.cdk-virtual-scroll-holder').trigger('touchstart', { touches: [{ pageY: 100 }] })
Expand Down
10 changes: 6 additions & 4 deletions packages/cdk/scroll/demo/Basic.vue
Expand Up @@ -19,11 +19,13 @@

<IxSpace>
<IxButton @click="scrollTo(100)"> Scroll To 100px </IxButton>
<IxButton @click="scrollTo({ key: 'key-50', align: 'top' })"> Scroll To key-50(top) </IxButton>
<IxButton @click="scrollTo({ rowKey: 'key-50', verticalAlign: 'top' })"> Scroll To key-50(top) </IxButton>
<IxButton @click="scrollTo({ index: 40, align: 'top' })"> Scroll To 40(top) </IxButton>
<IxButton @click="scrollTo({ index: 40, align: 'bottom' })"> Scroll To 40(bottom) </IxButton>
<IxButton @click="scrollTo({ index: 40, align: 'auto' })"> Scroll To 40(auto) </IxButton>
<IxButton @click="scrollTo({ index: 40, align: 'top', offset: 15 })"> Scroll To 40(top) + 15 offset </IxButton>
<IxButton @click="scrollTo({ rowIndex: 40, verticalAlign: 'bottom' })"> Scroll To 40(bottom) </IxButton>
<IxButton @click="scrollTo({ rowIndex: 40, verticalAlign: 'auto' })"> Scroll To 40(auto) </IxButton>
<IxButton @click="scrollTo({ rowIndex: 40, verticalAlign: 'top', offset: 15 })">
Scroll To 40(top) + 15 offset
</IxButton>
</IxSpace>
</div>
</template>
Expand Down
40 changes: 39 additions & 1 deletion packages/cdk/scroll/demo/Both.vue
Expand Up @@ -14,13 +14,49 @@
<span class="virtual-item" @click="onItemClick(item.key)">{{ row.key }} - {{ index }}</span>
</template>
</CdkVirtualScroll>
<IxSpace>
<IxButton @click="scrollTo({ top: 100, left: 100 })"> Scroll To [100px, 100px] </IxButton>
<IxButton
@click="scrollTo({ rowKey: 'row-key-50', colKey: 'col-key-50', verticalAlign: 'top', horizontalAlign: 'end' })"
>
Scroll To [row-key-50, col-key-50](top, end)
</IxButton>
<IxButton @click="scrollTo({ rowIndex: 40, colIndex: 40, verticalAlign: 'top', horizontalAlign: 'start' })">
Scroll To [40, 40](top, start)
</IxButton>
<IxButton @click="scrollTo({ rowIndex: 40, colIndex: 40, verticalAlign: 'bottom', horizontalAlign: 'end' })">
Scroll To [40, 40](bottom, end)
</IxButton>
<IxButton @click="scrollTo({ rowIndex: 40, colIndex: 40, verticalAlign: 'auto', horizontalAlign: 'auto' })">
Scroll To [40, 40](auto)
</IxButton>
<IxButton
@click="
scrollTo({
rowIndex: 100,
colIndex: 100,
verticalAlign: 'top',
horizontalAlign: 'end',
verticalOffset: 15,
horizontalOffset: 15,
})
"
>
Scroll To [100, 100](top, start) + 15 offset
</IxButton>
</IxSpace>
</div>
</template>

<script setup lang="ts">
import { h, ref } from 'vue'
import { VirtualRowRenderFn, VirtualScrollInstance, VirtualScrollRowData } from '@idux/cdk/scroll'
import {
VirtualRowRenderFn,
VirtualScrollInstance,
VirtualScrollRowData,
VirtualScrollToOptions,
} from '@idux/cdk/scroll'
const listRef = ref<VirtualScrollInstance>()
const colData: { key: string }[] = []
Expand All @@ -47,6 +83,8 @@ const rowRender: VirtualRowRenderFn = ({ children }) =>
const onItemClick = (key: string) => {
console.log('click:', key)
}
const scrollTo = (value: number | VirtualScrollToOptions) => listRef.value?.scrollTo(value)
</script>

<style lang="less">
Expand Down

0 comments on commit 437c034

Please sign in to comment.